使用 CRT 程式庫尋找記憶體遺漏
記憶體流失 (定義是無法正確解除配置先前配置的記憶體) 是 C/C++ 應用程式中最不易察覺和難以偵測的錯誤之一。一開始可能只有少許記憶體流失而未察覺,但隨著時間經過,累積的記憶體流失可能愈來愈多而造成一些症狀,包括應用程式效能降低,甚至是因記憶體不足而沒有反應。情況更嚴重者,發生記憶體流失的應用程式會因為耗盡所有可用的記憶體而造成其他應用程式也沒有反應,以致於難以判斷到底哪一個應用程式才是禍首。即使看似無害的記憶體流失也可能是其他問題的根源,而應該解決。
Visual Studio 偵錯工具和 C 執行階段 (CRT) 程式庫提供一種偵測和辨識記憶體流失的方法。
啟用記憶體流失偵測
偵測記憶體流失的主要工具是偵錯工具和 CRT 偵錯堆積函式。
若要啟用偵錯堆積函式,在您的程式裡包含下列陳述式:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
為了讓 CRT 功能正常運作,#include 陳述式必須遵循此處的順序。
加入 crtdbg.h 會將 malloc 和 free 函式對應至其偵錯版本 (_malloc_dbg 和 free),這些偵錯版本的函式會追蹤記憶體配置和解除配置。這種對應只會發生在偵錯組建 (具有 _DEBUG) 中。發行的組建使用一般的 malloc 和 free 函式。
#define 陳述式會將 CRT 堆積函式的基底版本對應到偵錯版本。如果省略 #define 陳述式,記憶體流失傾印就不會提供那麼詳細的資料。
在使用上述陳述式來啟用偵錯堆積函式之後,您可以在應用程式結束點之前加上 _CrtDumpMemoryLeaks 呼叫,以便在應用程式結束時顯示記憶體流失報告:
_CrtDumpMemoryLeaks();
如果您的應用程式有多個結束點,您不需要在每個結束點手動加上 _CrtDumpMemoryLeaks 呼叫。只要在應用程式開頭加上 _CrtSetDbgFlag 呼叫,就會自動在每個結束點呼叫 _CrtDumpMemoryLeaks。您必須設定兩個位元欄位,如下所示:
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
根據預設,_CrtDumpMemoryLeaks 會將記憶體流失報告輸出至 [輸出] 視窗的 [偵錯] 窗格。您可以使用 _CrtSetReportMode 將報告重新導向至其他位置。
如果您使用程式庫,該程式庫可能會重設輸出至其他位置。在這種情況下,您可以將輸出位置設回 [輸出] 視窗,如下所示:
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );
解讀記憶體流失報告
如果您的應用程式未定義 _CRTDBG_MAP_ALLOC,_CrtDumpMemoryLeaks 會顯示類似如下的記憶體流失報告:
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,則記憶體流失報告會類似如下:
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.
差別在於第二份報告會顯示首次配置流失的記憶體的檔案名稱和其內行號。
無論您是否定義 _CRTDBG_MAP_ALLOC,記憶體流失報告都會顯示下列資訊:
記憶體配置號碼,在此範例中為 18
區塊類型,在此範例中為 normal。
十六進位記憶體位置,在此範例中為 0x00780E80。
區塊的大小,在此範例中為 64 bytes。
區塊中前 16 個位元組的資料 (十六進位格式)。
記憶體流失報告會指出記憶體區塊是一般區塊、用戶端區塊還是 CRT 區塊。「一般區塊」(Normal Block) 是您的程式所配置的一般記憶體。「用戶端區塊」(Client Block) 是 MFC 程式專為需要解構函式的物件所使用的一種特殊類型的記憶體區塊。MFC 的 new 運算子會根據所建立的物件來建立一般區塊或用戶端區塊。「CRT 區塊」(CRT Block) 則是 CRT 程式庫配置來供己身使用的記憶體區塊。CRT 程式庫負責處理這些區塊的解除配置。因此,除非發生嚴重錯誤 (例如 CRT 程式庫損毀),否則您通常在記憶體流失報告中不會看到這些資訊。
另外還有兩種絕對不會出現在記憶體流失報告中的記憶體區塊。「可用區塊」(Free Block) 是已經釋放的記憶體。按照定義,這種記憶體本身就沒有流失的問題。「忽略區塊」(Ignore Block) 是您已明確標記為要從記憶體流失報告中排除的區塊。
上述方式適用於以標準 CRT malloc 函式配置的記憶體。不過,如果您的程式是以 C++ new 運算子配置記憶體,而您想要在記憶體流失報告中看見檔案和行號,您需要重新定義 new。這個動作可以透過類似如下的程式碼區塊達成:
#ifdef _DEBUG
#ifndef DBG_NEW
#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
#define new DBG_NEW
#endif
#endif // _DEBUG
在記憶體配置編號上設定中斷點
記憶體配置編號會指出流失的記憶體區塊是何時配置的。例如,區塊的記憶體配置編號為 18,代表這是在應用程式執行期間配置的第 18 個記憶體區塊。CRT 報告會算進所有在執行期間配置的記憶體區塊。這包括由 CRT 程式庫和其他程式庫 (例如 MFC) 配置的記憶體區塊。因此,記憶體配置編號為 18 的區塊,不見得是您的程式碼所配置的第 18 個記憶體區塊。通常情況下並不是。
您可以使用配置編號在記憶體配置上設定中斷點。
若要使用監看式視窗設定記憶體配置中斷點
在靠近應用程式的開頭設定中斷點,然後啟動應用程式。
當應用程式在中斷點中斷時,[監看式] 視窗隨即出現。
在 [監看式] 視窗的 [_crtBreakAlloc名稱] 欄中輸入 。
如果您使用 CRT 程式庫 (/MD 選項) 的多執行緒 DLL 版本,請包含內容運算子:{,,msvcr120d.dll}_crtBreakAlloc
請按 RETURN。
偵錯工具會評估呼叫,並將結果放在 [值] 欄中。如果您尚未在記憶體配置上設定任何中斷點,此值將會是 –1。
在 [值] 欄中,將顯示的值取代為您要在該處中斷的記憶體配置的配置編號。
在記憶體配置編號上設定中斷點之後,您可以繼續偵錯。請務必注意要在上次執行的相同條件下執行程式,這樣記憶體配置順序才不會變更。當程式在特定的記憶體配置中斷時,您可以使用 [呼叫堆疊] 視窗和其他偵錯工具視窗,判斷記憶體配置所處的條件。然後再恢復程式的執行,觀察物件有何變化,並判斷未將它適當解除配置的原因。
在物件上設定資料中斷點可能也很有用。如需詳細資訊,請參閱Set a data change breakpoint (native C++ only)。
您也可以在程式碼中設定記憶體配置中斷點。執行這項作業的方法有兩種:
_crtBreakAlloc = 18;
或:
_CrtSetBreakAlloc(18);
比較記憶體狀態
另一種尋找記憶體流失的技術,是使用在關鍵點的應用程式記憶體狀態之快照 (Snapshot)。若要建立應用程式中特定時點的記憶體狀態快照,請建立 _CrtMemState 結構,並將它傳遞給 _CrtMemCheckpoint 函式。這個函式以目前記憶體狀態的快照填寫結構:
_CrtMemState s1;
_CrtMemCheckpoint( &s1 );
_CrtMemCheckpoint 會以目前記憶體狀態的快照填入該結構。
若要輸出 _CrtMemState 結構的內容,請將結構傳遞給 _ CrtMemDumpStatistics 函式:
_CrtMemDumpStatistics( &s1 );
_ CrtMemDumpStatistics 會輸出類似如下的記憶體狀態傾印:
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 會比較記憶體狀態 s1 和 s2,然後在 s3 傳回 s1 和 s2 的差異。
尋找記憶體流失的一個方式是先在應用程式的開頭和結尾加上 _CrtMemCheckpoint 呼叫,然後使用 _CrtMemDifference 來比較結果。如果 _CrtMemDifference 顯示有記憶體流失,您再加上更多 _CrtMemCheckpoint 呼叫以使用二進位搜尋將程式分割,直到找出遺漏的來源為止。
誤報
在某些情況下,_CrtDumpMemoryLeaks 會提供錯誤的記憶體流失指示。如果您使用的程式庫將內部配置標記為 _NORMAL_BLOCKs,而非 _CRT_BLOCK 或 _CLIENT_BLOCK,則可能會發生這種情形。在這種情況下,_CrtDumpMemoryLeaks 無法區分使用者配置和內部程式庫配置。如果在您呼叫 _CrtDumpMemoryLeaks 之後執行程式庫配置的全域解構函式,則每個內部程式庫配置都會報告為記憶體流失。在 Visual Studio .NET 之前的舊版標準樣板程式庫會造成 _CrtDumpMemoryLeaks 提出這樣的誤報,但最新版本中已修正這個問題。