一、引子
我们都知道对指针( Pointer)的操作,实际上是对计算机内存地址的操作,通过访问内存地址实现间接访问该地址中保存的数据。其实就是CPU的寻址方式中的间接寻址。简单概括正常使用指针时的3个步骤为:
- 定义指针变量
- 绑定指针即给指针变量赋值
-
解引用即间接访问目标变量
通过一个简单的例子来看这3个步骤的实现:
1 int a = 5;
2 //定义指针变量p
3 int *p;
4 //绑定指针,就是给指针变量赋值,指向另一个变量a(指针的用途就是指向别的变量)
5 p = &a;
6 //将6放入p所指向的那个变量的空间中,这里就是a的空间
7 *p = 6;
可以看出,在定义指针变量p时,未初始化p,这个时候的p为随机值,此时解引用p是没有意义的,内存随机值的空间是否有效我们也不得而知。
绑定指针就是将变量a的地址赋值给指针变量p,此时p就有了意义,明确了内存中访问的具体空间位置,p是指向变量a的空间的,变量a是有具体内容的,因此指针必须给它赋值才能解引用它。
给指针变量p赋值实际上是在变量a前加一个“&”符号,这个符号是取地址符,&a就是指变量a的地址,编译器在给每个变量分配出内存空间,并将a与这块的内存空间地址绑定。这个地址只有编译器知道,而程序员并不知道编译器随机给这段空间分配什么随机地址值。程序员要获取或操作这个地址时,就需要使用取地址符。
由上述分析看来,给p赋予了变量a地址的值是一个合法的,在内存中明确的地址值,这个值是受控的,同时通过访问指针间接访问该地址中保存的数据也是受控的,p就是一个正常的指针。
野指针(Wild Pointer)。
二、需要明确的一点
野指针不同于空指针,所谓空指针,是给指针变量赋NULL值,即:
1 int *p = NULL;
所谓NULL值在C/C++中定义为:
1 #ifdef __cplusplus // 定义这个符号表示当前是C++环境中 2 #define NULL 0 // 在C++中NULL为0 3 #else 4 #define NULL (void *) 0 // 在C中的NULL是强制类型转换为void *的0 5 #endif
可以看出,给p赋值NULL值也就是让p指向空地址。在不同的系统中,NULL并不意味等于0,也有系统会使用地址0,而将NULL定义为其他值,所以不要把NULL和0等同起来。你可以将NULL通俗理解为是空值,也就是指向一个不被使用的地址,在大多数系统中,都将0作为不被使用的地址,因此就有了这样的定义,C或者C++编译器保证这个空值不会是任何对象的地址。
void *表示的是“无类型指针”,可以指向任何数据类型,在这里void指针与空指针NULL区别:NULL说明指针不指向任何数据,是“空的”;而void指针实实在在地指向一块内存,只是不知道这块内存中是什么类型的数据。
我们需要在让空指针真正指向了一块有意义的内存后,我们才能对它取内容。即:
1 int a = 5;
2 int *p = NULL;
3 p = &a;
NULL指针并没有危害,可以使用if语句来判断是否为NULL。
三、一些典型的error
我们要知道单纯的从语言层面无法判断一个指针所保存的地址是否是合法的,等到程序运行起来,配合硬件的内存实际地址,才能发现指针指向的地址是否是你想要让它指向的合理空间地址。在日常编码过程中有一些导致野指针或者内存溢出的错误编码方式:
1、指针变量未初始化
任何指针在被创建的时候,不会自动变成NULL指针,因此指针的值是一个随机值。这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的。
1 void main()
2 {
3 char* p;
4 *p = 6; //错误
5 }
2、使用了悬垂指针
在C或者C++中使用malloc或者new申请内存使用后,指针已经free或者delete了,没有置为NULL,此时的指针是一个悬垂指针。
free和delete只是把指针所指的内存给释放掉,并不会改变相关的指针的值。这个指针实际仍然指向内存中相同位置即其地址仍然不变,甚至该位置仍然可以被读写,只不过这时候该内存区域完全不可控即该地址对应的内存是垃圾,悬垂指针会让人误以为是个合法的指针。
1 void main()
2 {
3 char* p = (char *) malloc(10);
4 strcpy(p, “abc”);
5 free(p); //p所指的内存被释放,但是p所指的地址仍然不变
6 strcpy(p, “def”); // 错误
7 }
3、返回栈内存指针或引用
在函数内部定义的局部指针变量或者局部引用变量不能作为函数的返回值,因为该局部变量的作用域范围在函数内部,该函数在被调用时,由于局部指针变量或者引用已经被销毁,因此调用时该内存区域的内容已经发生了变化,再操作该内存区域就没有具体的意义。
1 char* fun1()
2 {
3 char* p = "hello";
4 return p;
5 }
6
7 char* fun2()
8 {
9 char a = 6;
10 return &a;
11 }
12
13 void main()
14 {
15 char* p1 = fun1(); //错误
16 char* p2 = fun2(); //错误
17 }
4、指针重复释放
1 void fun(char* p, char len)
2 {
3 for(char i = 0; i < len; i++)
4 {
5 p[i] = i;
6 }
7 free(p);
8 }
9
10 void main()
11 {
12 char * p1 = (char *)malloc(6 * sizeof(char));
13 fun(p1, 6);
14 free(p1); //重复释放指针导致错误
15 }
5、数组越界
使用的数组长度超过了定义的数组长度。
1 void main()
2 {
3 int a[6];
4 for(int i = 0; i<=6; i++) //错误
5 }
6、内存分配后未初始化
1 void main()
2 {
3 char* p = (char*)malloc(6);
4 printf(p); //p未初始化
5 free(p);
6 }
7、使用的内存大小超过了分配的内存大小
1 void main()
2 {
3 char* p = (char*)malloc(6);
4 for(int i = 0; i <= 6; i++) //错误
5 {
6 p[i] = i;
7 }
8 free(p);
9 }
四、避免错误的注意点
1、在定义指针变量时,要将其值置为NULL,即 char *p = NULL。
2、在指针使用之前,需要给指针赋具体值,就是将其绑定一个可用地址空间让其有意义,即p = &a。
3、在使用指针前,需要判断指针为非NULL,只有非NULL的指针才有意义。即判断if(p != NULL)。
4、free或者delete指针后,需要将指针值置为NULL。
5、malloc和free,new和delete注意配对使用,当 malloc或new次数大于 free或delete 时,会产生内存泄漏;需要防止多次重复free或者delete,当malloc或new 次数小于free或delete时,程序有可能会崩溃。
6、使用malloc或new分配内存后,需要初始化,同时在使用时注意不要超过分配的内存大小空间。
7、在哪个函数里面进行的 malloc或new ,就在哪个函数里面 free或delete,不要跨函数去释放动态的内存空间。
8、不要将局部指针变量,局部引用变量或局部数组作为函数的返回值。
9、使用数组时一定要注意定义的数组大小,防止数组越界;或者在定义数组时可以不定义数组长度,即int a[]。
10、在定义有指针操作相关的函数时必须指定长度信息,即void fun(char* p, char len)。
BTW:
最后根据以上的讨论,再结合以下网友的总结,我们可以更好的理解下野指针在实际程序中的危害:
a、指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了。
b、指向一个可用的、而且没什么特别意义的空间(譬如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的。
c、指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。
更多技术内容和书籍资料获取敬请关注微信公众号“明解嵌入式”