当前位置:C++技术网 > 精选软件 > VC++内存泄漏的检测与内存泄漏点定位

VC++内存泄漏的检测与内存泄漏点定位

更新时间:2016-08-10 18:54:14浏览次数:1+次

    本文大部分内容来自网络,只是做了适当的修改和补充,以便更贴近实际应用。

一 对于MFC程序
    如果检测到存在内存泄漏,退出程序的时候会在调试窗口提醒内存泄漏。例如:
class CMyApp : public CWinApp
{
public:
    BOOL InitApplication()
   {
      int* leak = new int[10];
      return TRUE;
   }
};

产生的内存泄漏报告大体如下:
Detected memory leaks!
Dumping objects ->
c:worktest.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

    双击“输出”窗口中,内存泄漏报告的文字"c:worktest.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.",或者在Debug窗口中逻辑按F4(VC++6.0),IDE就帮你定位到引起内存泄漏的对应文件的对应行,也就是这一句:
int* leak = new int[10];
    特别地,如果这个new仅对应一条delete(或者你把delete漏写),这将很快可以确认问题的症结。

二 对于非MFC
    需要做点工作,剩下的还是由VC++的C运行库去做。也就是说,只要你是VC++程序员,都可以很方便地检测内存泄漏。我们还是给个样例:
#include "crtdbg.h"
inline void EnableMemLeakCheck()
{
    _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
void main()
{
    EnableMemLeakCheck();
    int* leak = new int[10];
}
运行(提醒:不要按Ctrl+F5,按F5),你将发现,产生的内存泄漏报告与MFC类似,但有细节不同,如下:
Detected memory leaks!
Dumping objects ->
{52} normal block at 0x003C4410, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
差别在于:没有给出是哪一句引起的内存泄漏,从而不能双击定位。

那我们来模拟下MFC做的事情。看下例:

#include "crtdbg.h"
inline void EnableMemLeakCheck()
{
    _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
void main()
{
    EnableMemLeakCheck();
    int* leak = new int[10];
}

再运行这个样例,你惊喜地发现,现在内存泄漏报告和MFC没有任何分别了。

三 快速找到内存泄漏
    单确定了内存泄漏发生在哪一行,有时候并不足够。特别是同一个new对应有多处释放的情形。在实际的工程中,以下两种情况很典型:
    创建对象的地方是一个类工厂(ClassFactory)模式。很多甚至全部类实例由同一个new创建。对于此,定位到了new出对象的所在行基本没有多大帮助。
    COM对象。我们知道COM对象采用Reference Count维护生命周期。也就是说,对象new的地方只有一个,但是Release的地方很多,你要一个个排除。
    那么,有什么好办法,可以迅速定位内存泄漏?
    答:有。
    在内存泄漏情况复杂的时候,你可以用以下方法定位内存泄漏。这是我个人认为通用的内存泄漏追踪方法中最有效的手段。
    我们再回头看看crtdbg生成的内存泄漏报告:
Detected memory leaks!
Dumping objects ->
c:worktest.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
    除了产生该内存泄漏的内存分配语句所在的文件名、行号为,我们注意到有一个比较陌生的信息:{52}。这个整数值代表了什么意思呢?
    其实,它代表了第几次内存分配操作。象这个例子,代表了第52次内存分配操作发生了泄漏。你可能要说,我只new过一次,怎么会是第52次?这很容易理解,其他的内存申请操作在C的初始化过程调用的呗。:)
    有没有可能,我们让程序运行到第52次内存分配操作的时候,自动停下来,进入调试状态?所幸,crtdbg确实提供了这样的函数:即 long _CrtSetBreakAlloc(long nAllocID)。我们加上它:

#include "crtdbg.h"
inline void EnableMemLeakCheck()
{
    _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
void main()
{
    EnableMemLeakCheck();
    _CrtSetBreakAlloc(52);
    int* leak = new int[10];
}

    你发现,程序运行到 int* leak = new int[10]; 一句时,自动停下来进入调试状态。细细体会一下,你可以发现,这种方式你获得的信息远比在程序退出时获得文件名及行号有价值得多。因为报告泄漏文件名及行 号,你获得的只是静态的信息,然而_CrtSetBreakAlloc则是把整个现场恢复,你可以通过对函数调用栈分析以及其他在线调试技巧,来分析产生 内存泄漏的原因。通常情况下,这种分析方法可以在5分钟内找到肇事者。
    当然,_CrtSetBreakAlloc要求你的程序执行过程是可还原的(多次执行过程的内存分配顺序不会发生变化)。这个假设在多数情况下成立。不过,在多线程的情况下,这一点有时难以保证。

四 补充说明

对应非MFC需要加入如下若干语句:

#include "crtdbg.h"
inline void EnableMemLeakCheck()
{
    _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

    使用时,要先注释掉_CrtSetBreakAlloc语句,运行找到出错的内存序号后,再打开该语句。对于 _CrtSetBreakAlloc(long (52))函数。其中52是申请内存的序号(上面有说明),然后按F5运行(不需要设断点)。

1 在“VC++6.0”调试环境

    当按F5运行出现“Find Source”这个对话框,点击“Cancel取消”。 出现“User breakpoint called from code at xxxx”的对话框,点击“确定”,会看到一些汇编的代码,调出堆栈窗口(call stack),在其中的“main() line xxx + xxx bytes”上双击(或它的上一行双击,我的上一行是一个自定义函数,双击后直接定位到new的地方。当然,对于多个函数的,多双击有自己内容的函数,就会定位到出错的地方。

2 在VS2010环境下的VC2010方式

    当按F5运行出现 “Microsoft Visual Studio xxxx.exe 已触发了一个断点,中断,继续,忽略”提示框时,点击其中的“中断”按钮,然后连续按键“Shift+F11”(就是菜单上的 调试--跳出),就会返回到自己程序出错的语句上

3 这种情况我采用一种“试探法”。

    由于内存分配的块号不是固定不变的,而是每次运行都是变化的,所以跟踪起来很麻烦。但是我发现虽然内存分配的块号是变化的,但是变化的块号却总是那几个,也就是说多运行几次,内存分配的块号很可能会重复。因此这就是“试探法”的基础。

3.1 先在调试状态下运行几次程序,观察内存分配的块号是哪几个值;
3.2 选择出现次数最多的块号来设断点,在代码中设置内存分配断点:

添加如下一行(对于第 1875 个内存分配):

_crtBreakAlloc = 1875;

或者,可以使用具有同样效果的 _CrtSetBreakAlloc 函数:

_CrtSetBreakAlloc(1875); //我把这句放在了InitiaDialog里面

3.3 在调试状态下运行序,在断点停下时,打开“调用堆栈”窗口,找到对应的源代码处;

3.4 退出程序,观察“输出窗口”的内存泄露报告,看实际内存分配的块号是不是和预设值相同,如果相同,就找到了;如果不同,就重复步骤3,直到相同。

3.5 最后就是根据具体情况,在适当的位置释放所分配的内存。