深度探索DxFramework 2-2

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

第2章 DxFramework的架构剖析 2

2.3 DxFramework的内存泄漏检测机制

内存泄漏(memory leak)是使用C/C++等程序设计语言进行编程时常常见到的一个问题。内存泄漏给程序运行时带来的影响有很多。一般最后的结果要么是程序死掉;要么是提示系统内存不足。还有一种可能就是程序不会死掉,但是程序的响应速度会明显变慢。

2.3.1 内存泄漏的原因

产生内存泄漏的原因一般是如下几点:

  1. 动态分配了内存空间,在不需要时忘记释放。
  2. 动态分配了内存空间,在不需要时忘记释放。
  3. 对某些API函数的不正确使用,导致内存泄漏。

在编程时忘记释放不需要的内存空间是编程时很常见。动态分配的内存在使用完毕后,如若不需要了就一定要释放。如果不释放,那就会造成内存的泄漏。如果造成内存泄漏的代码经常被调用的话,那么内存泄漏的数目就会越来越多的。从而影响整个系统的运行。 如下面的代码段:

1void SomeOperation()
2{
3    int* pBuffer = new int[32];
4    for( int i=0; i<32 ; ++i)
5    {
6        pBuffer[i] = i*2+1;
7    }
8}

很明显,上述的局部变量pTemp指向了一段从堆(heap)空间中分配的内存空间,而在函数退出时没有作任何的释放操作。一旦调用此函数就将导致了内存泄漏。当这个函数被频繁调用的时候,堆空间的内存空间将会被消耗殆尽。

因为代码编写的问题,会导致某些动态分配的内存根本就无从回收,比如下面的代码段:

 1void SomeOperation()
 2{
 3    int* pBuffer1 = new int[32];
 4    int* pBuffer2 = new int[32];
 5    pBuffer1 = pBuffer2;
 6
 7    if( pBuffer1 )
 8    {
 9        delete [] pBuffer1;
10    }
11}

这样,pBuffer1最初指向的那段内存空间的首地址就丢掉了,无法恢复了。这时候最初分配的那一段内存空间就无法释放掉。

Windows提供了一些特殊的API,如FormatMessage。如果给它参数传递的函数有FORMAT_MESSAGE_ALLOCATE_BUFFER,它会在函数内部动态分配一块内存缓冲区,返回给用户。但是这个内存缓冲区需要用户显式调用LocalFree来释放。如果用户忘了的话。也会产生内存泄漏。如下面的代码段:

 1#include <windows.h>
 2#include <iostream>
 3
 4using namespace std;
 5
 6void FormatAndPrintOneMessage(DWORD dwError)
 7{
 8    HLOCAL hLocal = NULL; // 用来存储返回的描述错误信息的字符串的内存缓冲区
 9    BOOL fOK = FormatMessage(
10    // 指定标志,动态分配一块缓冲区
11    FORMAT_MESSAGE_FROM_SYSTEM |FORMAT_MESSAGE_ALLOCATE_BUFFER , 
12    NULL , dwError , MAKELANGID(LANG_ENGLISH,SUBLANG_ENGLISH_US),
13
14    // 返回动态分配的内存缓冲区
15    reinterpret_cast<LPTSTR>(&hLocal),  0 , NULL );
16
17    if( hLocal && fOK )
18    {
19        cout<<"Error Message: "<<(reinterpret_cast<LPCTSTR>
20            (LocalLock(hLocal)))<<endl;
21
22      // 如果忘记了这一句,将会导致通过FormatMessage函数动态分配的
23      // 内存无法释放,从而导致内存泄漏
24      LocalFree(hLocal); 
25   }
26   else
27   {
28      cout<<"Error number not found"<<endl;
29   }
30}
31
32void main()
33{
34    FormatAndPrintOneMessage(5);
35}

检查已经发布的程序是否存在内存泄漏实在是费时费力,所以我们要把内存泄漏扼杀在萌芽状态。在编码过程中就要时刻进行检查。

2.3.2 DxFramework的检测方案

有很多方法可以检测程序是否存在内存泄漏。最简单也最现代化的办法大概就是用CompuWare公司的BoundChecker工具来检测了。但这些工具的价格往往只有企业单位才能承受得起,对于个人而言实在是贵了。幸运的是,Visual C++提供了一整套的方案。利用Visual Studio的调试器(debugger)和一套C Runtime(CRT)函数来检查内存泄漏。DxFramework就是采用这一解决方案。

DxFramework简单地使用了下面的几个CRT函数来检测内存泄漏。下面是DxFramework在main.cpp文件中WinMain函数的代码。

 1#include "globals.h"
 2 
 3int WINAPI WinMain( HINSTANCE instanceHnd,  HINSTANCE prevInstanceHnd,  char* p_CmdLine, int numCmds)
 4{    
 5    // Memory leak debugging... 
 6    //http://DxFramework.sourceforge.net/manual/29.html
 7    _CrtMemState memstate;
 8    _CrtMemCheckpoint(&memstate);
 9    
10    //_crtBreakAlloc = 383;    
11    // DO NOT ADD CODE BEFORE THIS LINE!
12    //...
13    // DO NOT ADD CODE AFTER THIS LINE!  
14    //(except the possible change of the return value) 
15    _CrtMemDumpAllObjectsSince(&memstate);
16
17    //Dump memory leaks, see comment at top of this function. 
18    return 0;// could return a more useful value here
19} 

一进和一出WinMain函数都受到了有_Crt前缀的一组“_Crt家族成员”的欢迎和欢送。没有使用过这些函数的程序员可能会对此觉得陌生。要剖析DxFramework的这一套机制,还是从查“_Crt家族”的“家底”开始。

Visual C++ 提供的一套CRT函数可以报告调试堆(debug heap)在某个指定时刻的内容。DxFramework使用了如下的几个结构体和函数:

struct _CrtMemState _CrtMemState结构体是用来存放调试堆在某个指定时刻的内容快照(snapshot)。
void _CrtMemCheckpoint( _CrtMemState *state ) 此函数将会设置一个内存检查点,获取调试堆的当前状态,并且将数值存储到由用户体提供的一个_CrtMemState结构体中去。
void _CrtMemDumpAllObjectsSince( const _CrtMemState *state) 函数将在调试器的output窗口中倾印出在堆中分配给用户的每一块内存空间的信息。这些信息要么是从程序开始处;要么是从指定的调试堆状态处。当传递的参数为NULL时,倾印出从程序启动处的堆空间分配信息。

DxFramework内存泄漏侦测机制的原理如下:

首先在WinMain函数开始处,启动DxFramework之前,获取到内存状态的快照。然后在WinMain结束处,即假定所有的被申请的内存空间已被释放的地方,再获取一次内存状态的快照。这时候被分配而在程序结束时还未被释放的内存空间都将被显示出来。只要程序是以Debug模式编译链接。所有的未被释放的内存将被标识(tagged)。通过这个标识(tag),用户可以查出这段未被释放的内存是在代码何处被分配的。当代码是以Release模式编译的时候,这些侦测函数将会自动失效。仿照DxFramework的内存泄漏侦测架构,这里以一个简单的程序为例子:

 1#include <windows.h>
 2#include <iostream>
 3#include <crtdbg.h> // 要使用CRT内存泄漏检测机制,就必须包含这个头文件
 4
 5using namespace std;
 6
 7int main()
 8{  
 9    _CrtMemState memstate;
10    _CrtMemCheckpoint(&memstate); // 在程序开始处设置一个检查点,获取开始时的内存快照
11    //_crtBreakAlloc = 383;    
12    // DO NOT ADD CODE BEFORE THIS LINE!
13
14    int* pTmp = new int[32];
15    int* pTmp2 = new int[16];
16    //DO NOT ADD CODE AFTER THIS LINE!
17    //(except the possible change of the return value)
18    _CrtMemDumpAllObjectsSince(&memstate); // 检查从程序开始处的内存分配和释放情况,并且倾印出来
19    return 0;
20}

编译运行这段程序,很明显,这段程序是会产生内存泄漏。运行后,Visual Studio的调试器output窗口将会出现如下的提示:

1Dumping objects ->
2{43} normal block at 0x00340950, 64 bytes long.
3Data: <  > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
4{42} normal block at 0x00340888, 128 bytes long.
5Data: <  > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
6Object dump complete.

分析上面的提示信息:

{43},{42}: 花括弧内的数字是内存分配序号。

64 bytes ,128 bytes: 表示两个分配的内存块的大小,分别为64个字节和128个字节

CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD: 表示内存块的前16个字节的内容,从低位到到高位排列

0x00340950,0x00340888: 表示在地址0x00340888起始处,有一个128字节大小的内存块被分配,但是未被释放。在地址0x00340950起始处,有一个64字节大小的内存块被分配,但也未被释放。

normal block: 表示产生泄漏的内存块的类型,CRT把每一个被分配的内存块分为以下几种类型:

名称 描述
normal block(普通块) 是指由用户代码分配的内存
client block(客户端块) 是一种特殊类型的内存块,专用于MFC程序中有析构函数的对象。MFC的new操作符视具体情况既可以为所创建的对象建立普通块,也可以为之建立客户块。
CRT block(C运行期块) 是由 C RunTime Library 供自己使用而分配的内存块。由 CRT 库自己来管理这些内存的分配与释放,一般不会在内存泄漏报告中发现CRT内存泄漏,除非程序发生了严重的错误。(例如 CRT 库崩溃)。
free block(空闲块) 已经被释放(free)的内存块。
Ignore block(忽略块) 这是程序员显式声明过不要在内存泄漏报告中出现的内存块。

最后两种类型的内存块,它们不会出现在内存泄漏报告中:

上面提示了检测到代码中有导致内存泄漏的地方,但是仍未有明确的信息告知用户引起内存泄漏的原因。调试器只是告诉用户一段数据,通过再次运行此代码。这个数据可以帮助用户找到泄漏处。

注意在_CrtMemCheckpoint函数后有一句注解语句

1//_crtBreakAlloc = 383;

crtBreakAlloc其实是一个CRT运行库的全局变量。再观察提示内存泄漏的倾印信息,最下面一句的提示语句,即

{42} normal block at 0x00340888, 128 bytes long.

中花括号内的数字,即42,把原来的383替换掉,然后把注解符号去掉,使其成为一句有效的代码语句。重新编译链接,再次以debug方式启动程序,这时候,程序将会在new操作符或者是malloc函数的内部实现代码处停下,弹出一个提示对话框(图Image0003.bmp)。这时候通过观察函数调用栈便可以得知未被释放的内存是在何处被分配的。如下:

1_heap_alloc_dbg(unsigned int 128, int 1, const char * 0x00000000, int 0) line 338
2_nh_malloc_dbg(unsigned int 128, int 1, int 1, const char * 0x00000000, int 0) 
3line 248 + 21 bytes
4_nh_malloc(unsigned int 128, int 1) line 197 + 19 bytes
5operator new(unsigned int 128) line 24 + 11 bytes
6main() line 15 + 10 bytes
7mainCRTStartup() line 206 + 25 bytes
8KERNEL32! 77e71af6()

值得注意的是,采用上述的侦测方法检查是否有内存泄漏,当在IDE的output窗口没有倾印出泄漏信息,并不意味着你的代码一定不存在内存泄漏(的可能),这仅仅代表着,在最后一次的调试运行时,没有执行到“有分配而无释放”的代码而已。如下代码:

 1#include <iostream>
 2#include <crtdbg.h>
 3
 4using namespace std;
 5
 6void main()
 7{
 8    _CrtMemState memstate;
 9    _CrtMemCheckpoint(&memstate);
10
11    int number;
12    int *p = NULL *q = NULL;
13    cout<<"please input an arbitrary number: "<<endl;
14    cin>>number;
15
16    if( number )
17    {
18        p = new int;
19    }
20    else
21    {
22        q = new int;
23    }
24
25    if( p ){delete p;}
26
27    _CrtMemDumpAllObjectsSince(&memstate);
28}

当输入一个非零的数字值的时候,使用这种机制并不会有任何的内存泄漏信息被倾印出来——因为导致内存泄漏的代码并没有被执行到。

在C++代码中,要使用CRT的debug heap来进行调试,必须要使用_CRTDBG_MAP_ALLOC宏 ,

当使用了上述的符号时,的所有在代码中的new操作符都会自动映射到debug版本的new操作符,这时候将会时候将会自动记录new语句所在的源文件文件名和所在行行号。DxFramework在globas.h定义了此宏,如下:

1#ifdef _DEBUG
2#define _CRTDBG_MAP_ALLOC // include Microsoft memory leak detection procedures
3#define _INC_MALLOC          // exclude standard memory  alloc procedures
4#define enw DEBUG_NEW     // 注,此处是应是程序设计者的笔误。enw应为“new”
5#endif