野指针,又名迷途指针。即指向“垃圾”内存的指针。此时指针所指向的内存已被操作系统回收。可执行程序已经无法在访问了。野指针一般不是NULL指针,因为用if语句很容易判断。而是那些看上去指向合法内存,而实际上此指向的内存已不合法的指针。“野指针”是很危险的,if无法判断一个指针是正常指针还是“野指针”。使用这种指针,轻则产生未知现象,重者程序指针挂死。
注意事项
- 野指针不是NULL指针,而是指向“垃圾”内存的指针。
- 在某些环境下使用野指针,程序可以正常运行。但是并不说明程序是正确的。
野指针产生的原因是多种多样的,但从其产生过程的不同可以分为三类。下面我们详细讨论一下他们。
(1)指针变量未初始化。这种情况下,指针将成为一个野指针。
指针变量(除全局指针变量、静态指针变量)创建时如果不进行初始化,它的缺省值是随机的。所以指针变量在创建时应该同时给以初始化,否则指针将成为野指针。一般情况下,如果你创建的指针没有固定地址初始化,你可以给指针变量赋值NULL。
(2)指针所指向的对象已经不存在了。编程员误以为指针还是一个合法的指针。
指针p被free或delete后,没有设置NULL。指针所指的内存被释放掉了,但并没有对指针本身进行处理。这种情况,通常使用语句if(p != NULL)进行防错处理。但遗憾的是if语句无法起到防错的作用,因为p不是NULL指针,却指向一块不合法的内存。看下面这个例子。
01 #include <stdio.h>
02 #include <string.h>
03 #include <stdlib.h>
04 int main()
05 {
06 char *p = new char[10];
07 memset(p, 0, 10);
08 delete [] p;
09
10 if (p != NULL)
11 {
12 strcpy(p, "hello!");
13 cout << p;
14 }
15 }
将上述代码在VS2010编译器中运行运行结果是输出:hello!字符串。但你不要高兴的太早了,这只是一种巧合,VS2010执行delete命令时,对内存不进行任何操作。将代码在GCC编译器下运行,也许就没这么幸运了。因为代码从第10行开始就引用了悬空指针(即野指针)。程序在第8行执行了delete命令,但由于delete后,未将p赋值为NULL。导致第10行指针判断出错。
除了这种引用已释放指针外,C/C++中还有一种引用已释放内存情况,这就是函数返回局部变量的地址(或引用)。这种情况在代码监视时更难发现,因为这种错误是由C++的编译机制造成的。
C++函数的局部变量分配到栈内存上 ,函数栈在函数运行时生成(存在),函数运行结束退出栈内存释放。因此如果函数返回局部变量的地址,当函数退出时局部变量栈内存已释放。此时通过指针对变量的操作结果是未定义的。看这个函数:
char *GetStr()
{
char szStr[] = “hello world!”;
return szStr;
}
编译器是检查不出代码问题的。编译实现好的编译器会提“warning C4172: 返回局部变量或临时变量的地址”警告。
最佳实践:指针变量执行内存释放操作后,务必将其赋值为NULL。通过这种方式可在一定程度上避免野指针;禁止返回局部变量的地址或引用,因为这样会产生悬空指针。
(3)指针操作超越变量作用范围。这种情况让人防不胜防。也是代码出现概率最高的一种错误。
这种错误经常表现为:1、引用数组操作数组长度,2、变量强制转换类型转化错误。3、对指针变量赋值一个非法地址。下面这段代码:
char szName[] = “C++”;
for (int i = 0; i < 5; i++)
{
if (szName[i] == ‘C’)
{
return true;
}
}
return false;
这段代码的明显错误就是数组操作越界,但是编译器不会报出任何错误或提示。
野指针确实让人头疼,但是对付它并非无任何策略。我这儿给大家介绍几种预防野指针的方法。
(1)声明指针时记得初始化,如:
char *p = NULL;
(2)当指针没有使用价值时记得释放,释放成功后记得为此指针赋值NULL。例如:
if (NULL != p)
{
delete p;
p = NULL;
}
(3)如果指针作为函数的输入参数时,在引用参数前首先对指针进行参数检查。
在函数的入口处使用assert(NULL != p)对参数进行校验。或者用if(NULL != p)来校验。它会提醒有没有初始化指针,起到定位错误的功能。assert是个宏,它后面括号里的条件若不满足,则程序终止运行并提示出错。使用完指针后务必记得释放指针所指向的内存,否则不知道什么时候又改变了指针的值,使其变成了野指针。
(4)尽量使用引用替代指针。
引用具有指针的功能,同时它还具备普通变量的属性。引用对应的变量必须真实存在,这可有效的防止悬空指针的存在,而且引用作为函数的输入参数具有比指针更直接的视觉效果。我们看swap函数的指针实现和引用实现。
void swap(int *piVa, int *piVb) // 实现两个int变量数值互换
{
int iVtemp = *piVa;
*piVa = *piVb;
*piVb = iVtemp;
}
void swap(int &iVa, int &iVb) // 实现两个int变量数值互换
{
int iVtemp = iVa;
iVa = iVb;
iVb = iVtemp;
}
对比上面两种swap函数实现方式,我们可以看出:通过引用实现数值互换时,函数调用时只需要将两整数传递给swap函数即可实现数值的互换。而第一种实现方式函数调用时必须将两整数的地址传递给函数才可以实现两个整数值的互换。还有如果传递的指针为野指针其运行结果不可知。
(5)使用智能指针避免野指针。
如果不同对象都需要访问堆上同一份指针,智能指针能有效避免野指针:用智能指针(推荐shared_ptr)进行包装,不同对象可拥有智能指针包装后的指针,每次存取之前,使用智能指针的方法_Expired()进行指针的有效性检查,如果失效,则表明对象已经被释放。
请谨记
- 野指针指所指向的内存已被操作系统回收的指针。由于指针引起的编程错误大部分是野指针造成的。
- 指针所指向的内存已经释放了。但是指针不不知晓此段内存已释放。这是产生野指针的主要原因。
- 合理的使用引用和智能指针加上优秀的编程规范和在一定程度上降低野指针