1、概述
在Windows下微軟給我們提供了一個十分強大的C/C++運行時庫,這個運行時庫中包含了很多有用的功能。而眾多強大功能之一就是內存泄露的檢測。
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,以此類推。
C/C++無論使用new、malloc、strdup等最后都會到一個CRT的內部函數:_heap_alloc_dbg_impl。這個函數的作用就是填充上面看到的內存塊的頭和尾,然后做一些HOOK函數以及檢查調試堆的工作。
有了上面關於調試堆管理內存的基本了解以后,我們通過MSDN(ms-help://MS.MSDNQTR.v90.chs/dv_vccrt/html/cb4d2664-10f3-42f7-a516-595558075471.htm)可以看到一些CRT內存狀態查看、管理的函數。
其中幾個對內存泄露檢測有用的函數是:
_CrtDumpMemoryLeaks:將目前尚未釋放的內存信息打印出來;
_CrtMemCheckpoint:建立一個當前堆上內存使用的快照;
_CrtMemDifference:比較兩個快照之間的內存塊變化;
_CrtSetBreakAlloc:指定在第幾次內存分配的時候中斷;
_CrtSetDbgFlag:設置一些調試的標志。
2、 基本使用方法
下面我們通過幾個實例來簡單介紹內存泄露的使用方法:
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.