分类: C/C++
2016-08-09 13:54:48
原文地址:浅谈C/C++内存泄漏及其检测工具 作者:帅得不敢出门
#ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif |
有了这样的定义,在编译DEBUG版时,出现在这个cpp文档中的任何new都被替换成DEBUG_NEW了。那么DEBUG_NEW是什么呢?DEBUG_NEW也是个宏,以下摘自afx.h,1632行
#define DEBUG_NEW new(THIS_FILE, __LINE__) |
所以假如有这样一行代码:
char* p = new char[200]; |
经过宏替换就变成了:
char* p = new( THIS_FILE, __LINE__)char[200]; |
根据C++的标准,对于以上的new的使用方法,编译器会去找这样定义的operator new:
void* operator new(size_t, LPCSTR, int) |
我们在afxmem.cpp 63行找到了一个这样的operator new 的实现
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine) { return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine); } void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine) { … pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine); if (pResult != NULL) return pResult; … } |
第二个operator new函数比较长,为了简单期间,我只摘录了部分。很显然最后的内存分配还是通过_malloc_dbg函数实现的,这个函数属于MS C-Runtime Library 的Debug Function。这个函数不但需要传入内存的大小,另外更有文档名和行号两个参数。文档名和行号就是用来记录此次分配是由哪一段代码造成的。假如这块内存在程式结束之前没有被释放,那么这些信息就会输出到Debug窗口里。
这里顺便提一下THIS_FILE,__FILE和__LINE__。__FILE__和__LINE__都是编译器定义的宏。当碰到__FILE__时,编译器会把__FILE__替换成一个字符串,这个字符串就是当前在编译的文档的路径名。当碰到__LINE__时,编译器会把__LINE__替换成一个数字,这个数字就是当前这行代码的行号。在DEBUG_NEW的定义中没有直接使用__FILE__,而是用了THIS_FILE,其目的是为了减小目标文档的大小。假设在某个cpp文档中有100处使用了new,假如直接使用__FILE__,那编译器会产生100个常量字符串,这100个字符串都是飧?/SPAN>cpp文档的路径名,显然十分冗余。假如使用THIS_FILE,编译器只会产生一个常量字符串,那100处new的调用使用的都是指向常量字符串的指针。
再次观察一下由MFC Application Wizard生成的项目,我们会发现在cpp文档中只对new做了映射,假如您在程式中直接使用malloc函数分配内存,调用malloc的文档名和行号是不会被记录下来的。假如这块内存发生了泄漏,MS C-Runtime Library仍然能检测到,但是当输出这块内存块的信息,不会包含分配他的的文档名和行号。
要在非MFC程式中打开内存泄漏的检测功能很容易,您只要在程式的入口处加入以下几行代码:
int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG ); tmpFlag |= _CRTDBG_LEAK_CHECK_DF; _CrtSetDbgFlag( tmpFlag ); |
这样,在程式结束的时候,也就是winmain,main或dllmain函数返回之后,假如更有内存块没有释放,他们的信息会被打印到Debug窗口里。
假如您试着创建了一个非MFC应用程式,而且在程式的入口处加入了以上代码,并且故意在程式中不释放某些内存块,您会在Debug窗口里看到以下的信息:
{47} normal block at 0x00C91C90, 200 bytes long. Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F |
内存泄漏的确检测到了,但是和上面MFC程式的例子相比,缺少了文档名和行号。对于一个比较大的程式,没有这些信息,解决问题将变得十分困难。
为了能够知道泄漏的内存块是在哪里分配的,您需要实现类似MFC的映射功能,把new,maolloc等函数映射到_malloc_dbg函数上。这里我不再赘述,您能够参考MFC的源代码。
由于Debug Function实现在MS C-RuntimeLibrary中,所以他只能检测到堆内存的泄漏,而且只限于malloc,realloc或strdup等分配的内存,而那些系统资源,比如HANDLE,GDI Object,或是不通过C-Runtime Library分配的内存,比如VARIANT,BSTR的泄漏,他是无法检测到的,这是这种检测法的一个重大的局限性。另外,为了能记录内存块是在哪里分配的,源代码必须相应的配合,这在调试一些老的程式很麻烦,毕竟修改源代码不是一件省心的事,这是这种检测法的另一个局限性。
对于研发一个大型的程式,MS C-Runtime Library提供的检测功能是远远不够的。接下来我们就看看外挂式的检测工具。我用的比较多的是BoundsChecker,一则因为他的功能比较全面,更重要的是他的稳定性。这类工具假如不稳定,反而会忙里添乱。到底是出自鼎鼎大名的NuMega,我用下来基本上没有什么大问题。