一. 概述
内存泄漏一直是软件开发人员最头大的问题之一,尤其像C/C++这样自由度非常大的编程语言,几乎是每一个用其开发出来的软件都会出现内存泄漏的情况。
如果没有内存泄漏,世界或许会变的美好。然而,完全美好的世界是不存在的,我们能做的就是尽量让它变的更美好。
二. 工具介绍
纵观目前市面上有不少内存泄漏工具,在这里就不多做介绍。笔者最常用的是IBM公司出产的IBM Rational Purify,说起Purify这东西,稳定性虽然不怎么样,但功能很强大。它是IBM Rational PurifyPlus产品组中三剑客的一员。
Purify是面向C/C++, VB, Java语言,测试内存错误,Windows API访问错误,COM方法调用情况等。
图1. Purify主界面
Purify对于C/C++的分析支持源代码级,或通过其专利技术:“目标代码插入(Object Code Insertion)”工作在无源代码级。Purify的功能其实非常之强大,此次我们只讨论怎么通过Purify进行C/C++下的内存泄漏发现。在介绍它的工作流程前,我们来认识一下几个缩写术语的意思:
l ABR: Array Bounds Read,数组越界读
l ABW:Array Bounds Write,数组越界写
l FMR:Free Memory Read,空间内存读取
l IPR: Invalid Pointer Read,无效指针读取
l IPW:Invalid Pointer Write,无效指针写入
l NPR:Null Pointer Read,空指针读取
l NPW:Null Pointer Write,空指针写入
l UMR:Uninitialized Memory Read,未初始化内存阅读
l MLK:Memory leaks,内存泄漏
l MIU:Memory in use, 内存使用中
l MPK:Potential memory leaks,潜在的内存泄漏
当我们完成了目标代码,生成可执行程序文件后,在Purify中加载它,便可自动完成可执行程序解析,运行,分析的一应工作。
图2. Rational Purify的工作流程
三. 泄漏场景及检测
现在,我们就来做几个案例,看看效果如何。开始之前,先介绍一下笔者的工作及测试环境:
l Microsfot Windows XP Tablet PC Edition 2005 SP3
l 3G内存(再多的内存也怕泄漏J)
l Microsfot Visual Studio 2005
l IBM Rational Purify 7.0
在这里想额外的说明一下,笔者曾经测试对比过VC2005和VC6生成程序,发现VC2005生成汇编代码质量比VC6的好很多,几乎接近人工编写。所以这就是为什么笔者不用VC6的原因。
好的,现在来编写一个超简单的内存泄漏例子,打开VS2005,新建一个标准的VC++控制台项目,输入如下例子,编译。
int _tmain(int argc, _TCHAR* argv[]) {
int* i = new int(); // 内存在此申请后无释放,发生泄漏
*i = 10;
printf("%d", *i);
return 0;
}
上面这段程序非常简单,在new完一个int实例后,却没有delete它。产生了一个4个字的内存泄漏。虽然简单,但还是来看看Rational Purify是怎么运作和发现它的吧:
首先,打开Purify,点击主菜单“File”,然后点击“Run…”,在弹出窗口(Run Program)中选中要测试的程序,最后点击按钮“Run”。至此,已完成程序加载步骤,Purify自动开始解析并运行程序。由于这是一个控制台应用程序,所以程序在输出一个10后便已完成,按任意键结束程序后,Purify便开始了自动分析并运行,一切都那么自然,那么简单。 J
图3. 加载程序并运行
图4. 例一分析报告窗口
果然,Purify没有让人失望的发现了程序的问题。现在,让我们再来试试复杂一点的,请看如下代码:
int _tmain(int argc, _TCHAR* argv[]) {
// 制造5个4个字节的内存泄漏
for (int i = 0; i < 5; ++i) {
// 此处申请了1个4个字节的内存,但没有释放它
int* j = new int;
*j = i;
printf("%d", *j);
}
return 0;
}
我们人为的制造了20个字节的内存泄漏,通过如上所述之步骤加载到Purify,运行结束如下图:
图5. 例二分析报告窗口
通过例二的分析,我们发现Purify也能正确的分析出内存泄漏的位置。最后,让我们来写一个类,在这个类里我们故意在构造函数里造成1个4个字节的内存泄漏,然后在main函数里new一个此类,不释放它,看看是什么效果,代码如下:
class LeaksTest
{
public:
LeaksTest() {
// 此处申请了个个字节的内存,但没有释放
i = new int;
*i = 1;
}
int geti() {
return *i;
}
private:
int* i;
};
int _tmain(int argc, _TCHAR* argv[]) {
// 此处new了一个LeaksTest对像,没有释放
LeaksTest* lt = new LeaksTest();
printf("%d", lt->geti());
return 0;
}
还是老样子,编译完成后通过Purify来加载它,如图:
图6. 例三的分析报告窗口
四. 避免
在C/C++语言中,指针带给了人们非常大的灵活度,但它却是一把双刃剑,掌握不好却极易造成软件的内存问题,轻则出错,重则崩溃。
怎么有效的避免此类问题,目前比较有效的方法就是使用智能指针,所谓智能指针说白了就是用一个类把指针再封装起来以保证在这个封装类被释放时及时释放指针。但是要想非常完美的实现智能指针却是一件难度非常大的活。C++ STL就提供了一个智能指针,但功能却一般。正因为如此,BOOST库中也提供了智能指针类,为了应付不同情况下不同的需求,BOOST库提供了多个智能指针类,最大化了满足了这些要求。至于怎么使用这智能指针却不是这里要讲的内容了,读者如有兴趣可以自行去研究学习。