記憶體流失是 C/C++ 應用程式中最微妙且難以偵測的錯誤之一。 記憶體流失的原因是無法正確解除分配先前配置的記憶體。 一開始可能不會注意到小型記憶體流失,但隨著時間推移,可能會造成從效能不佳到應用程式記憶體不足時損毀等徵兆。 使用所有可用記憶體的流失應用程式可能會導致其他應用程式當機,造成對哪個應用程式負責的混淆。 即使是無害的記憶體流失也可能表示應該更正的其他問題。
Visual Studio 調試程式和 C 運行時間連結庫 (CRT) 可協助您偵測和識別記憶體流失。
啟用記憶體流失偵測
偵測記憶體流失的主要工具是 C/C++ 調試程式和 CRT 偵錯堆積函式。
若要啟用所有偵錯堆積函式,請依下列順序在C++程式中包含下列語句:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#define 陳述式會將 CRT 堆積函式的基底版本對應到偵錯版本。 如果您省略 #define 語句,記憶體流失傾印將會 較不詳細。
包括 crtdbg.h 會將 malloc 和 free 函式對應至其偵錯版本, _malloc_dbg 以及 _free_dbg,其會追蹤記憶體配置和解除分配。 這種對應只會發生在偵錯組建 (具有 _DEBUG) 中。 發行的組建使用一般的 malloc 和 free 函式。
使用上述語句啟用偵錯堆積函式之後,請在應用程式結束點之前呼叫 _CrtDumpMemoryLeaks ,以在應用程式結束時顯示記憶體流失報告。
_CrtDumpMemoryLeaks();
如果您的 app 有數個結束,則不需要手動在每個結束點放置 _CrtDumpMemoryLeaks 。 若要在每一個結束點自動呼叫 _CrtDumpMemoryLeaks ,請在應用程式的開頭加上如下所示的位欄位來呼叫 _CrtSetDbgFlag :
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
根據預設, _CrtDumpMemoryLeaks 會將記憶體流失報告輸出至 [輸出] 視窗的 [偵錯] 窗格。 如果您使用程式庫,該程式庫可能會重設輸出至其他位置。
您可以使用 _CrtSetReportMode 將報表重新導向至其他位置,或回到 [輸出 ] 視窗,如下所示:
_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_DEBUG );
下列範例顯示簡單的記憶體流失,並使用 顯示記憶體流失資訊 _CrtDumpMemoryLeaks();。
// debug_malloc.cpp
// compile by using: cl /EHsc /W4 /D_DEBUG /MDd debug_malloc.cpp
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>
int main()
{
std::cout << "Hello World!\n";
int* x = (int*)malloc(sizeof(int));
*x = 7;
printf("%d\n", *x);
x = (int*)calloc(3, sizeof(int));
x[0] = 7;
x[1] = 77;
x[2] = 777;
printf("%d %d %d\n", x[0], x[1], x[2]);
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG);
_CrtDumpMemoryLeaks();
}
解譯記憶體流失報告
如果您的應用程式未定義 _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:\users\username\documents\projects\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連結庫發生嚴重問題,否則CRT區塊不會出現在記憶體流失報告中。
另外還有兩種絕對不會出現在記憶體流失報告中的記憶體區塊。 可用區塊是已釋放的記憶體,因此依定義不會外洩。 忽略區塊是您明確標示要從記憶體流失報告中排除的記憶體。
上述技術會識別使用標準CRT malloc 函式配置之內存的記憶體流失。 不過,如果您的程式使用 C++ new 運算子設定記憶體,您可能只會看到記憶體流失報告中呼叫operator new的檔名和行號_malloc_dbg。 若要建立更實用的記憶體流失報告,您可以撰寫類似下列的巨集來報告進行配置的程式碼行:
#ifdef _DEBUG
#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
// Replace _NORMAL_BLOCK with _CLIENT_BLOCK if you want the
// allocations to be of _CLIENT_BLOCK type
#else
#define DBG_NEW new
#endif
現在您可以使用程式代碼中的 巨集來取代 new 運算子 DBG_NEW 。 在偵錯組建中, DBG_NEW 會使用全域 operator new 多載,針對區塊類型、檔案和行號採用額外的參數。 要記錄額外資訊的呼叫new多載_malloc_dbg。 記憶體流失報告會顯示配置流失物件的檔名和行號。 發行組建仍然使用預設 new。 以下是技術的範例:
// debug_new.cpp
// compile by using: cl /EHsc /W4 /D_DEBUG /MDd debug_new.cpp
#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>
#ifdef _DEBUG
#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
// Replace _NORMAL_BLOCK with _CLIENT_BLOCK if you want the
// allocations to be of _CLIENT_BLOCK type
#else
#define DBG_NEW new
#endif
struct Pod {
int x;
};
int main() {
Pod* pPod = DBG_NEW Pod;
pPod = DBG_NEW Pod; // Oops, leaked the original pPod!
delete pPod;
_CrtDumpMemoryLeaks();
}
當您在 Visual Studio 調試程式中執行此程式代碼時,呼叫 _CrtDumpMemoryLeaks 會在 [輸出 ] 視窗中產生類似如下的報表:
Detected memory leaks!
Dumping objects ->
c:\users\username\documents\projects\debug_new\debug_new.cpp(20) : {75}
normal block at 0x0098B8C8, 4 bytes long.
Data: < > CD CD CD CD
Object dump complete.
此輸出會報告洩漏的配置是在debug_new.cpp的第 20 行。
注意
不建議您建立名為 new、 或任何其他語言關鍵詞的預處理器巨集。
在記憶體配置編號上設定斷點
記憶體配置編號會指出流失的記憶體區塊是何時配置的。 例如,記憶體配置編號為18的區塊是應用程式執行期間配置的第18個記憶體區塊。 CRT 報表會計算執行期間的所有記憶體區塊配置,包括CRT連結庫和其他連結庫的配置,例如MFC。 因此,記憶體配置區塊編號 18 可能不是程式代碼所配置的第 18 個記憶體區塊。
您可以使用配置編號在記憶體配置上設定中斷點。
若要使用 [監看式] 視窗設定記憶體配置中斷點
在應用程式的開頭附近設定斷點,並開始偵錯。
當應用程式在斷點暫停時,選取] 來開啟 [>式] 視窗(或 [監看式 2]、>] 或 [監看式 4]。
在 [ 監看 式] 視窗中,輸入
_crtBreakAlloc[ 名稱] 資料行。如果您使用 CRT 連結庫的多線程 DLL 版本 (/MD 選項),請新增內容運算子:
{,,ucrtbased.dll}_crtBreakAlloc請確定已載入偵錯符號。 否則,
_crtBreakAlloc會回報為 不明。按 Enter 鍵。
偵錯工具會評估呼叫,並將結果放在 [值] 欄中。 如果您尚未在記憶體配置上設定任何斷點,此值為 -1 。
在 [ 值] 資料行中,將值取代為您想要調試程式中斷的記憶體配置配置編號。
在記憶體配置編號上設定斷點之後,請繼續偵錯。 請務必在相同的條件下執行,因此記憶體配置號碼不會變更。 當您的程式在指定的記憶體配置中斷時,請使用 [呼叫堆棧 ] 視窗和其他調試程式視窗來判斷記憶體配置的條件。 然後,您可以繼續執行來觀察物件會發生什麼情況,並判斷其未正確解除分配的原因。
在物件上設定資料中斷點可能也很有用。 如需詳細資訊,請參閱使用中斷點。
您也可以在程式碼中設定記憶體配置中斷點。 您可以設定:
_crtBreakAlloc = 18;
or:
_CrtSetBreakAlloc(18);
比較記憶體狀態
另一種尋找記憶體流失的技術,是使用在關鍵點的應用程式記憶體狀態之快照。 若要在應用程式中指定點建立記憶體狀態的快照集,請建立 _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) 傳回 與之間的差異s1s2結果。
尋找記憶體流失的其中一個技巧,一開始是在應用程式的開頭和結尾放置 _CrtMemCheckpoint 呼叫,然後使用 _CrtMemDifference 來比較結果。 如果 _CrtMemDifference 顯示記憶體流失,您可以新增更多 _CrtMemCheckpoint 呼叫,以使用二進位搜尋來分割程式,直到您隔離洩漏的來源為止。
誤判
_CrtDumpMemoryLeaks 如果連結庫將內部配置標示為一般區塊,而不是CRT區塊或用戶端區塊,可能會提供記憶體流失的錯誤指示。 在這種情況下, _CrtDumpMemoryLeaks 無法區分使用者配置和內部程式庫配置。 如果在您呼叫 _CrtDumpMemoryLeaks之後執行程式庫配置的全域解構函式,則每個內部程式庫配置都會報告為記憶體流失。 比 Visual Studio .NET 更早的標準範本連結庫版本可能會導致 _CrtDumpMemoryLeaks 回報這類誤判。