一、引子
咱们都晓得对指针(Pointer)的操作,实际上是对计算机内存地址的操作,通过拜访内存地址实现间接拜访该地址中保留的数据。其实就是 CPU 的寻址形式中的间接寻址。简略概括失常应用指针时的 3 个步骤为:
- 定义指针变量
- 绑定指针即给指针变量赋值
-
解援用即间接拜访指标变量
通过一个简略的例子来看这 3 个步骤的实现:
int a = 5;
// 定义指针变量 p
int *p;
// 绑定指针,就是给指针变量赋值,指向另一个变量 a(指针的用处就是指向别的变量)p = &a;
// 将 6 放入 p 所指向的那个变量的空间中,这里就是 a 的空间
*p = 6;
能够看出,在定义指针变量 p 时,未初始化 p,这个时候的 p 为随机值,此时解援用 p 是没有意义的,内存随机值的空间是否无效咱们也不得而知。
绑定指针就是将变量 a 的地址赋值给指针变量 p,此时 p 就有了意义,明确了内存中拜访的具体空间地位,p 是指向变量 a 的空间的,变量 a 是有具体内容的,因而指针必须给它赋值能力解援用它。
给指针变量 p 赋值实际上是在变量 a 前加一个“&”符号,这个符号是取地址符,&a 就是指变量 a 的地址,编译器在给每个变量调配出内存空间,并将 a 与这块的内存空间地址绑定。这个地址只有编译器晓得,而程序员并不知道编译器随机给这段空间调配什么随机地址值。程序员要获取或操作这个地址时,就须要应用取地址符。
由上述剖析看来,给 p 赋予了变量 a 地址的值是一个非法的,在内存中明确的地址值,这个值是受控的,同时通过拜访指针间接拜访该地址中保留的数据也是受控的,p 就是一个失常的指针。
相同,如果指针指向了内存中不可用的区域,或者是指针的值是非法的随机值也就是非正常内存地址,那么这个指针就是不受控的,同时通过拜访指针间接拜访该地址中保留的数据也是不受控的,同时是不可知的,此时这个指针就是野指针(Wild Pointer)。
二、须要明确的一点
野指针不同于空指针,所谓空指针,是给指针变量赋 NULL 值,即:
int *p = NULL;
所谓 NULL 值在 C /C++ 中定义为:
#ifdef __cplusplus // 定义这个符号示意以后是 C ++ 环境中
#define NULL 0 // 在 C ++ 中 NULL 为 0
#else
#define NULL (void *) 0 // 在 C 中的 NULL 是强制类型转换为 void * 的 0
#endif
能够看出,给 p 赋值 NULL 值也就是让 p 指向空地址。在不同的零碎中,NULL 并不象征等于 0,也有零碎会应用地址 0,而将 NULL 定义为其余值,所以不要把 NULL 和 0 等同起来。你能够将 NULL 艰深了解为是空值,也就是指向一个不被应用的地址,在大多数零碎中,都将 0 作为不被应用的地址,因而就有了这样的定义,C 或者 C ++ 编译器保障这个空值不会是任何对象的地址。
void * 示意的是“无类型指针”,能够指向任何数据类型,在这里 void 指针与空指针 NULL 区别:NULL 阐明指针不指向任何数据,是“空的”;而 void 指针实实在在地指向一块内存,只是不晓得这块内存中是什么类型的数据。
空指针的值是受控的,但并不是有意义的,咱们是将指针指向了 0 地址,这个 0 地址就是作为内存中的一个非凡地址,因而空指针是一个对任何指针类型都非法的指针,但并不是正当的指针,指针变量具备空指针值,示意它处于闲置状态,没有指向任何有意义的内容。咱们须要在让空指针真正指向了一块有意义的内存后, 咱们能力对它取内容。即:
int a = 5;
int *p = NULL;
p = &a;
NULL 指针并没有危害,能够应用 if 语句来判断是否为 NULL。
三、一些典型的 error
咱们要晓得单纯的从语言层面无奈判断一个指针所保留的地址是否是非法的,等到程序运行起来,配合硬件的内存理论地址,能力发现指针指向的地址是否是你想要让它指向的正当空间地址。在日常编码过程中有一些导致野指针或者内存溢出的谬误编码方式:
1、指针变量未初始化
任何指针在被创立的时候,不会主动变成 NULL 指针,因而指针的值是一个随机值。这时候去解援用就是去拜访这个地址不确定的变量,所以后果是不可知的。
void main()
{
char* p;
*p = 6; // 谬误
}
2、应用了悬垂指针
在 C 或者 C ++ 中应用 malloc 或者 new 申请内存应用后,指针曾经 free 或者 delete 了,没有置为 NULL,此时的指针是一个悬垂指针。
free 和 delete 只是把指针所指的内存给开释掉,并不会扭转相干的指针的值。这个指针理论依然指向内存中雷同地位即其地址依然不变,甚至该地位依然能够被读写,只不过这时候该内存区域齐全不可控即该地址对应的内存是垃圾,悬垂指针会让人误以为是个非法的指针。
void main()
{char* p = (char *) malloc(10);
strcpy(p,“abc”);
free(p); // p 所指的内存被开释,然而 p 所指的地址依然不变
strcpy(p,“def”); // 谬误
}
3、返回栈内存指针或援用
在函数外部定义的部分指针变量或者部分援用变量不能作为函数的返回值,因为该局部变量的作用域范畴在函数外部,该函数在被调用时,因为部分指针变量或者援用曾经被销毁,因而调用时该内存区域的内容曾经产生了变动,再操作该内存区域就没有具体的意义。
char* fun1()
{
char* p = "hello";
return p;
}
char* fun2()
{
char a = 6;
return &a;
}
void main()
{char* p1 = fun1(); // 谬误
char* p2 = fun2(); // 谬误}
4、指针反复开释
void fun(char* p, char len)
{for(char i = 0; i < len; i++)
{p[i] = i;
}
free(p);
}
void main()
{char * p1 = (char *)malloc(6 * sizeof(char));
fun(p1, 6);
free(p1); // 反复开释指针导致谬误
}
5、数组越界
应用的数组长度超过了定义的数组长度。
void main()
{int a[6];
for(int i = 0; i<=6; i++) // 谬误
a[i] = i;
}
6、内存调配后未初始化
void main()
{char* p = (char*)malloc(6);
printf(p); // p 未初始化
free(p);
}
7、应用的内存大小超过了调配的内存大小
void main()
{char* p = (char*)malloc(6);
for(int i = 0; i <= 6; i++) // 谬误
{p[i] = i;
}
free(p);
}
四、防止谬误的留神点
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 的值,导致这个变量莫名其妙的被扭转,程序呈现离奇的谬误。个别最终都会导致程序解体,或者数据被侵害。这种危害是最大的。
更多技术内容和书籍材料获取敬请关注微信公众号“明解嵌入式”