C运行时库分析-检测内存泄漏篇(模拟实现)

本文详细介绍了如何利用C运行时库检测并报告内存泄漏,通过包含<crtdbg.h>头文件和定义特定宏,实现内存泄漏的精确报告。进一步,通过重载new、delete等函数,模拟C运行库对内存泄漏的支持,实现更细致的内存管理。重点在于创建双向链表结构存储内存信息,以及实现内存申请函数,确保在程序结束时能准确报告未释放的内存。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


内存泄漏对于每个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函数来申请内存。

明天我将继续完成接下来的内容。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值