在Windows下微软给咱们提供了一个十分强大的C/C++运行时库,这个运行时库中包含了不少有用的功能。而众多强大功能之一就是内存泄露的检测。html
C/C++提供了强大的内存管理功能,不过随之而来的倒是内存管理的复杂问题。内存泄露、踩内存等问题随之大量产生。要彻底杜绝这些问题是比较困难,不过一个高效有用的工具却能够将内存泄露的问题第一时间发现并处理掉。函数
VS的C/C++运行时库中内存管理系统的基础就是调试堆,调试堆和普通的堆不一样之处就在于每一块分配的内存先后都有一些额外的信息。下面就是每块分配内存的额外信息: 工具
typedef struct _CrtMemBlockHeader { struct _CrtMemBlockHeader * pBlockHeaderNext; struct _CrtMemBlockHeader * pBlockHeaderPrev; char * szFileName; int nLine; #ifdef _WIN64 /* These items are reversed on Win64 to eliminate gaps in the struct * and ensure that sizeof(struct)%16 == 0, so 16-byte alignment is * maintained in the debug heap. */ int nBlockUse; size_t nDataSize; #else /* _WIN64 */ size_t nDataSize; int nBlockUse; #endif /* _WIN64 */ long lRequest; unsigned char gap[nNoMansLandSize]; /* followed by: * unsigned char data[nDataSize]; * unsigned char anotherGap[nNoMansLandSize]; */ } _CrtMemBlockHeader;
从上面的定义咱们看到每一块分配的内存(假设的data变量)先后都有一个NoMansLoad的gap。这个gap会填充一些数据,检测是否内存操做超出了合法的范围。szFileName和nLine则表示该内存是从什么文件的哪一行分配的。nDataSize表示该块内存的实际大小,nBlockUse表示当前分配的内存的类型号是什么,类型号稍后会有详细解释。lRequest表示当前分配的序号,这个序号是进程惟一的,第一次分配的为1,第二次为2,以此类推。spa
C/C++不管使用new、malloc、strdup等最后都会到一个CRT的内部函数:_heap_alloc_dbg_impl。这个函数的做用就是填充上面看到的内存块的头和尾,而后作一些HOOK函数以及检查调试堆的工做。debug
有了上面关于调试堆管理内存的基本了解之后,咱们经过MSDN(ms-help://MS.MSDNQTR.v90.chs/dv_vccrt/html/cb4d2664-10f3-42f7-a516-595558075471.htm)能够看到一些CRT内存状态查看、管理的函数。调试
其中几个对内存泄露检测有用的函数是:code
_CrtDumpMemoryLeaks:将目前还没有释放的内存信息打印出来;orm
_CrtMemCheckpoint:创建一个当前堆上内存使用的快照;htm
_CrtMemDifference:比较两个快照之间的内存块变化;blog
_CrtSetBreakAlloc:指定在第几回内存分配的时候中断;
_CrtSetDbgFlag:设置一些调试的标志。
下面咱们经过几个实例来简单介绍内存泄露的使用方法:
1. 简单使用_CrtDumpMemoryLeaks:
#include "crtdbg.h" int main() { int *leakptr = (int *)malloc(100 * sizeof(int)); memset(leakptr, 0x5f, 100 * sizeof(int)); _CrtDumpMemoryLeaks(); return 0; }
输出结果:
Detected memory leaks!
Dumping objects ->
{54} normal block at 0x00393190, 400 bytes long.
Data: <________________> 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F
Object dump complete.
其中,绿色部分表示分配的内存序号,也就是该内存为第几块内存。红色部分为内存块的有效长度,蓝色部分为该内存的前16个字节。橙色部分为该内存的地址。
有了这些咱们已经能基本定位很是简单的内存泄露了。不过稍复杂一点的程序都有不少出口,在每一个出口都放一个_CrtDumpMemoryLeaks是不合适的。同时不少全局变量也是在main以后析构的。因此实际中咱们更加愿意使用下面的方法。
2. 增长_CRTDBG_LEAK_CHECK_DF标志
typedef std::list<int> intlist; class A { intlist* m_var; public: A(){ m_var = new intlist; } ~A(){ delete m_var; } }; A g_a; int main() { _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF); int *leakptr = (int *)malloc(100 * sizeof(int)); memset(leakptr, 0x5f, 100 * sizeof(int)); //_CrtDumpMemoryLeaks(); return 0; }
输出结果为:
Detected memory leaks!
Dumping objects ->
{54} normal block at 0x00393190, 400 bytes long.
Data: <________________> 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F
Object dump complete.
若是不使用_CrtSetDbgFlag而使用_CrtDumpMemoryLeaks输出结果为:
Detected memory leaks!
Dumping objects ->
{122} normal block at 0x00396248, 400 bytes long.
Data: <________________> 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F
{121} normal block at 0x00396200, 12 bytes long.
Data: < b9 b9 > 00 62 39 00 00 62 39 00 CD CD CD CD
{120} normal block at 0x003961A8, 24 bytes long.
Data: < > 00 00 00 00 CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
也就是说_CrtDumpMemoryLeaks仅能保证获取当前还没有释放的内存。而经过设置_CRTDBG_LEAK_CHECK_DF标志对于内存泄露检测更加有意义!
遗憾的是MFC使用的前一种,因此MFC默认检测的内存泄露意义不大。
3. 获取内存泄露的位置
#define _CRTDBG_MAP_ALLOC #include "crtdbg.h" int main() { _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF); int *leakptr = (int *)malloc( 100 * sizeof(int) ); memset(leakptr, 0x5f, 100 * sizeof(int)); return 0; }
输出:
Detected memory leaks!
Dumping objects ->
e:\vsprj\win32_test\mfc_console\mfc_console.cpp(30) : {122} normal block at 0x00396248, 400 bytes long.
Data: <________________> 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F
Object dump complete.
咱们看到红色的文件名和绿色的行号都出现了。对于这个咱们只须要很是简单地增长两行语句:
#define _CRTDBG_MAP_ALLOC
#include "crtdbg.h"
4. C++中new如何享受到这种便利
C++中的new能够支持替换,这给咱们提供了一个很是方便的解决方案。而CRT也提供了Debug版本的new,原型以下:
void *__CRTDECL operator new[](
size_t cb,
int nBlockUse,
const char * szFileName,
int nLine
)
void *__CRTDECL operator new(
size_t cb,
int nBlockUse,
const char * szFileName,
int nLine
)
因此咱们的思路就是将原来的new替换为debug版本的new。
#define _CRTDBG_MAP_ALLOC #include "crtdbg.h" #include <string.h> #ifdef _DEBUG #define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__) #define new DEBUG_CLIENTBLOCK #else #define DEBUG_CLIENTBLOCK #endif int main() { _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF); int *leakptr = new int[100]; memset(leakptr, 0x5f, 100 * sizeof(int)); return 0; }
输出:
Detected memory leaks!
Dumping objects ->
e:\vsprj\win32_test\mfc_console\mfc_console.cpp(21) : {54} client block at 0x00393190, subtype 0, 400 bytes long.
Data: <________________> 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F
Object dump complete.
5. 块类型的使用
块类型是CRT的一个重要功能,经过它咱们可让内存分配检查的粒度更加细化。所谓的快内存就是每次分配的时候指定的块号。块类型目前总共五种:
_NORMAL_BLOCK:就是咱们平常分配的默认类型;
_CRT_BLOCK:CRT本身使用的内存;
_CLIENT_BLOCK:就是给咱们本身用的客户类型,能够指定子类型号,也是咱们须要重点关注的类型;
_FREE_BLOCK:已经释放的块,由于CRT可让链表上保留已经释放的块来模拟内存不足的状况,因此这类内存通常被填充0xDD,块类型标为_FREE_BLOCK。
_IGNORE_BLOCK:有可能在一段时间内关闭调试堆操做。在该时间段内,内存块保留在列表上,但被标记为“忽略”块。
那么_CLIENT_BLOCK是如何使用的呢?还记得调试堆中的nBlockUse吧,这是一个32位的数据,其中低16位表示块的大类型,而高16位则表示子类型。因此每次咱们申请内存的时候给出的块类型都包含了大类型和子类型,分别设置他们的高16位和低16位便可。
例如:
#define IGS_SUBTYPE 0x50
#define IGS_BLOCK _CLIENT_BLOCK | ( IGS_SUBTYPE << 16 )
从定义看出IGS_BLOCK表示一个subtype为50的客户端数据块。
下面咱们用代码简单说明如何使用客户端数据块。
#define _CRTDBG_MAP_ALLOC #include "crtdbg.h" #include <string.h> #ifdef _DEBUG #define IGS_SUBTYPE 0x50 #define IGS_BLOCK _CLIENT_BLOCK | ( IGS_SUBTYPE << 16 ) #define IGS_DBGNEW new( IGS_BLOCK, __FILE__, __LINE__) #define new IGS_DBGNEW #else #define DEBUG_CLIENTBLOCK #endif int main() { _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF); int *leakptr = new int[100]; memset(leakptr, 0x5f, 100 * sizeof(int)); return 0; }
输出:
Detected memory leaks!
Dumping objects ->
e:\vsprj\win32_test\mfc_console\mfc_console.cpp(23) : {54} client block at 0x00393190, subtype 50, 400 bytes long.
Data: <________________> 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F 5F
Object dump complete.