在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.