在 Windows 下,通常使用 Visual Studio + C/C++ 开发完一个模块或者一个应用后
都要在 DEBUG 模式检测下是否有内存泄露的问题。
内存泄露, 即先前分配的内存使用之后却没有释放。轻微的内存泄漏对程序的性能并没有
明显的影响,一般不易察觉;严重的内存泄漏会对程序的性能有明显的影响,导致程序性
能下降明显,甚至会导致程序 Crash,或者由于内存耗尽,申请不到内存而退出。有时内
存泄漏会导致一些不相关的奇怪的现象出现,从而难以排查。不管内存泄漏严重与否,对
程序的正确性及健壮性都是一个威胁。本文根据网络上搜索到的资料编写而成,具体链接
见文章末尾。另外本文暂时只涉及使用 C Run-Time (CRT) 库对内存进行操作而导致的
内存泄漏问题。
使用 CRT + C/C++ 对内存的申请方式分为两种:1)使用 <stdlib.h> 中的 malloc ,
calloc;2)使用 C++ 的 new 操作符。
在 Visual Studio 中对 Memory Leak 的检测方法也分为两种:1)通过 <crtdbg.h>
中的 _CrtDumpMemoryLeaks 或者 _CrtSetDbgFlag 输出内存泄漏点;2) 通过
<crtdbg.h> 中的 _CrtMemCheckpoint , _CrtMemDifference ,
_CrtMemDumpStatistics 来定位内存泄漏点。检测方法 1)一般适用于对一个独立完整
的程序的检测排查,而方法 2)则适用于对复杂程序中的一个的模块的检测排查。
在检测方法 1)中要在程序和开头加上如下代码:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
NB:对于不使用 malloc , calloc ,只使用 new 对内存进行使用的程序 <stdlib.h> 可不
包含。 [Ref 1] 中说必须保持如上代码的书写顺序,不知是什么原因。(???)
如果程序的退出点只有一个,可以在主程序的入口使用 _CrtSetDbgFlag ,使程序在调试
状态运行结束后,在输出窗口输出内存泄漏点的信息,或者在主程序的出口使用
_CrtDumpMemoryLeaks ,使程序在调试状态运行结束后,在输出窗口输出内存泄漏点的
信息。如果程序有多个退出点,使用 _CrtDumpMemoryLeaks 的话, 需要在每个退出点
对 _CrtDumpMemoryLeaks 进行调用,而使用 _CrtSetDbgFlag 只需在程序的入口点
进行一次调用。相比较而言,如果程序有多个退出点,使用 _CrtSetDbgFlag 会比较方便。
在输出窗口输出的 Memory Leak 的信息格式如下:
Detected memory leaks!
Dumping objects ->
c:\users\\desktop\workshop\temp.cpp(46) : {139} normal block at 0x007C96E8, 8 bytes long.
Data: < > 00 00 00 00 00 00 00 00
Object dump complete.
此信息指明了内存泄漏点所在的源文件及行号:...\temp.cpp(46),内存的分配号:{139},
内存块的类型:normal block, 内存块的地址:0x007c96e8, 内存泄漏的大小:8 bytes long,
以及此内存块前 8 个字节的内容:Data:< > 00 00 00 00 00 00 00 00。
上面的 Memory Leak 输出的格式是在使用 malloc , calloc 的情况下输出的。如果使用
new 的话,输出的 Memory Leak 的格式如下:
Detected memory leaks!
Dumping objects ->
{139} normal block at 0x004596E8, 8 bytes long.
Data: < > 00 00 00 00 00 00 00 00
Object dump complete.
与上一个输出的 Memory Leak 的信息相比,这个输出缺少了内存泄漏点的源文件及行号。
在这种情况下怎么办呢?一般有两种方法:1)定义宏 new ,使得对 new 操作符的调用,
转换到对新定义的宏的调用,在宏定义中加入源文件名与行号,使行在输出的 Memory Leak
的信息中包含有源文件名与行号;2)使用 <crtdbg.h> 中的 _crtBreakAlloc 根据内存
分配号下断点,使得程序运行到内存泄漏点时暂停。
定义宏 new 的代码如下:
#ifdef _DEBUG
#ifndef DBG_NEW
#define DBG_NEW new (_NORMAL_BLOCK , __FILE__ , __LINE__)
#define new DBG_NEW
#endif
#endif // _DEBUG
把些代码放到程序开始的位置,当在 Debug 模式下运行结束后,输出窗口便会输出包含源文
件名与行号的 Memory Leak 的信息了。如果这种方法不可行,那么只能使用第 2) 中方法了,
即使用 _crtBreakAlloc 。
_crtBreakAlloc 实际上是一个 long int 类型的变量,如果在代码中使用的话,需要在程序的
入口处对 _crtBreakAlloc 赋值为待查找的内存泄漏点的内存分配号,比如:
_crtBreakAlloc = 139;
如果在调试的 Watch 窗口中使用的话,需要做一些变化。在 Watch 窗口的 name 列中输入
_crtBreakAlloc 然后在 value 列中输入待查找的内存泄漏点的分配号,比如 139。然后在
DEBUG 模式下继续运行,调试器便会在对分配号为 139 的内存块进行分配时中断,这里查
看代码与堆栈便可定位内存泄漏点的位置了。如果程序中使用的是多线程的 CRT ,在 Watch
窗口中的 name 列中输入的内容要稍作修改,改为 {,,msvcr120d.dll}_crtBreakAlloc。
其中的 120 可根据不同的 Visual Studio 的版本所使用的 CRT 的版本作相应的修改。
上述步骤完全是按照 [Ref 1] 中所描述的方法做的,实际上却行不通,不知为何。(??????)
要想在 Watch 窗口中使得这种方法有效,还需要修改下 name 列中的名字。当修改为
(long*){,,msvcr120d.dll}__crtBreakAlloc 时(两个下划线),再在其 value 列中填入待查
找的内存的分配号便可以了。[Ref 2] 在 Watch 窗口中设置这个值的前提是在程序的入口处,
或者所认为的可能的内存申请语句之前下断点,使程序停在这里后,再按这种方法设置,设置
好后,让程序继续在 DEBUG 模式下运行。
上面所说的是根据程序在 DEUBG 模式下调试运行结束时,output 窗口中输出 Memory Leak
的信息来定位内存泄漏点。下面来说另外一种方法,即使用<crtdbg.h> 中的 _CrtMemCheckpoint ,
_CrtMemDifference , _CrtMemDumpStatistics 来定位导致内在泄漏的代码块。这种方法同
前面一种方法一样,也需要定义宏 _CRTDBG_MAP_ALLOC,和包含 <crtdbg.h>。
找到可能导致内存泄漏的代码段,用两个 _CrtMemCheckpoint 的调用把可能代码段包起来,
在后一个 _CrtMemCheckpoint 调用之后,调用 _CrtMemDifference ,再使用
_CrtMemDumpStatistics 输出比较结果到 output 窗口。一步步缩小排查的范围,最终找出
导致内存泄漏的元凶。
参考链结:
[Ref 1]: Finding Memory Leaks Using the CRT Library
[Ref 2]: {,,msvcr100d.dll}_crtBreakAllo CXX0017:symbol not found