改善C++ 程序的150个建议学习之建议32:借助工具监测内存泄漏问题

来源:互联网 发布:美工设计助理骗局 编辑:程序博客网 时间:2024/05/16 19:52
建议32:借助工具监测内存泄漏问题
内存管理确实是一个令众多C/C++程序员感到费神又费力的问题,内存错误通常都具有隐蔽性,难以再现,而且其症状一般不能在相应的源代码中找到。C/C++应用程序的大部分缺陷和错误都和内存相关,预防、发现、消除代码中和内存相关的缺陷,成为C/C++程序员编写、调试、维护代码时的重要任务。然而任何人都无法时刻高度谨慎,百密中难免会有一疏,一不小心就会发生内存问题。如果泄漏内存,则运行速度会逐渐变慢,并最终会停止运行;如果覆盖内存,则程序会变得非常脆弱,很容易受到恶意用户的攻击。因此,需要特别关注C/C++编程的内存问题,特别是内存泄漏。幸运的是,现在有许多的技术和工具能够帮助我们验证内存泄漏是否存在,寻找到发生问题的位置。内存泄漏一般指的是堆内存的泄漏。如果我们使用malloc函数或new操作符从堆中分配到一块内存,在使用完后,程序员必须负责调用相应的free或delete显式地释放该内存块,否则,这块内存就不能被再次使用,此时就出现了传说中的“内存泄漏”问题。如下面的代码片段所示:
void Function(size_t nSize)
{
char* pChar= new char[nSize];
if( !SetContent(pChar, nSize ) )
{
cout<<"Error: Fail To Set Content"<<endl;
return;
}
...//using pChar
delete pChar;
}
程序在入口处分配内存,在出口处释放内存,但是这里忽视了代码片段中的return;,如果函数SetContent()失败,指针pChar指向的内存就不会被释放,会发生内存泄漏。这是一种常见的内存泄漏情形。检测内存泄漏的关键是要能截获对分配内存和释放内存的函数的调用。通过截获的这两个函数,我们就能跟踪每一块内存的生命周期。每当成功分配一块内存时,就把它的指针加入一个全局的内存链中;每当释放一块内存时,再把它的指针从内存链中删除。这样,当程序运行结束的时候,内存链中剩余的指针就会指向那些没有被释放的内存。这就是检测内存泄漏的基本原理。检测内存泄漏的常用方法有如下几种:MS C-Runtime Library内建的检测功能使用MFC开发的应用程序时,会在Debug模式下编译执行,程序运行结束后,VisualC++会输出内存的使用情况,如果发生了内存泄漏,在Debug窗口中会输出所有发生泄漏的内存块的信息,如下所示:
Detected memory leaks!
Dumping objects ->
mainFrm.cpp(45) : {352} normal block at 0x0058A4B8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

这是因为在编译过程中,IDE自动加入了内存泄漏的检测代码。MFC在程序执行过程中维护了一个内存链,以便跟踪每一块内存的生命周期。在程序退出的时候,dbgheap.c文件中的extern "C" _CRTIMP int __cdecl _CrtDumpMemoryLeaks(void)函数被调用,遍历当前的内存链,如果发现存在没有被释放的内存,则打印出内存泄露的信息。一般,大家都误以为这些内存泄漏的检测功能是由MFC提供的,其实不然。这是VC++的C运行库(CRT)提供的功能,MFC只是封装和利用了MS C-Runtime Library的Debug Function而已。所以,在编写非MFC程序时我们也可以利用MS C-Runtime Library的Debug Function加入内存泄漏的检测功能。要在非MFC程序中打开内存泄漏的检测功能非常容易,只须在程序的入口处添加以下代码:

_CrtSetDbgFlag( _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)
| _CRTDBG_LEAK_CHECK_DF );
这样,在程序运行结束时,如果还有内存块没有释放,它们的信息就会被打印到Debug窗口里,如下面的代码片段所示:
#include <crtdbg.h>
#ifdef _DEBUG 
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#endif 
void EnableMemLeakCheck() 

_CrtSetDbgFlag( _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) 
| _CRTDBG_LEAK_CHECK_DF ); 

int main() 

EnableMemLeakCheck();
_CrtSetBreakAlloc(53); 
int* pLeak = new int[10];
return 0;

在Debug模式下,程序退出时,内存块pLeak因为没有显式地释放,发生了内存泄漏,泄漏信息被打印出来:
Detected memory leaks!
Dumping objects ->
main.cpp(26) : {53} normal block at 0x002E1508, 40 bytes long.
Data: <  > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.
请读者思考一下,_CrtSetBreakAlloc(53)起到的是什么作用?目前这种方式只支持MS系统开发环境。当然,如果开发系统环境是Linux,也可以根据MS C-Runtime Library内建检测功能的实现方式开发出自己的Linux C-Runtime Library内建检测版本。外挂式的检测工具如果开发的是一个大型程序,MS C-Runtime Library提供的检测功能便显得有点笨拙了。此时,我们可以采用外挂式的检测工具BoundsChecker或Insure++。BoundsChecker采用的是一种被称为Code Injection的技术,来截获对分配内存和释放内存的函数的调用的。简单地说,当程序开始运行时,BoundsChecker的DLL被自动载入进程的地址空间中,然后它会修改进程中对内存分配和释放的函数调用,让这些调用首先转入它的代码,然后再执行原来的代码。BoundsChecker在做这些动作时,无须修改被调试程序的源代码或工程配置文件,这使得使用它非常简便、直接。而Insure++则是利用其专利技术( 源码插装和运行时指针跟踪)来发现大量的内存操作错误,准确报告错误的源代码行和执行轨迹。如果开发环境是Linux,MS C-Runtime Library内建检测功能就会彻底失效,BoundsChecker或Insure++也无能为力。这时,外挂式的检测工具Rational Purify或Valgrind便派上了用场。Rational Purify主要是针对软件开发过程中难以发现的内存错误、运行时错误。它可以在软件开发过程中自动地发现错误,准确地定位错误,并提供完备的错误信息,从而减少调试时间。同时它也是市场上唯一支持多种平台的相关工具,并且可以和很多主流开发工具集成。Purify 可以检查应用的每一个模块,甚至可以查出复杂的多线程或进程应用中的错误。另外,它不仅可以检查 C/C++,还可以对 Java 或 .NET 中的内存泄漏问题给出报告。在 Linux 系统中,使用 Purify非常简单,只须重新编译程序:
purify g++ -g main.cpp -o LeakDetector
运行编译生成的可执行文件LeakDetector,就可以定位出内存泄漏的具体位置。除了Rational Purify,Valgrind 也是Linux系统下开发应用程序时用于调试内存问题的有效工具。它尤其擅长发现内存管理的问题,检查发现程序运行时的内存泄漏。至于上述这些外挂式检测工具的具体使用方法就不赘述了。根据应用程序的具体情况,合理采用上述方法和工具,可以有效防止和查找代码中的内存泄漏问题,并且能和开发人员日常编码无缝结合,有效提高开发效率,增强应用程序鲁棒性。
请记住:内存泄露是一个大问题,但是可以通过一定的方法或借助于专业的检测工具,来查找并发现这些问题,有效地提升程序员的开发效率。