一般用在MFC中比較準確,在InitInstance裏面調用_CrtDumpMemoryLeaks
Detected memory leaks!
Dumping objects ->
{52} normal block at 0x006D2498, 512 bytes long.
?Data: <??????????????? > 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
{51} normal block at 0x006D2440, 24 bytes long.
?Data: < 4????????????? > 10 34 14 00 FF FF FF FF 00 00 00 00 00 00 00 00
Object dump complete.
3._CrtSetBreakAlloc
知道某個錯誤分配塊的分配請求編號後,可以將該編號傳遞給 _CrtSetBreakAlloc 以創建一個斷點
_CrtSetBreakAlloc(51);這樣可以快速在{51}次內存泄漏處設上斷點。
最快速度找到內存泄漏
許式偉
2006年11月某日
確認是否存在內存泄漏
我們知道,MFC程序如果檢測到存在內存泄漏,退出程序的時候會在調試窗口提醒內存泄漏。例如:
{
public:
BOOL InitApplication()
{
int* leak = new int[10];
return TRUE;
}
};
產生的內存泄漏報告大體如下:
Dumping objects ->
c:/work/test.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
這挺好。問題是,如果我們不喜歡MFC,那麼難道就沒有辦法?或者自己做?
呵呵,這不需要。其實,MFC也沒有自己做。內存泄漏檢測的工作是VC++的C運行庫做的。也就是說,只要你是VC++程序員,都可以很方便地檢測內存泄漏。我們還是給個樣例:
inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
void main()
{
EnableMemLeakCheck();
int* leak = new int[10];
}
運行(提醒:不要按Ctrl+F5,按F5),你將發現,產生的內存泄漏報告與MFC類似,但有細節不同,如下:
Dumping objects ->
{52} normal block at 0x003C4410, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
爲什麼呢?看下面。
定位內存泄漏由於哪一句話引起的
你已經發現程序存在內存泄漏。現在的問題是,我們要找泄漏的根源。
一般我們首先確定內存泄漏是由於哪一句引起。在MFC中,這一點很容易。你雙擊內存泄漏報告的文字,或者在Debug窗口中按F4,IDE就幫你定位到申請該內存塊的地方。對於上例,也就是這一句:
int* leak = new int[10];
這多多少少對你分析內存泄漏有點幫助。特別地,如果這個new僅對應一條delete(或者你把delete漏寫),這將很快可以確認問題的癥結。
我們前面已經看到,不使用MFC的時候,生成的內存泄漏報告與MFC不同,而且你立刻發現按F4不靈。那麼難道MFC做了什麼手腳?
其實不是,我們來模擬下MFC做的事情。看下例:
{
_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的地方很多,你要一個個排除。
那麼,有什麼好辦法,可以迅速定位內存泄漏?
答:有。
在內存泄漏情況複雜的時候,你可以用以下方法定位內存泄漏。這是我個人認爲通用的內存泄漏追蹤方法中最有效的手段。
我們再回頭看看crtdbg生成的內存泄漏報告:
Dumping objects ->
c:/work/test.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
除了產生該內存泄漏的內存分配語句所在的文件名、行號爲,我們注意到有一個比較陌生的信息:{52}。這個整數值代表了什麼意思呢?
其實,它代表了第幾次內存分配操作。象這個例子,{52}代表了第52次內存分配操作發生了泄漏。你可能要說,我只new過一次,怎麼會是第52次?這很容易理解,其他的內存申請操作在C的初始化過程調用的唄。:)
有沒有可能,我們讓程序運行到第52次內存分配操作的時候,自動停下來,進入調試狀態?所幸,crtdbg確實提供了這樣的函數:即 long _CrtSetBreakAlloc(long nAllocID)。我們加上它:
{
_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];
}
你發現,程序運行到 int* leak = new int[10]; 一句時,自動停下來進入調試狀態。細細體會一下,你可以發現,這種方式你獲得的信息遠比在程序退出時獲得文件名及行號有價值得多。因爲報告泄漏文件名及行號,你獲得的只是靜態的信息,然而_CrtSetBreakAlloc則是把整個現場恢復,你可以通過對函數調用棧分析(我發現很多人不習慣看函數調用棧,如果你屬於這種情況,我強烈推薦你去補上這一課,因爲它太重要了)以及其他在線調試技巧,來分析產生內存泄漏的原因。通常情況下,這種分析方法可以在5分鐘內找到肇事者。
當然,_CrtSetBreakAlloc要求你的程序執行過程是可還原的(多次執行過程的內存分配順序不會發生變化)。這個假設在多數情況下成立。不過,在多線程的情況下,這一點有時難以保證。
附加說明:
對“內存管理”相關的技術感興趣?這裏可以看到我的所有關於內存管理的文章。
/****************************************************************************************************************/VC使用CRT調試功能來檢測內存泄漏
作者:JerryZ
C/C++ 編程語言的最強大功能之一便是其動態分配和釋放內存,但是中國有句古話:“最大的長處也可能成爲最大的弱點”,那麼 C/C++ 應用程序正好印證了這句話。在 C/C++ 應用程序開發過程中,動態分配的內存處理不當是最常見的問題。其中,最難捉摸也最難檢測的錯誤之一就是內存泄漏,即未能正確釋放以前分配的內存的錯誤。偶爾發生的少量內存泄漏可能不會引起我們的注意,但泄漏大量內存的程序或泄漏日益增多的程序可能會表現出各種 各樣的徵兆:從性能不良(並且逐漸降低)到內存完全耗盡。更糟的是,泄漏的程序可能會用掉太多內存,導致另外一個程序垮掉,而使用戶無從查找問題的真正根源。此外,即使無害的內存泄漏也可能殃及池魚。
幸運的是,Visual Studio 調試器和 C 運行時 (CRT) 庫爲我們提供了檢測和識別內存泄漏的有效方法。下面請和我一起分享收穫——如何使用 CRT 調試功能來檢測內存泄漏?
一、如何啓用內存泄漏檢測機制
VC++ IDE 的默認狀態是沒有啓用內存泄漏檢測機制的,也就是說即使某段代碼有內存泄漏,調試會話的 Output 窗口的 Debug 頁不會輸出有關內存泄漏信息。你必須設定兩個最基本的機關來啓用內存泄漏檢測機制。
一是使用調試堆函數:
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
注意:#include 語句的順序。如果更改此順序,所使用的函數可能無法正確工作。
通過包含 crtdbg.h 頭文件,可以將 malloc 和 free 函數映射到其“調試”版本 _malloc_dbg 和 _free_dbg,這些函數會跟蹤內存分配和釋放。此映射只在調試(Debug)版本(也就是要定義 _DEBUG)中有效。發行版本(Release)使用普通的 malloc 和 free 函數。#define 語句將 CRT 堆函數的基礎版本映射到對應的“調試”版本。該語句不是必須的,但如果沒有該語句,那麼有關內存泄漏的信息會不全。
二是在需要檢測內存泄漏的地方添加下面這條語句來輸出內存泄漏信息:
_CrtDumpMemoryLeaks();
當在調試器下運行程序時,_CrtDumpMemoryLeaks 將在 Output 窗口的 Debug 頁中顯示內存泄漏信息。比如: Detected memory leaks!
Dumping objects ->
C:/Temp/memleak/memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
c:/program files/microsoft visual studio/vc98/include/crtdbg.h(552) : {44} normal
block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
c:/program files/microsoft visual studio/vc98/include/crtdbg.h(552) : {43} normal
block at 0x00441C20, 40 bytes long.
Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
如果不使用 #define _CRTDBG_MAP_ALLOC 語句,內存泄漏的輸出是這樣的:
Detected memory leaks!
Dumping objects ->
{45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
{44} normal block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
{43} normal block at 0x00441C20, 40 bytes long.
Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
根據這段輸出信息,你無法知道在哪個源程序文件裏發生了內存泄漏。下面我們來研究一下輸出信息的格式。第一行和第二行沒有什麼可說的,從第三行開始:
xx}:花括弧內的數字是內存分配序號,本文例子中是 {45},{44},{43};
block:內存塊的類型,常用的有三種:normal(普通)、client(客戶端)或 CRT(運行時);本文例子中是:normal block;
用十六進制格式表示的內存位置,如:at 0x00441BA0 等;
以字節爲單位表示的內存塊的大小,如:32 bytes long;
前 16 字節的內容(也是用十六進制格式表示),如:Data: 41 42 等;
仔細觀察不難發現,如果定義了 _CRTDBG_MAP_ALLOC ,那麼在內存分配序號前面還會顯示在其中分配泄漏內存的文件名,以及文件名後括號中的數字表示發生泄漏的代碼行號,比如:
C:/Temp/memleak/memleak.cpp(15)
雙擊 Output 窗口中此文件名所在的輸出行,便可跳到源程序文件分配該內存的代碼行(也可以選中該行,然後按 F4,效果一樣) ,這樣一來我們就很容易定位內存泄漏是在哪裏發生的了,因此,_CRTDBG_MAP_ALLOC 的作用顯而易見。
使用 _CrtSetDbgFlag
如果程序只有一個出口,那麼調用 _CrtDumpMemoryLeaks 的位置是很容易選擇的。但是,如果程序可能會在多個地方退出該怎麼辦呢?在每一個可能的出口處調用 _CrtDumpMemoryLeaks 肯定是不可取的,那麼這時可以在程序開始處包含下面的調用:_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );這條語句無論程序在什麼地方退出都會自動調用 _CrtDumpMemoryLeaks。注意:這裏必須同時設置兩個位域標誌:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。
設置 CRT 報告模式
默認情況下,_CrtDumpMemoryLeaks 將內存泄漏信息 dump 到 Output 窗口的 Debug 頁, 如果你想將這個輸出定向到別的地方,可以使用 _CrtSetReportMode 進行重置。如果你使用某個庫,它可能將輸出定向到另一位置。此時,只要使用以下語句將輸出位置設回 Output 窗口即可:
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );
有關使用 _CrtSetReportMode 的詳細信息,請參考 MSDN 庫關於 _CrtSetReportMode 的描述。
二、解釋內存塊類型
前面已經說過,內存泄漏報告中把每一塊泄漏的內存分爲 normal(普通塊)、client(客戶端塊)和 CRT 塊。事實上,需要留心和注意的也就是 normal 和 client,即普通塊和客戶端塊。
1.normal block(普通塊):這是由你的程序分配的內存。
2.client block(客戶塊):這是一種特殊類型的內存塊,專門用於 MFC 程序中需要析構函數的對象。MFC new 操作符視具體情況既可以爲所創建的對象建立普通塊,也可以爲之建立客戶塊。
3.CRT block(CRT 塊):是由 C RunTime Library 供自己使用而分配的內存塊。由 CRT 庫自己來管理這些內存的分配與釋放,我們一般不會在內存泄漏報告中發現 CRT 內存泄漏,除非程序發生了嚴重的錯誤(例如 CRT 庫崩潰)。
除了上述的類型外,還有下面這兩種類型的內存塊,它們不會出現在內存泄漏報告中:
1.free block(空閒塊):已經被釋放(free)的內存塊。
2.Ignore block(忽略塊):這是程序員顯式聲明過不要在內存泄漏報告中出現的內存塊。
三、如何在內存分配序號處設置斷點
在內存泄漏報告中,的文件名和行號可告訴分配泄漏的內存的代碼位置,但僅僅依賴這些信息來了解完整的泄漏原因是不夠的。因爲一個程序在運行時,一段分配內存的代碼可能會被調用很多次,只要有一次調用後沒有釋放內存就會導致內存泄漏。爲了確定是哪些內存沒有被釋放,不僅要知道泄漏的內存是在哪裏分配的,還要知道泄漏產生的條件。這時內存分配序號就顯得特別有用——這個序號就是文件名和行號之後的花括弧裏的那個數字。
例如,在本文例子代碼的輸出信息中,“45”是內存分配序號,意思是泄漏的內存是你程序中分配的第四十五個內存塊:
Detected memory leaks!
Dumping objects ->
C:/Temp/memleak/memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
......
Object dump complete.
CRT 庫對程序運行期間分配的所有內存塊進行計數,包括由 CRT 庫自己分配的內存和其它庫(如 MFC)分配的內存。因此,分配序號爲 N 的對象即爲程序中分配的第 N 個對象,但不一定是代碼分配的第 N 個對象。(大多數情況下並非如此。)這樣的話,你便可以利用分配序號在分配內存的位置設置一個斷點。方法是在程序起始附近設置一個位置斷點。當程序在該點中斷時,可以從 QuickWatch(快速監視)對話框或 Watch(監視)窗口設置一個內存分配斷點:
例如,在 Watch 窗口中,在 Name 欄鍵入下面的表達式:
_crtBreakAlloc
如果要使用 CRT 庫的多線程 DLL 版本(/MD 選項),那麼必須包含上下文操作符,像這樣:
{,,msvcrtd.dll}_crtBreakAlloc
現在按下回車鍵,調試器將計算該值並把結果放入 Value 欄。如果沒有在內存分配點設置任何斷點,該值將爲 –1。
用你想要在其位置中斷的內存分配的分配序號替換 Value 欄中的值。例如輸入 45。這樣就會在分配序號爲 45 的地方中斷。
在所感興趣的內存分配處設置斷點後,可以繼續調試。這時,運行程序時一定要小心,要保證內存塊分配的順序不會改變。當程序在指定的內存分配處中斷時,可以查看 Call Stack(調用堆棧)窗口和其它調試器信息以確定分配內存時的情況。如果必要,可以從該點繼續執行程序,以查看對象發生了什麼情況,或許可以確定未正確釋放對象的原因。
儘管通常在調試器中設置內存分配斷點更方便,但如果願意,也可在代碼中設置這些斷點。爲了在代碼中設置一個內存分配斷點,可以增加這樣一行(對於第四十五個內存分配):
_crtBreakAlloc = 45;
你還可以使用有相同效果的 _CrtSetBreakAlloc 函數:
_CrtSetBreakAlloc(45);
四、如何比較內存狀態
定位內存泄漏的另一個方法就是在關鍵點獲取應用程序內存狀態的快照。CRT 庫提供了一個結構類型 _CrtMemState。你可以用它來存儲內存狀態的快照:
_CrtMemState s1, s2, s3;
若要獲取給定點的內存狀態快照,可以向 _CrtMemCheckpoint 函數傳遞一個 _CrtMemState 結構。該函數用當前內存狀態的快照填充此結構:
_CrtMemCheckpoint( &s1 );
通過向 _CrtMemDumpStatistics 函數傳遞 _CrtMemState 結構,可以在任意地方 dump 該結構的內容:
_CrtMemDumpStatistics( &s1 );
該函數輸出如下格式的 dump 內存分配信息:
0 bytes in 0 Free Blocks.
75 bytes in 3 Normal Blocks.
5037 bytes in 41 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 5308 bytes.
Total allocations: 7559 bytes.
若要確定某段代碼中是否發生了內存泄漏,可以通過獲取該段代碼之前和之後的內存狀態快照,然後使用 _CrtMemDifference 比較這兩個狀態:
_CrtMemCheckpoint( &s1 );// 獲取第一個內存狀態快照
// 在這裏進行內存分配
_CrtMemCheckpoint( &s2 );// 獲取第二個內存狀態快照
// 比較兩個內存快照的差異
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );// dump 差異結果
顧名思義,_CrtMemDifference 比較兩個內存狀態(前兩個參數),生成這兩個狀態之間差異的結果(第三個參數)。在程序的開始和結尾放置 _CrtMemCheckpoint 調用,並使用 _CrtMemDifference 比較結果,是檢查內存泄漏的另一種方法。如果檢測到泄漏,則可以使用 _CrtMemCheckpoint 調用通過二進制搜索技術來分割程序和定位泄漏。
五、結論
儘管 VC ++ 具有一套專門調試 MFC 應用程序的機制,但本文上述討論的內存分配很簡單,沒有涉及到 MFC 對象,所以這些內容同樣也適用於 MFC 程序。在 MSDN 庫中可以找到很多有關 VC++ 調試方面的資料,如果你能善用 MSDN 庫,相信用不了多少時間你就有可能成爲調試高手。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1551985