如果内存已经释放,而指针还在引用原始内存,这样的指针就称为迷途指针。迷途指针没有指向有效对象,有时候也称为过早释放。
使用迷途指针会造成一系列问题,包括:
- 如果访问内存,则行为不可预期;
- 如果内存不可访问,则是段错误;
- 潜在的安全隐患。
导致这几类问题的情况可能如下:
- 访问已释放的内存;
- 返回的指针指向的是上次函数调用中的自动变量(在3.2.5节中会讨论)。
迷途指针示例
在下面这个简单的例子中我们用malloc
函数为一个整数分配内存,接下来,用free
函数释放内存:
int *pi = (int*) malloc(sizeof(int));
*pi = 5;
printf("*pi: %d\n", *pi);
free(pi);
pi
变量持有整数的地址,但堆管理器可以重复利用这块内存,且其中存放的可能是非整数数据。图2-11说明了free
函数执行前后的程序状态。假设pi
变量属于main
函数,位于地址100,malloc
函数分配的内存位于地址500。
图2-11:迷途指针
执行free
函数将释放地址500处的内存,此后就不应该再使用这块内存了。但大部分运行时系统不会阻止后续的访问或修改。我们还是可以向这个位置写入数据,如下所示。这么做的结果是不可预期的。
free(pi);
*pi = 10;
还有一种迷途指针的情况更难觉察:一个以上的指针引用同一内存区域而其中一个指针被释放。如下所示,p1
和p2
都引用同一块内存区域(称为指针别名),不过p1
被释放了:
int *p1 = (int*) malloc(sizeof(int));
*p1 = 5;
...
int *p2;
p2 = p1;
...
free(p1);
...
*p2 = 10; // 迷途指针
图2-12说明了内存分配情况,虚线框表示释放的内存。
图2-12:迷途指针和指针别名
使用块语句时也可能出现一些小问题,如下所示。这里pi
被赋值为tmp
的地址,变量pi
可能是全局变量,也可能是局部变量。不过当包含tmp
的块出栈之后,地址就不再有效:
int *pi;
...
{
int tmp = 5;
pi = &tmp;
}
// 这里pi变成了迷途指针
foo();
大部分编译器都把块语句当做一个栈帧。tmp
变量分配在栈帧上,之后在块语句退出时会出栈。pi
指针现在指向一块最终可能被其他活跃记录(比如foo
函数)覆盖的内存区域。图2-13说明的就是这种情形。
图2-13:块语句的问题
处理迷途指针
有时候调试指针诱发的问题会很难解决,以下方法可用来对付迷途指针。
- 释放指针后置为
NULL
,后续使用这个指针会终止应用程序。不过,如果存在多个指针的话还是会有问题。因为赋值只会影响一个指针,free函数中有相关说明。 - 写一个特殊的函数代替
free
函数(参见传递指针的指针)。 - 有些系统(运行时或调试系统)会在释放后覆写数据(比如0xDEADBEEF,取决于被释放的对象,Visual Studio会用0xCC、0xCD或者0xDD)。在不抛出异常的情况下,如果程序员在预期之外的地方看到这些值,可以认为程序可能在访问已释放的内存。
- 用第三方工具检测迷途指针和其他问题。
在调试迷途指针时打印指针的值可能会有所帮助,但需要注意打印的方式。打印指针的值已经讨论过如何打印指针的值。确保用一致的方式打印,从而避免比较指针的值时产生歧义。assert
宏也可能有用,指针声明和初始化问题中会讲到。
调试器对检测内存泄漏的支持
微软提供了解决动态分配内存的覆写和内存泄漏的技术。这种方法在调试版程序里用了特殊的内存管理技术:
- 检查堆的完整性;
- 检查内存泄漏;
- 模拟堆内存不够的情况。
微软是通过使用一种特殊的数据结构管理内存分配来做到这一点的。这种结构维护调试信息,比如malloc
调用点的文件名和行号,还会在实际的内存分配之前和之后分配缓冲区来检测对实际内存的覆写。关于这种技术的更多信息可以参考Microsoft Developer Network(http://msdn.microsoft.com/en-us/library/x98tx3cf.aspx)。
Mudflap库(http://gcc.fyxm.net/summit/2003/mudflap.pdf)为GCC编译器提供了类似的功能,它的运行时库支持对内存泄漏的检测和其他功能,这种检测是通过监控指针解引操作来实现的。
评论前必须登录!
注册