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


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

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



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 C# 中,可以使用 .NET 框架自带的垃圾回收器(Garbage Collector)来自动管理内存。垃圾回收器会自动跟踪并清理不再使用的对象,从而避免内存泄漏的问题。但是,在某些情况下,可能会出现一些难以检测和解决的内存泄漏问题,因此需要采取一些措施来检测和解决内存泄漏。 下面是一些常用的检测内存泄漏的方法: 1. 使用性能分析器(Profiler):Visual Studio 自带性能分析器,可以帮助开发者定位内存泄漏问题。可以在代码执行过程中,记录对象的创建和销毁情况,分析对象的引用关系,从而找出可能存在内存泄漏的问题。 2. 使用代码审查(Code Review):通过仔细阅读代码,查找可能导致内存泄漏的代码段。例如,没有正确释放资源、长时间持有对象的引用、循环引用等。 3. 使用内存分配跟踪工具:使用内存分配跟踪工具来记录和分析对象的创建和销毁情况,以及对象的引用关系。常用的工具有ANTS Memory Profiler、dotMemory、CLR Profiler 等。 4. 使用内存泄漏检测器:可以使用第三方内存泄漏检测器,例如 SciTech .NET Memory Profiler、dotTrace 等。这些工具可以自动检测内存泄漏问题,提供详细的报告和分析结果,方便开发者定位和解决问题。 总之,检测和解决内存泄漏需要开发者掌握一定的技能和经验。如果开发者能够遵循良好的编程习惯,并且使用适当的工具和技术,就可以避免和解决大部分的内存泄漏问题。 ### 回答2: C#是一种高级编程语言,它在内存管理方面具有自动垃圾回收的特性。自动垃圾回收(Garbage Collection,GC)可以帮助我们管理内存资源,释放不再使用的对象,避免内存泄漏的发生。然而,有时候我们仍然需要手动检测内存泄漏以确保性能和资源的有效利用。 以下是一些常用的方法来检测C#中的内存泄漏: 1. 使用性能计数器:使用C#内置的性能计数器(Performance Counter)可以监测应用程序的内存使用情况。通过监测应用程序的内存增长,可以判断是否存在内存泄漏。 2. 内存剖析器(Memory Profiler):使用一些第三方工具(如.NET Memory Profiler、ANTS Memory Profiler等),可以进行内存剖析,分析应用程序的内存使用情况。这些工具可以帮助我们找到内存泄漏的原因,提供可视化的数据和报告。 3. 对象生命周期跟踪:跟踪对象的创建和销毁,以确保对象在不再使用时能够被垃圾回收。可以通过代码调试和日志记录来实现。 4. 监测不正常的内存增长:在运行时,可以通过监测应用程序的内存使用情况来判断是否存在内存泄漏。如果内存使用量不断增长,而又没有明显的原因,可能就发生了内存泄漏。 5. 测试和性能优化:编写各种测试用例和模拟场景,通过大规模的测试和性能优化来发现和解决潜在的内存泄漏问题。 总之,在编写代码和开发应用程序时,我们应该遵循良好的编码习惯和内存管理的原则,避免出现内存泄漏的情况。及时使用合适的工具和方法来检测内存泄漏,并及时修复它们,以确保应用程序的稳定和性能。 ### 回答3: 在C#中,可以通过以下几种方式来检测内存泄漏: 1. 使用垃圾回收器(Garbage Collector):C#的垃圾回收器负责自动释放不再使用的对象和内存空间。通过垃圾回收机制,可以检测是否存在内存泄漏。如果内存中存在无法访问的对象,垃圾回收器会在适当的时候自动释放这些对象占用的内存。 2. 使用性能分析工具:C#提供了一些性能分析工具(如.NET Memory Profiler、ANTS Memory Profiler等),可以帮助我们检测内存泄漏。这些工具可以跟踪应用程序的内存使用情况,并提供详细的内存分析报告。通过分析报告,可以找到内存泄漏的根本原因。 3. 运行时调试:通过在代码中显示地释放不再使用的资源,可以减少内存泄漏的发生。在使用完对象后,及时调用它们的Dispose()方法或者使用using语句,确保资源在不再需要时得到释放。此外,还可以使用标准的调试技术,如断点调试、内存断点等,来检查内存泄漏问题。 4. 手动跟踪对象的引用:在代码中,可以手动追踪对象引用的生命周期。通过记录对象创建和销毁的时间点,可以判断是否有对象未被正确释放。通过监控对象引用的数量和生命周期,可以找出潜在的内存泄漏问题。 需要注意的是,内存泄漏并非只有在代码中显式地保留了对对象的引用才会发生,还可能由于事件处理、线程管理等问题导致。因此,在检测和解决内存泄漏问题时,需要综合考虑以上几种方法,并结合具体的业务场景进行分析和调试。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值