具有動態分配和釋放記憶體的能力是C/C++程式語言的重要特色之一。VisualC++ debugger和CRT庫提供了一系列有效檢測和評鑑記憶體流失的工具。
設定記憶體流失檢測
檢測記憶體流失的基本工具是調試器和CRT調試堆函數。為了使用調試堆函數,在你的程式中你必須含有下面的說明:
#define _CRTDBG_MAP_ALLOC #include<stdlib.h> #include<crtdbg.h> |
必須保證上面聲明的順序,如果改變了順序,可能不能正常工作。<crtdbg.h>的_malloc_dbg和_free_dbg將取代標準的malloc和free函數出現在DEBUG版中,它可以跟蹤記憶體的分配和釋放。但是這隻會在DEBUG版本中發生(當#define _DEBUG的時候),而Release版本仍使用標準的malloc和free功能。
#define _CRTDBG_MAP_ALLOC表示使用CRT堆函數的相應的DEBUG版本。這個定義不是必須的,但是沒有它,記憶體流失報告含有的只是沒有什麼用處的資訊。
一旦你已經添加了剛才的聲明,你就能夠通過在程式中加入下面的代碼來報告記憶體流失資訊:
當在DEBUG模式下運行程式時,在Output視窗的Debug標籤處_CrtDumpMemoryLeaks會顯示記憶體流失的資訊,例如:
Detected memory leaks! Dumping objects -> C:/PROGRAM FILES/VISUAL STUDIO/MyProjects/leaktest/leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long. Data:< > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. |
如果沒有#define _CRTDBG_MAP_ALLOC,記憶體流失報告就會像下面這樣:
Detected memory leaks! Dumping objects -> {18} normal block at 0x00780E80, 64 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. |
由此可見,定義_CRTDBG_MAP_ALLOC時,_CrtDumpMemoryLeaks可以提供更多的有用資訊。
如果沒有定義_CRTDBG_MAP_ALLOC,那麼記憶體流失報告如下顯示:
記憶體配置數值(花括弧內)
模組的類型(normal、client或者CRT)
以十六進位格式定位的記憶體
以位元組計模組的大小
第一個十六位元組的內容(也可以用十六進位)
如果定義了_CRTDBG_MAP_ALLOC,報告的內容還包括出現分配所泄漏記憶體的檔案。在檔案名稱之後括弧內的數字是檔案內的行數值。
此時雙擊包含行數值和檔案名稱的輸出行, 或者選擇輸出行並按F4:
C:/PROGRAM FILES/VISUAL STUDIO/MyProjects/leaktest/leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
編輯視窗將會跳到檔案中分配所泄漏記憶體的那一行代碼,leaktest.cpp中的行號為20的那一行。
使用_CrtSetDbgFlag
如果你的程式只在一個地方退出,那麼在選擇調用_CrtDumpMemoryLeaks的位置是非常容易的。但是,如果你的程式可能會在程式多處位置退出該怎麼辦?如果不希望在每一個可能的出口處調用_CrtDumpMemoryLeaks,那麼你可以在你的程式開始處包含下面的調用:
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
當程式退出時,將會自動地調用_CrtDumpMemoryLeaks(必須設定_CRTDBG_ALLOC_MEM_DF和 _CRTDBG_LEAK_CHECK_DF)。
翻譯記憶體模組的類型
記憶體流失報告中把每一塊泄漏的記憶體分為普通塊、客戶塊和CRT塊。事實上,你只需要留心普通塊和客戶塊類型。
普通塊(normal block):是由你的程式分配的記憶體。
客戶塊(client block):是一種特殊的記憶體塊,它是由MFC使用的一個對象,程式退出時,該對象的解構函式沒有被調用。MFC new操作符可以用來建立普通塊和客戶塊。
CRT塊(CRT block):是由C RunTime Library供自己使用而分配的記憶體塊。CRT庫自己來管理這些記憶體的分配與釋放,通常你不會在記憶體流失報告中發現有CRT記憶體流失,除非程式發生了嚴重的錯誤(例如CRT庫崩潰)。
下面這兩種類型的記憶體塊不會出現在記憶體流失報告中:
空閑塊(free block):已經被釋放(free)的記憶體塊。
忽略塊(Ignore block):是程式員顯式聲明過不要在記憶體流失報告中出現的記憶體塊。
設定CRT報告樣式
通常_CrtDumpMemoryLeaks()會dump記憶體流失的資訊到output視窗的Debug欄位。你可以使用_CrtSetReportMode()來重新設定輸出到另一個位置。關於更詳細的如何使用_CrtSetReportMode()說明,請查看MSDN。
在記憶體配置數目處設定一個斷點
在記憶體流失報告中的檔案名稱和行號可告訴分配泄漏的記憶體的代碼位置,但是光是有這些資訊對於完整瞭解泄漏原因,有時候還是不夠的。因為一個程式在運行時,一段分配分配記憶體的代碼將會被調用很多次,但可能是某次調用後沒有釋放記憶體而導致了泄漏記憶體。為了確定是那些記憶體沒有被釋放,你必須不僅要知道泄漏的記憶體在那裡被分配,還要知道泄漏產生的條件。對你來說,有協助的資訊就是記憶體配置號——在檔案名稱和行號之後的花括弧對中出現的數值。
例如,在下面的輸出資訊中,"18"是記憶體配置號,意思是泄漏的記憶體是你程式中分配的第十八個記憶體塊:
Detected memory leaks! Dumping objects -> C:/PROGRAM FILES/VISUAL STUDIO/MyProjects/leaktest/leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. |
CRT庫為在程式運行期間分配的所有記憶體模組計數,包括CRT自己分配的記憶體或者諸如MFC等分配的其它模組。因此帶有分配號n的一個對象是在你的程式中分配的第n個對象,但不代表是由那段代碼分配的第n個對象(在大部分情況下,它都不會是。)
這樣的話,你可以利用分配號在記憶體配置的地方設定一個斷點。為了設定這樣一個端點,你可以在你的程式開始處,設定一個位置斷點。當你的程式在那一點break時,你就能夠從QuickWatch對話方塊或者Watch視窗設定這樣一個位置斷點。
例如,在Watch視窗中,在Name欄鍵入下面的運算式:
如果你正在用CRT庫dynamic-link library (DLL)的多線程版本,你必須含有上下文操作符,像這樣:
{,,msvcrtd.dll}_crtBreakAlloc |
現在按下RETURN鍵,調試器找到該值並把結果放置在value欄。如果你在記憶體配置過程中還沒有設定任何記憶體配置斷點,那麼這個值是-1。用你想要去中斷的記憶體配置的分配數值來,取代value表中的值——例如,18。
當設定完記憶體配置斷點之後,繼續調試。這時,運行程式時一定要小心,要保證記憶體塊分配的順序不會改變。當你的程式在記憶體配置點中斷的時候,你就能夠查看Call Stack視窗和其他的DEBUG資訊來分析泄漏原因了。你仍然可以繼續從那一點執行程式,以至於瞭解到底發生了什麼,同時確定為什麼記憶體沒有被釋放(設定一個記憶體配置斷點是很有協助的)。
雖然在調試器中設定記憶體配置斷點通常更容易,但是如果你喜歡的話,你也可以在你的代碼中設定它們。為了在你的代碼中設定一個記憶體配置斷點,可以增加這樣一行(對於第十八個記憶體配置):
你還有可以使用有相同效果的_CrtSetBreakAlloc函數:
比較記憶體狀態
定位記憶體流失的另一個方法就是在關鍵點對應用程式的記憶體狀態做快照。CRT庫提供了一個結構類型_CrtMemState。你可以用它來儲存記憶體狀態的一個快照:
為了在特定點對記憶體狀態進行快照,可以傳遞一個_CrtMemState結構到he _CrtMemCheckpoint函數。此函數用當時記憶體狀態的一個快照來填充此結構:
_CrtMemCheckpoint( &s1 ); |
你可以通過傳遞此結構到_CrtMemDumpStatistics函數來dump _CrtMemState結構的任意點的內容:
_CrtMemDumpStatistics( &s1 ); |
此函數列印出類似於下面這樣的一堆記憶體配置資訊:
0 bytes in 0 Free Blocks. 0 bytes in 0 Normal Blocks. 3071 bytes in 16 CRT Blocks. 0 bytes in 0 Ignore Blocks. 0 bytes in 0 Client Blocks. Largest number used: 3071 bytes. Total allocations: 3764 bytes. |
為了確定一個記憶體流失是否在一節代碼中出現,你可以在此節前和此節後對記憶體狀態作快照,然後用_CrtMemDifference比較兩種狀態:
_CrtMemCheckpoint( &s1 ); // memory allocations take place here …… _CrtMemCheckpoint( &s2 ); if ( _CrtMemDifference( &s3, &s1, &s2) ) _CrtMemDumpStatistics( &s3 ); |
從名字上可以知道,_CrtMemDifference用來比較兩個記憶體狀態(最後面的那兩個參數),並返回狀態差異的結果(第一個參數)。在你的函數開始和結尾處的_CrtMemCheckpoint調用和使有_CrtMemDifference來比較結果為檢測記憶體流失提供了另一種方法。如果檢測到一個泄漏,那麼可以使用_CrtMemCheckpoint調用來分割你的程式,並使用binary search technique來定位泄漏。