前言:
对于许多正在学习C语言的小伙伴来说,指针可能会让你非常的头疼,很多人不知道如何控制指针变量,甚至都不敢用指针来写代码。但是在实际的开发中还是经常会和指针打交道的,今天我们开启C语言指针系列的章节学习~
什么是指针?
1)初识指针
指针是什么?这个我们先不着急,我们先来模拟一个场景:
[事件1] 现在,假设你今天要去西藏旅游,到了地方总得有住的地方,于是你去希望旅馆定了一个房间号为302的房间。你是第一次来这个旅馆并不熟悉房间的排列,所以你得一个一个的去找到你的房间。你按着顺序找到了302的房间,并将行李放了进去… [结束]
现在就可以回答前面的问题了,指针其实就是地址,事件1的房间号就可看作地址,有了房间号,我们就可以找到对应的房间,将行李都放进房间了。指针也是如此:指针通过地址进而找到对应的内存空间从而进行访问。
对于指针:
指针是内存中一个最小单元的编号,也就是地址。
注意:我们平常所说的指针,其实是指 指针变量,是用来存放地址的 变量。
我们也可以这样来理解指针:
通过指针的地址来找到对应的内存空间。
2)地址的大小
我们已经了解了指针就是地址,那么地址又是什么?实际上:
在计算机运行时,数据会存放在内存中,内存会以 字节 为单位划分为多个存储空间,并且为每个字节默认设置一个对应的编号,这个编号就是地址。
可能你还会有疑问:“为什么内存会以字节为单位划分呢?”
其实经过前人的计算与考量,发现一个字节给一个对应的地址是比较合适的。在32的机器上,假设有32根地址线,每根地址线在寻址的时候产生的高低电平就是0和1。
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
00000000 00000000 00000000 00000002
…
111111111 111111111 111111111 111111111
这里就会产生2^32 次方个地址。如果每个地址来标识一个字节,那么我们就能给大约4GB(2^32 Byte = = 2^32 /1024KB = = 2^32/1024/1024MB= =2^32/1024/1024/1024GB = = 4GB)的的空间进行编址了。同样的方式,64位机器能编址多大空间?可以自己算算。
这里我们就知道了:
1、在32位机器上,地址是32个0或1组成的二进制序列,地址有 4个字节 的空间的大小来存储,所以在32位机器下,一个指针变量的大小就是4字节。
2、在64位的机器上,有64个地址线,一个指针变量的大小就是8个字节了。
3)指针变量
还记不记得我们在学C语言中用到的 ‘&’ 符号?是不是很眼熟?没错,我们经常在scanf函数里面用到这个符号,其实这个符号叫做: 取地址操作符 。顾名思义,就是用来提取变量的地址。
我们可以通过& 来取出变量的内存地址,把地址可以存放到一个变量中,这个变量就是指针变量。
#include<stdio.h> int main() { int a = 10;//整形大小为4个字节 int *p = &a;//取变量a的地址赋给指针变量p,虽然整形大小为4个字节 //但是指针存储的仅仅是四个字节中的第一个起始字节 return 0; }
总结:
1、 指针变量是用来存放地址的
2、 在32位平台下,指针大小为4字节,在64位平台下,指针大小为8字节
指针的类型
看到标题你可能会有些疑问:既然我们的指针只能保存一个字节的内容,我们为什么还要给指针分为不同的类型呢?
实际上,我们规定指针这样定义:
int a = 0; int *p = &a; float b = 0; float *pb = &b; double c = 0; doublr *pc = &c; //...
我们可以看到,指针变量的 定义方式为:类型 + * 而指针前面的类型表示指针的类型,我们可以看到,指针的类型有,int,double,float…我们常用的类型都有对应的指针类型。
那这些指针类型究竟有什么用呢?代表什么意思呢?
1)指针对整数加减运算
我们来看这样一段代码:
#include<stdio.h> int main() { int a = 1; char *pa1 = (char *)a;//既然指针只保存一个字节的值,那我们不妨直接把int强转成char //只取int的首个字节的地址进行操作看看会发生什么? int *pa2 = &a;//将未强转的类型也用指针保存,用来做对照 printf("%p\n", &a);//地址打印用%p printf("%p\n", pa1); printf("%p\n", pa1 + 1); printf("%p\n", pa2); printf("%p\n", pa2 + 1); return 0; }
那么结果会是多少呢?
我们发现第一个与第二个和第四个的打印结果是相同的,也就是说他们的起始地址是相同的,第一个和第四个就不用多说,两个是同一个变量取地址打印。
第二个结果也刚好能验证我们指针取的地址是元素的首个字节的地址。
可以看到,pa1 + 1的地址要比pa1的地址大了1(16进制),也就是说pa1向后加一就是往后走一个字节的距离。
再来看pa2与pa2 + 1,这里的差值却为4(十六进制10 - 0C),也就是说pa2加一是跳过了4个字节。我们发现,他们跳过的字节数刚好和指针对应的类型大小相同!这里我们就可以得出结论:
指针类型决定了指针向前或向后走一步的步长(距离)
2)指针的解引用
我们已经知道了指针如何在内存中工作的,那么我们该如何将指针给用起来呢?其实啊,我们有了变量的地址,保存在指针变量里,接下来就是放行李的过程,也就是对内存空间进行访问。
[事件2] 你打开了302房间的房门,刚走进去,不禁皱起了眉头,里面的杯子还是乱的,垃圾桶还没清理,甚至地下还有垃圾,你直接去找了酒店前台,前台十分抱歉,于是叫来了保洁阿姨,很快的,你的房间就焕然一新了,上个房客剩下的东西统统清理干净,随后将你的东西放进角落…[结束]
指针的意义就是为了来管理我们的内存,在C语言中用指针来访问内存有一个专门的运算符:*(解引用运算符) ,这里的解引用,就可以对指针指向的内存空间随意访问啦。
用法为:
int a = 0; int *p = &a;//正常取变量地址 *a = 1;//这就是对指针所指向的内存空间进行访问 //也就是说,指针可以通过解引用来更改变量a的内容
这里将原来的0通过指针解引用改变为了1,酒店里你发现订的房间居然很乱?现在是你要住进来,你可不管之前住的是谁。这就是通过对指针解引用,来访问内存,可以对于原来的值进行修改。
注意:初学者总是会搞错指针类型的大小与指针所指向变量的大小关系,指针的 大小永远为4/8个字节。我们来看下面例子:
#include<stdio.h> int main() { printf("%d\n",sizeof(int *));//sizeof对不同的指针类型求大小 printf("%d\n",sizeof(char *)); printf("%d\n",sizeof(short *)); printf("%d\n",sizeof(long *)); printf("%d\n",sizeof(float *)); printf("%d",sizeof(double *)); //... return 0; }
得出的结果为:
其实说白了指针就是地址,指针可不管你是int、还是double还是什么类型,到我这里都是地址,指针的类型大小是跟指针所指向的类型无关,我的机器为64位机器,所以我的指针大小就一定是8个字节。
我相信你还有一些疑问:“还是那个问题,既然指针只需要一个字节的地址,那为什么还要分什么类型,我全都是char *不就完了吗?”。
其实上面我已经解释了为什么指针需要类型,这里在从解引用的角度来分析一下,我们来看下面的例子:
#include<stdio.h> int main() { int a = 0x44332211;//这里不是地址,而是16进制的数字进行赋值 int *p = &a;//整形指针取地址 printf("%x\n",*p);//以十六进制形式打印 char *pa = (char *)&a;//字符指针取地址 printf("%x\n", *pa);//十六进制打印 printf("%x\n", *(pa+1));//指针向下一个位置访问 return 0; }
由打印结果我们可以看到:不同类型的指针使用解引用而访问到不同的字节数,这里char*指针只访问了变量a的一个字节,而int*指针访问了变量a的4个字节。
总结:
指针的类型 决定了指针解引用时候有多大权限(访问几个字节数)
野指针
1)为什么会有野指针
哎呀,指针真好用!我有了谁的地址,我就可以随便来玩了,有一天,你写了这样一段代码:
#include<stdio.h> int main() { int *p; *p = 20; printf("%d\n", *p); return 0; }
这个时候在打印:
哎呀,这里程序怎么挂了?其实,这里没有对指针p进行初始化,他没有保存任何变量的地址。这个指针也是一个局部变量,当局部变量不初始化的时候,内容是随机值。
既然是随机值,也就是说这里的指针是随机的地址,你说万一这地址里面存的是什么重要数据,你在这里把他改了?是不是就太危险了?!
这种有越界访问的指针我们统称:野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
除此之外,还有其他导致野指针的原因,我们来看下面代码:
#include <stdio.h> int main() { int arr[10] = {0}; int *p = arr;//相当于int *p = &a[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i;//先解引用赋值,再后置++使指针指向下一个位置 } return 0; }
在C语言中,数组名表示数组首元素地址,我们将数组的首元素地址给了指针p,我们通过for循环用指针对数组进行访问,这里我们只有10个元素,我们却要访问12次,那么就会发生越界访问问题。
这里也就会造成指针越界访问的问题,同样,当出了数组之后,指针也会变成随机值,造成越界访问。
[事件3] 这几天在西藏你玩的很爽,玩够了,也该回家族打理产业了,然后你就想在过了今天晚上,明天就回去,可是当你走到302的房门前,发现你的行李整齐地摆放在地下,这个时候你才想到,原来房间今天早上就到期了…身上的钱也只够回家了,看来今天只能露宿街头了…[结束]
除了上面的情况之外,我们还有一种常见的导致野指针的问题:
#include<stdio.h> int *Test() { int a = 1; int *p = &a; return p; } int main() { int *ret = Test(); *ret = 2; printf("%d\n", *ret); return 0; }
我们来仔细分析一下:从main函数开始,第一个语句直接进入到Test函数里,那么Test函数会在函数栈帧上开辟一块空间,变量a也开了一块空间,指针变变量p也开辟一块空间用来记录a的地址。
在函数调用结束的时候,会创建一个临时变量记录返回值,函数栈帧销毁,变量a和指针变量p都销毁了,临时变量被返回值传到main函数的ret。
那么ret就记录下了这个地址,我们对ret解引用赋值,但是这个时候Test函数已经销毁了,里面的变量的值已经回收了,这个时候再去访问这个已经回收的地址,那么肯定会发生越界访问的。也就是说,你的房间已经被退房了,这个时候你还想去302,就是非法的了。
总结:
1、野指针会造成越界访问的问题,因此对于指针控制范围非常重要。
2、已经回收资源的地址,再次访问这个地址就是非法访问。
2)如何避免野指针
由上面的学习我们知道指针玩不好代价是很大的,那么有没有什么办法防止指针越界等问题呢?要想玩好指针,你必须要记住这五个点:
1、指针一定要初始化
2、小心指针越界
3、指针指向的空间释放,及时将这个指针置为NULL
4、避免返回局部变量的地址
5、指针使用之前要检查有效性
指针在使用之前一定要初始化,如果没有需要引用的对象,就将指针置为NULL,如下:
#include<stdio.h> int main() { int a = 10; int *p = &a; int *ptr = NULL; return 0; }
这里的NULL我们可以转到定义来看一下:
我们可以看到,在c++中的NULL就是0,在C语言中NULL的类型就是(void *)空指针类型,严格来说C语言的NULL是更加正确的。
牢记这5点,妈妈就再也不用担心我的指针老是出错了。
【C语言期末不挂科——指针初阶篇】(下)https://developer.aliyun.com/article/1386231