检测是否存在内存泄漏问题
Windows平台下面Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法,原理大致如下:内存分配要通过CRT在运行时实现,只要在分配内存和释放内存时分别做好记录,程序结束时对比分配内存和释放内存的记录就可以确定是不是有内存泄漏。在vs中启用内存检测的方法如下:
- STEP1,在程序中包括以下语句: (#include 语句必须采用上文所示顺序。 如果更改了顺序,所使用的函数可能无法正常工作。)
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
通过包括 crtdbg.h,将 malloc 和 free 函数映射到它们的调试版本,即 _malloc_dbg 和 _free_dbg,这两个函数将跟踪内存分配和释放。 此映射只在调试版本(在其中定义了_DEBUG)中发生。 发布版本使用普通的 malloc和 free函数。
#define
语句将 CRT 堆函数的基版本映射到对应的“Debug”版本。 并非绝对需要该语句;但如果没有该语句,内存泄漏转储包含的有用信息将较少。
STEP2, 在添加了上述语句之后,可以通过在程序中包括以下语句(通常应恰好放在程序退出位置之前)来转储内存泄漏信息:
_CrtDumpMemoryLeaks();
未定义 _CRTDBG_MAP_ALLOC 时,所显示的会是:
1. 内存分配编号(在大括号内)。
2. 块类型(普通、客户端或 CRT)。
3. 十六进制形式的内存位置。
4. 以字节为单位的块大小。
5. 前 16 字节的内容(亦为十六进制)。
块类型分为:
1. “普通块”是由程序分配的普通内存。
2. “客户端块”是由 MFC 程序用于需要析构函数的对象的特殊类型内存块。 MFC new 操作根据正在创建的对象的需要创建普通块或客户端块。
3. “CRT 块”是由 CRT 库为自己使用而分配的内存块。 CRT 库处理这些块的释放,因此您不大可能在内存泄漏报告中看到这些块,除非出现严重错误(例如 CRT 库损坏)。
从不会在内存泄漏信息中看到下面两种块类型:
1. “可用块”是已释放的内存块。
2. “忽略块”是您已特别标记的块,因而不出现在内存泄漏报告中。
定义了 _CRTDBG_MAP_ALLOC 时,还会显示在其中分配泄漏的内存的文件。 文件名后括号中的数字(本示例中为 10)是该文件中的行号。
如果程序总是在同一位置退出,调用 _CrtDumpMemoryLeaks 将非常容易。 如果程序从多个位置退出,则无需在每个可能退出的位置放置对_CrtDumpMemoryLeaks 的调用,而可以在程序开始处包含以下调用:
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
该语句在程序退出时自动调用 _CrtDumpMemoryLeaks。 必须同时设置 _CRTDBG_ALLOC_MEM_DF和 _CRTDBG_LEAK_CHECK_DF两个位域
定位具体的内存泄漏地方
定位内存泄漏的另一种技术涉及在关键点对应用程序的内存状态拍快照。 CRT 库提供一种结构类型 _CrtMemState,您可用它存储内存状态的快照:
_CrtMemState s1, s2, s3;
若要在给定点对内存状态拍快照,请向 _CrtMemCheckpoint 函数传递 _CrtMemState 结构。 该函数用当前内存状态的快照填充此结构:
_CrtMemCheckpoint( &s1 );
通过向 _CrtMemDumpStatistics 函数传递 _CrtMemState 结构,可以在任意点转储该结构的内容:
_CrtMemDumpStatistics( &s1 );
若要确定代码中某一部分是否发生了内存泄漏,可以在该部分之前和之后对内存状态拍快照,然后使用 _CrtMemDifference 比较这两个状态:
_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );
顾名思义,_CrtMemDifference 比较两个内存状态(s1 和 s2),生成这两个状态之间差异的结果(s3)。 在程序的开始和结尾放置 _CrtMemCheckpoint 调用,并使用_CrtMemDifference 比较结果,是检查内存泄漏的另一种方法。 如果检测到泄漏,则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来划分程序和定位泄漏。
VLD
https://archive.codeplex.com/?p=vld
Visual Leak Detector是一个免费,强大的开源内存泄漏检测系统,适用于Visual C ++。
关键api:
_CrtSetAllocHook
StackWalk64
SymGetLineFromAddr64
SymFromAddr
Allocation Hooking
对我们来说幸运的是,Microsoft提供了一种简单的方法来监视从调试堆进行的每个分配:分配挂钩。分配钩子只是一个用户提供的回调函数,它将在从调试堆进行每次分配之前调用。Microsoft提供了一个函数,_CrtSetAllocHook它使用调试堆注册分配挂钩函数。当调试堆调用分配挂钩时,传递的参数之一是唯一标识每个分配的ID号 - 它基本上是分配的每个内存块的序列号。内存块标题中没有足够的空间让我们直接在其中记录任何信息,但我们可以使用这个唯一的ID号作为键将每个块映射到我们想要记录的任何数据。
Walking the Stack
现在我们有一种方法可以在每次分配一个块时得到通知,以及一种唯一标识每个分配的方法,剩下要做的就是每次分配时记录调用堆栈。我们可以想象使用内联汇编来尝试展开堆栈。但是堆栈帧可以以不同的方式组织,这取决于编译器优化和调用约定,因此以这种方式执行它可能变得复杂。微软再一次为我们提供了一个帮助我们的工具。这次它是一个函数,我们可以迭代地调用来逐帧遍历堆栈。那个功能是StackWalk64。它是Debug Help Library(dbghelp.dll)的一部分)。只要我们为它提供建立起始“参考框架”所需的信息,可以说,它可以从那里检查我们的堆栈并为我们可靠地解除它。每次StackWalk64调用时,它都会返回一个STACKFRAME64结构,可以将其重新用作下次调用的输入StackWalk64。它可以以这种方式重复调用,直到到达堆栈的末尾。
Initializing the Memory Leak Detector
我们现在有一个更好的内存泄漏检测器的开始。我们可以监控每个分配,对于每个监控的分配,我们可以获取并记录堆栈跟踪。剩下的唯一挑战是确保在程序开始执行时立即向调试堆注册分配挂钩函数。这可以通过创建C ++类对象的全局实例来非常简单地解决。初始化程序时,构造函数将运行。从构造函数中,我们可以调用_CrtSetAllocHook注册我们的分配钩子函数。但是等等,如果我们正在调试的程序已经有了其他的话分配内存的全局C ++类对象?我们如何确保首先调用构造函数,并且在构造任何其他全局对象之前安装我们的分配钩子函数?不幸的是,C ++规范没有说明决定构造全局对象的顺序的任何规则。因此,没有绝对保证我们的构造函数将首先被调用。但我们可以非常接近保证它。我们可以利用特定于编译器的预处理器指令,该指令明确告诉编译器确保我们的全局变量尽快构建:#pragma init_seg (compiler)。该指令告诉编译器将我们的全局对象放在“编译器”初始化段中。此段中的对象是第一个构造的对象。接下来,构建“库”段中的对象,并且最后构造“用户”段中的对象。“用户”段是全局对象的默认段。一般来说,不应将普通用户对象放在“编译器”段中,因此这提供了在任何用户对象之前构造我们的全局对象的合理确定性。
检测内存泄漏
因为全局对象以它们构造的相反顺序被销毁,所以我们的全局对象将在任何用户对象之后被销毁。然后我们可以检查堆,就像内置检测器一样。如果我们在堆上找到一个尚未释放的块,则它是一个泄漏,我们可以使用我们的分配钩子函数记录的唯一ID号查找其调用堆栈。STL映射在这里可以正常工作,用于将ID号映射到调用堆栈。我没有使用STL地图,因为我希望我的库兼容新旧版本的Visual C ++。旧版本的STL与较新版本不兼容,因此我无法使用STL组件。但好消息是,这让我有机会创建一个与STL地图概念相似的数据结构,
你还记得内置的泄漏检测器在内存块中对等,以获取文件的名称和分配块的行号吗?好吧,我们所有的调用堆栈都是一堆程序地址。将所有这些十六进制数转储到调试器中的用处不大。为了使这些地址更有意义,我们需要将它们转换为人类可读的信息:文件和行号(以及函数名称)。微软再次提供了有助于我们完成工作的工具:符号处理程序API。就像StackWalk64,它们也恰好是Debug Help Library的一部分。我不会在这里详细介绍它们,因为它们有很多,而且使用起来非常简单。他们不需要那么多的聪明才智StackWalk64确实。我们可以使用两个符号处理程序API来获取我们想要的文件名,行号和函数名。恰当命名的SymGetLineFromAddr64地址将地址转换为源文件名和行号。它的姐妹API SymFromAddr将地址转换为符号名称。对于我们所拥有的程序地址,相应的符号名称将是包含该程序地址的函数的名称。
源代码的关键部分
如果你对上面的部分感到厌倦并跳过了,我会在这里总结一下。简而言之,这个内存泄漏检测器的工作原理如下:
自动构建全局对象。它是第一个构建的对象。构造函数注册我们的分配钩子函数。
每个分配最终调用我们的分配钩子函数。分配挂钩函数获取并记录每个分配的调用堆栈。调用堆栈信息记录在类似STL的专用映射中。
程序终止时,全局对象是最后一个被销毁的对象。它检查堆并识别泄漏。在地图中查找泄漏的块并与其对应的调用堆栈进行匹配。生成的数据将发送到调试器以进行显示。
限制
VLD不会检测COM泄漏,进程外资源泄漏或与CRT堆无关的任何其他类型的内存泄漏。简单来说,VLD只检测因调用new或导致的内存泄漏malloc。请记住,VLD是作为内置内存泄漏检测器的替代产品而创建的,它也只检测来自new和的泄漏malloc。
内容转载:
太有用了,故直接转载以下链接内容
最快速度找到内存泄漏
确认是否存在内存泄漏
inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
void main()
{
EnableMemLeakCheck();
int* leak = new int[10];
}
如果检测到存在内存泄漏,退出程序的时候会在调试窗口提醒内存泄漏。现在内存泄漏报告和MFC没有任何分别了。
快速找到内存泄漏
单确定了内存泄漏发生在哪一行,有时候并不足够。特别是同一个new对应有多处释放的情形。在实际的工程中,以下两种情况很典型:
创建对象的地方是一个类工厂(ClassFactory)模式。很多甚至全部类实例由同一个new创建。对于此,定位到了new出对象的所在行基本没有多大帮助。
COM对象。我们知道COM对象采用Reference Count维护生命周期。也就是说,对象new的地方只有一个,但是Release的地方很多,你要一个个排除。
我们让程序运行到第52次内存分配操作的时候,自动停下来,进入调试状态?所幸,crtdbg确实提供了这样的函数:即 long _CrtSetBreakAlloc(long nAllocID)。我们加上它:
inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
void main()
{
EnableMemLeakCheck();
_CrtSetBreakAlloc(52);
int* leak = new int[10];
}
Aspect C ++
面向AOP切面编程
实现一个内存分配记录管理器:
memory_recorder.h
#ifndef __MEMORY_RECORDER_H__
#define __MEMORY_RECORDER_H__
#include <map>
#include <typeinfo>
using namespace std;
class MemoryRecorder
{
map<void *, const type_info *> objects;
public:
~MemoryRecorder ();
void addObject(void *obj, const type_info &ti);
void removeObject(void *obj, const type_info &ti);
};
extern MemoryRecorder g_memoryRecorder;
#endif // __MEMORY_RECORDER_H__
memory_recorder.cc
#include "memory_recorder.h"
#include <iostream>
using namespace std;
MemoryRecorder g_memoryRecorder;
MemoryRecorder::~MemoryRecorder ()
{
if (objects.size() > 0)
{
cout << objects.size() << " objects not released:" << endl;
for (map<void *, const type_info *>::const_iterator iter = objects.begin();
iter != objects.end();
iter ++)
{
cout << "\t" << iter->second->name() << ": " << (iter->first) << endl;
delete (iter->first);
}
}
}
void MemoryRecorder::addObject(void *obj, const type_info &ti)
{
objects.insert(make_pair(obj, &ti));
}
void MemoryRecorder::removeObject(void *obj, const type_info &ti)
{
objects.erase(obj);
}
实现方面,test.ah
#ifndef __TEST_AH__
#define __TEST_AH__
#include "memory_recorder.h"
#include <iostream>
using namespace std;
aspect MemberRecorder
{
pointcut all_class() = classes("Test%");
advice construction (all_class()) : after ()
{
g_memoryRecorder.addObject (tjp->target(), typeid(*tjp->target()));
}
advice destruction (all_class()) : after ()
{
g_memoryRecorder.removeObject (tjp->target(), typeid(*tjp->target()));
}
};
#endif // __TEST_AH__
这个方面实现的功能很简单,首先定义了一个pointcut(切面),它匹配所有以“Test”开头的类。
接下来定义了2个处理方法,分别在这些类的构造函数和析构函数调用之后执行。
tjp->target()指向Test*对象实例
其他
实际项目中检测内存泄漏通常会十分繁琐,所以有许多工具帮助我们检测内存泄漏,比如 mtrace,valgrind。