内存泄漏对于每个C\C++程序员来说一定不会陌生,很多程序员的一生都深受其害,要写出一个没有内存泄漏的C++程序对于一个大型软件来说是非常困难的。那么有什么办法可以非常容易与检测程序的内存泄漏呢?
其实C运行时库,已经为我们准备了很多办法来检测内存泄漏,而且用起来非常方便,看下例:
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
int _tmain(int argc, _TCHAR* argv[])
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
int* i = new int;
return 0;
}
让我们运行以上代码,并查看VS的output窗口,当程序结束时,我们将看下如下输出:
Detected memory leaks!
Dumping objects ->
c:\users\jyang\documents\visual studio 2010\projects\dbugmemorycheck2\dbugmemorycheck2\memorycheck.cpp(12) : {101} normal block at 0x007D1B90, 4 bytes long.
Data: < > CD CD CD CD
Object dump complete.
很明显,输出内容指出在memorycheck.cpp文件的12行,存在一个4字节的内存泄漏,对应我们的代码,非常容易发现,我们new了一个int型的对象,却从没有释放它。
C运行库是怎么样如此精确地报告内存泄漏信息的呢?我们看到,与普通的代码相比,我们多包含了一个<crtdbg.h>头文件,定义了一个宏CRTDBG_MAP_ALLOC以及宏替换了一个new函数,在程序的开始,我们调用了一个CrtSetDbgFlag设置了若干信息,仅此而已。相要探知究竟,最好的办法就是打开<crtdbg.h>及其它一些相关的源码文件,阅读它的源代码。
为了更好的理解C运行库的源码,我将在这里做一个demo,来模拟C运行库对内存泄漏检测的支持。
C++程序员都知道,函数是可以重载与覆盖的,那么,我们就可以通过函数的重载,来重新定义内存申请与释放函数,如new, delete, malloc和free等。在重新定义的函数里,我们可以记录每一块内存的具体信息,跟踪它们的使用情况,并在最后程序结束的时候,查看哪些内存还没有被释放,重载的函数如下所示:
void* __cdecl operator new(size_t nSize, const char* lpszFileName, int nLine);
void __cdecl operator delete(void* p);
void* __cdecl operator new[](size_t nSize, const char* lpszFileName, int nLine);
void __cdecl operator delete[](void* p);
在重载的new函数中,我们添加了两个参数,new函数被调用时所处的源代码文件及在该文件中的行号,有了这两个参数,我们就可以将其记录下来以便于将来报告内存泄漏的具体信息所用。
对于每一块从堆中申请的内存,我们都需要记录其具体信息,如该堆在哪个源文件里被申请,内存的大小等;对于这些信息,我们又需要一个合适的数据结构来保存。由于我们常常需要对该数据结构进行查找,添加和删除操作,双向链表是一个非常不错的选择,于是,就有了如下的定义:
typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader * pBlockHeaderNext; //指向下一条记录
struct _CrtMemBlockHeader * pBlockHeaderPrev; //指向上一条记录
char * szFileName; //该内存在哪个源文件被申请
int nLine; //该内存在源文件的哪一行被申请
size_t nDataSize; //申请的内存有多大.
}_CrtMemBlockHeader;
由于每一个记录都保存了上一条记录与下一条记录,于是我们所有申请的内存信息就组成了一个双向链表。接下来我们要创建一个内存申请函数,该函数将会被new及malloc调用,因此有必要抽象出一个独立函数:
static void * _heap_alloc_dbg_impl(size_t nSize,
const char * szFileName,
int nLine
)
{
int blockSize = sizeof(_CrtMemBlockHeader) + nSize; //申请的内存大小为实际大小加上_CrtMemBlockHeader的大小
HANDLE hHeap = GetProcessHeap();
_CrtMemBlockHeader* pHead = (_CrtMemBlockHeader *)HeapAlloc(hHeap, 0, blockSize); //从堆中申请内存
//以下代码用于创建链表结构及给_CrtMemBlockHeader赋值。
if (_pFirstBlock)
_pFirstBlock->pBlockHeaderPrev = pHead;
else
_pLastBlock = pHead;
pHead->pBlockHeaderNext = _pFirstBlock;
pHead->pBlockHeaderPrev = NULL;
pHead->szFileName = (char *)szFileName;
pHead->nLine = nLine;
pHead->nDataSize = nSize;
_pFirstBlock = pHead;
return (char *)pHead + sizeof(_CrtMemBlockHeader);
}
代码里已包含了解释,该函数首先计算实际需要申请的内存的大小,由于我们需要一个_CrtMemBlockHeader结构来记录内存信息,所以实际需要的内存大小为原来需要的内存大小加上_CrtMemBlockHead结构的大小;我们将该内存的最前面部分用于保存_CrtMemBlockHeader信息,后面部分用于返回给调用者使用。接下来是构建链表结构及给_CrtMemBlockHeader赋值,最后通过(char *)pHead + sizeof(_CrtMemBlockHeader) 来计算除去给_CrtMemBlockHeader保留的内存外,真正给调用者使用的内存的地址,然后将该地址返回。
有了这个函数,我们就可以非常方便地完成new函数:
void* __cdecl operator new(size_t nSize, const char* lpszFileName, int nLine)
{
return _heap_alloc_dbg_impl(nSize, lpszFileName, nLine);
}
void* __cdecl operator new[](size_t nSize, const char* lpszFileName, int nLine)
{
return _heap_alloc_dbg_impl(nSize, lpszFileName, nLine);
}
new函数中,我们直接调用_heap_alloc_dbg_impl函数来申请内存。
明天我将继续完成接下来的内容。