学习笔记:VS2005内存泄露检查

内存泄露指在某检查点,拿走的“堆”内存块未被归还。比如:

  • 使用malloc分配内存,如地址A = malloc(N);,若未调用free(地址A),就会造成内存泄露。
  • 使用new分配内存,像地址B = new 类型T;,若未调用delete 地址B,也会出现内存泄露。
  • 从用户内存池中获取内存块后,未执行相应的归还操作,同样属于“内存泄露”。

_CrtDumpMemoryLeaks()函数的功能是检查内存泄露,并在VC的输出窗口打印出泄露的内存块信息。

下面通过多个例子详细介绍在VS2005中检查内存泄露的方法:

  1. 示例1
    1
    2
    3
    4
    5
    6
    7
    #include <stdlib.h>
    #include <crtdbg.h>

    int main() {
    int* x = new int();
    _CrtDumpMemoryLeaks();
    }
    输出
    1
    2
    3
    4
    Detected memory leaks!
    Dumping objects ->
    {61} normal block at 0x00382650, 4 bytes long.
    Data: < > 00 00 00 00
    该示例成功检测到int* x对应的内存块泄露。
  2. 示例2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template <int Size>
    struct Test {
    Test() {
    m_p = new char[Size];
    }
    ~Test() {
    delete[] m_p;
    }
    char* m_p;
    };
    Test<123> t;

    int main() {
    int* x = new int();
    _CrtDumpMemoryLeaks();
    }
    输出
    1
    2
    3
    Detected memory leaks!
    {62} normal block at 0x00382708, 4 bytes long.
    {61} normal block at 0x00382650, 123 bytes long.
    这里出现了误报,原因是在调用_CrtDumpMemoryLeaks()时,全局变量t还未离开生存期,其析构函数~Test()未被调用,delete[] m_p也未执行。
  3. 示例3
    1
    2
    3
    4
    5
    Test<123> t;
    int main() {
    _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    int* x = new int();
    }
    输出
    1
    2
    Detected memory leaks!
    {62} normal block at 0x00382708, 4 bytes long.
    通过_CrtSetDbgFlag函数,让Crt库在程序完全退出时打印内存泄露情况。此时全局变量t已析构,误报消除。
  4. 示例4
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <string>
    using namespace std;

    #define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)
    #define new DEBUG_NEW

    int main() {
    _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    int* x = new int();
    }
    输出
    1
    2
    Detected memory leaks!
    c:\sdfdfsdf\sdfdfsdf.cpp(14) : {61} normal block at 0x00382650, 4 bytes long.
    此示例在调试输出窗口中显示了造成内存泄露的代码位置,双击可自动跳转到文本编辑器中对应的代码行。
  5. 示例5
    1
    2
    3
    4
    5
    #include <iostream>
    int main() {
    _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    std::cout << "hello world" << std::endl;
    }
    输出
    1
    2
    Detected memory leaks!
    {62} normal block at 0x00386028, 4480 bytes long.
    使用STLPort时出现了一大块内存泄露,换成微软自带的STL则正常。推测是cout这个全局变量中有一大块预分配的IO缓冲区未释放,该误报尚未解决。
  6. 示例6
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int main() {
    _CrtMemState s1, s2, s3;
    _CrtMemCheckpoint( &s1 );
    new int;
    new char;
    _CrtMemCheckpoint( &s2 );
    if ( _CrtMemDifference( &s3, &s1, &s2) ) _CrtMemDumpStatistics( &s3 );
    return 0;
    }
    输出
    1
    2
    3
    4
    5
    6
    7
    0 bytes in 0 Free Blocks.
    5 bytes in 2 Normal Blocks.
    0 bytes in 0 CRT Blocks.
    0 bytes in 0 Ignore Blocks.
    0 bytes in 0 Client Blocks.
    Largest number used: 0 bytes.
    Total allocations: 5 bytes.
    该API不仅能查找程序开始执行时的内存泄露,还能在任意一段代码段之间查找内存泄露。
  7. 示例7
    1
    2
    3
    4
    5
    int main() {
    _CrtSetBreakAlloc(62);
    int* x = new int;
    return 0;
    }
    输出:无。启动调试(F5),调试器会在int* x = new int;语句处暂停。打开“窗口->调用堆栈窗口”,能看到函数调用关系,并找到int* x = new int;这一行。

内存泄露输出信息中的{62}表示第62次分配内存时,这块内存泄露了。通过_CrtSetBreakAlloc调用,可让Crt库在第62次分配内存的调用时自动暂停程序,方便程序员检查函数调用栈。但无法保证下次程序运行时,第62次分配内存的调用就是int* x = new int;这句造成的。若程序没有复杂的时序相关逻辑(多线程)且输入值固定,则程序每次运行的行为是确定的。
8. 示例8

1
2
3
4
5
6
7
8
9
10
11
12
class Init_before_main {
public:
Init_before_main() {
_CrtSetBreakAlloc(62);
}
};
Init_before_main g_tmp;

int main() {
int* x = new int;
return 0;
}

这是对示例7的改进,确保在int* x = new int;调用之前调用_CrtSetBreakAlloc(62);,避免出现int* x = new int;在前,_CrtSetBreakAlloc(62);在后而漏过第62次分配调用的情况。

自己实现内存泄露检查工具的思路:重载newdelete运算符,使用自己的宏替换mallocfree。让所有的分配和释放动作都经过处理,以便加入统计信息等,从而实现内存泄露检查。