待到秋来九月八,我花开后百花杀
C语言的特殊变量——指针
相信大家在刚入手C语言是,对指针的概念相当模糊。首先我们要使用指针,必须要了解指针到底是什么?
指针是什么
在计算机科学中,指针(pointer)是编程语言中的一个对象,利用地址,他的值只想存在电脑储存其中另一个地方的值。由于通过地址能找到所需变量单元,可以说,地址指向该变量单元。因此,将地址形象化地成为“指针”。意思是可以通过他找到以他为地址的内存地址单元。
也就是说,指针就是一个用来专门储存计算机储存地址的变量,应该与char、int、float、等一样被归类为变量,准确的不该简单的称为指针,而应该叫做指针变量,只不过它被我们像称int为整型一样简称为了“指针”。
那么上述谈到的内存地址又是什么呢?
内存
最初计算机被制造时,就被数学家冯·诺伊曼规定了一套储存架构,被称为冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同,如英特尔公司的8086中央处理器的程序指令和数据都是16位宽。
我们可以形象的将内存理解为宿舍楼,数据理解为同学,当你要找一个同学的所在宿舍时,我们就需要对方提供给你相应的宿舍楼号、楼层以及宿舍号等信息组成的编码,这样我们就能够更加方便快捷的找到目标同学的位置。
同理计算机的储存也是如此,每个数据的储存是有地址编号的。
那么内存一个单元存储多大的数据呢?如何编址呢?指针变量又有多大呢?
编址
经过仔细地计算和衡量我们发现一个储存单元的大小是1字节,也就是8bit是比较合适的。
对于32位的机器,假设有32根地址线,那么假设没根地址线在寻址的是产生一个电信号(1/0)
那么对32位的编址就是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
111111111 111111111 111111111 111111111
这里就有了2的32次方个地址了。
每个地址标记一个字节的数据,那么我们就可以有了2^ 32也就是4GB(2^ 32Byte = 2^ 32/1024KB = 2^ 32/1024/1024MB = 2^ 32/1024/1024/1024GB = 4GB)的空闲进行编址了。
到此我们应该明白了:
- 在32位的机器上,地址是32位二进制序列,那么地址就需要使用4字节的空间来存储,所以一个指针变量大小就是4字节。同理,64位的机器是8字节。
- 同时我们也可以更好的选择计算机内存条和操作系统,以便让计算机达到更好的效率,例如32位适配4GB的内存条,而64位计算下来已经达到16777216TB了,相信日常中我们是没有这么大的内存,所以即使操作系统设计真能达到64位,硬件也是做不到的,具体是如何的呢?
参见:https://www.crucial.cn/learn-with-crucial/memory/how-much-memory-does-your-windows-support
指针类型
指针作为一个特殊的变量类型,它自己也有不用的类型分类:
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
这里我们也就知道了,指针的定义是“ type ”+“ * ”。各种类型的指针存放对应类型数据的地址。
很多大佬用C语言编程喜欢像char *pc
这样定义指针,其实个人认为char* pc
更能解释指针是变量的定义。
多级指针
根据以上,我们知道指针是存储地址的变量,那么既然是变量,它就可以储存它自己本身类型的其他指针变量的地址,这就形成了多级指针。
同样,那么多级指针的“ type ”就是与它定义本身所指向的变量类型。
例如:
int **pp
的“ type ”就是int*
。
指针的意义
指针所存的地址
众所周知int、char、float等类型的数据不止一个字节,根据上述的每个存储单元为1字节,每个存储单元都有各自的地址,那么指针存放的是这些类型的哪个地址呢?
我们可以联想数组的地址如何表示:数组是用首元素的地址来表示数组的,那么数据的地址也应该用首地址表示,为了清楚确定是首地址,那么我们规定数据地址中最小的那个为该数据的地址。(同样我们也可以得出结构体的地址就是首元素的最小的地址)
例如:int类型有4字节,那么就有4个地址,使用其中最小的那个地址作为表示自身的地址。
#include<stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n",&n);
printf("%p\n",pc);
printf("%p\n",pi);
return 0;
}
理解了指针内部所存地址是什么之后,因此,也就懂得了上述代码中的&n、pc、pi都应该是相同的。
指针加减整数
理解了指针所存内容,我们才能懂得指针被加减的意义是什么。
#include<stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n",&n);
printf("%p\n",pc);
printf("%p\n",pc + 1);
printf("%p\n",pi);
printf("%p\n",pi + 1);
return 0;
}
我们可以看到,指针加1看上去加了1,实际上它加了一个它所指变量的字节数。
这样也就保证了我们想要对指针操作让它指向该数据下一个地址时,不会再次指向原来的数据
总结:指针的类型决定了指针向前或者向后走的一步有多大
指针的解引用
首先看一个示例代码:
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0;
*pi = 0;
return 0;
}
在平常我们使用指针解引用时,毋庸置疑,*pi
可以被看作n,那么*pi=0;
直接就可以当做n=0;
,但是原理是怎么样的呢?
这个和前面的指针的加减类似,指针的类型将要决定这个指针的解引用能够操作多少字节的数。
也就是说,上例的代码中关键点在于*pc
,这个指针被强转成为了字符指针,它仅仅能够操作一字节的数据,也就是按理论上来说,n中的数据只有1字节被改变了,而*pi
改变了整个n。
但实际上,内存中应该是什么样呢?
这就是n在内存中的地址和其中所存数据的样子。
这里我们将遇到一个大端和小端的问题。
大端机器和小端机器
我们可以看到实际在内存中数据的存储有自己的规律,并不是我们认为的11 22 33 44这样存储。
数据是有权重的,例如11223344,这个数中11要占一个字节,并且权重最高,我们可以看到我的计算机把它放在了高地址位,44权重小放在了低地址位,所以这是个小端机器。
反之,如果把权重高的放在低地址位,就是大端机器。
因此上例的代码中,对于我的电脑,*pc = 0;
改变了n中44的值,最后变成了11223300,*pc指针指向的也应该是44的地址。
总结:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
指针与指针间的关系运算
#include<stdio.h>
int main()
{
int a[10];
int *p = a;
int *q = &a[9];
printf("%d\n",q - p);
return 0;
}
#include<stdio.h>
int main()
{
int a[10];
short *p = a;
short *q = &a[9];
printf("%d\n",q - p);
return 0;
}
通过这两个实例可以看到:
前者说明指针与指针相减,所得的应该是两个指针之间元素的个数。
后者说明这两个指针之间元素个数的计算是以这两个指针的类型所决定的。
那么不同类型指针相减如何呢?
这个是没有意义的,但是也可以编译通过,元素个数的计算主要由被减数决定。
至此我们就可以理解strlen()函数的原理了!
int strlen(char *s)
{
char *p = s;
while(*p != '/0')
p++;
return p - s;
}
甚至我们清空数组内容的问题也变得简单了!
void clean(int *values[N_VALUES])
{
int *vp;
for(vp = &values[0];vp < &values[N_VALUES];){
*vp++ = 0;
}
}
函数指针
首先看一段代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的结果:
输出的是两个地址,这两个地址是 test 函数的地址。也就是说函数在内存上开辟栈帧空间也是有自己的地址的。
那我们的函数的地址要想保存起来,怎么保存?
答案是:
void (*pfun1)();
//pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
函数指针数组
函数指针既然是指针,那么它就是一个指针变量,是变量,那么就可以被储存在数组中。
定义
int (*parr1[10]])();
// parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。
函数指针数组的用途:转移表
例子:(计算器)
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0;
}
指向函数指针数组的指针
上面讲到的函数指针数组,它是一个数组就是变量集,是数组就一定有地址,有地址就一定可以被指针储存,这个就被称为指向函数指针数组的指针。
定义
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[10])(const char*) = &pfunArr;
return 0;
}