MFC 檢測記憶體洩漏的方法

十日十乞001發表於2017-06-10

C/C++的一個重要特色是可以動態的分配和釋放記憶體,我們的口號是new和delete成對出現。

可是,總會由於各種各樣的原因,有記憶體洩露的情況發生,藉助visual C++ 整合開發環境,可以方便的檢查記憶體洩露資訊,具體方法如下:

1、在stdafx.h標頭檔案中新增兩行程式碼
//記憶體洩露檢測
#define _CRTDBG_MAP_ALLO
#include <crtdbg.h>

2、在程式退出的地方新增函式,

_CrtDumpMemoryLeaks();

執行程式,如果有記憶體洩露則可以在除錯輸出視窗看到如下資訊

  1. Detected memory leaks!  
  2. Dumping objects ->  
  3. tooltip.cpp(394) : {387} client block at 0x00387FE0, subtype c0, 112 bytes long.  
  4. a CToolTipCtrl object at $00387FE0, 112 bytes long  
  5. g:\c++\vc_prac\mfc_prac\pos_system\pos_systemview.cpp(174) : {312} normal block at 0x00387F40, 100 bytes long.  
  6.  Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD   

能看到洩露的記憶體分配時候的檔案、程式碼行,等資訊

===========================================================================

  介紹:
  動態分配、回收記憶體是C/C++程式語言一個最強的特點,但是中國哲學家孫(Sun Tzu,我不知道是誰?那位知道?) 指出,最強的同時也是最弱的。這句話對C/C++應用來說非常正確,在記憶體處理出錯的地方通常就是BUGS產生的地方。一個最敏感和難檢測的BUG就是記憶體洩漏-沒有把前邊分配的記憶體成功釋放,一個小的記憶體洩漏可能不需要太注意,但是程式洩漏大塊記憶體,或者漸增式的洩漏記憶體可能引起的現象是:先是效能低下,再就是引起復雜的記憶體耗盡錯誤。最壞的是,一個記憶體洩漏程式可能用完了如此多的記憶體以至於引起其他的程式出錯,留給使用者的是不能知道錯誤到底來自哪裡。另外,一個看上去無害的記憶體洩漏可能是另一個問題的先兆。幸運的是VC++DEBUGER和CRT庫提供了一組有效的檢測和定位記憶體洩漏的工具。本文描述如何使用這些工具有效和系統的排除記憶體洩漏。 
  
  啟動記憶體洩漏檢測:
  主要的檢測工具是DEBUGER和CRT堆除錯函式。要使除錯函式生效,必須要在你的程式中包含以下幾個語句:
  
  #define _CRTDBG_MAP_ALLOC
  #include "stdlib.h"
  #include "crtdbg.h"

  
  並且這些#include 語句必須按上邊給出的順序使用。如果你改變了順序,可能導致使用的函式工作不正常。包含crtdbg.h的作用是用malloc和free函式的debug版本(_malloc_dbg 和 _free_dbg)來替換他們,他們能跟蹤記憶體分配和回收。這個替換僅僅是在debug狀態下生效,Relese版本中還是使用普通的malloc和free函式。
  上面的#define語句使用crt堆函式相應的debug版本來替換正常的堆函式。這個語句不是必需的,但是沒有他,你可能會失去一些有用的記憶體洩漏資訊。
  
  你一旦在你的程式中增加了以上的語句,你可以通過在程式中增加_CrtDumpMemoryLeaks();函式來輸出記憶體洩漏資訊。
  
  當你在debuger下執行你的程式時,_CrtDumpMemoryLeaks 顯示記憶體洩漏資訊在OutPut視窗的Debug標籤項裡。記憶體洩漏資訊舉例如下:
  
  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 的情況下,顯示資訊包含:
  1.記憶體分配的編號(大括弧中的數字);
  2.記憶體快的型別(普通型、客戶端型、CRT型);
  3.16進製表示的記憶體位置;
  4.記憶體快的大小;
  5.前16bytes的內容。
  
  如果定義了_CRTDBG_MAP_ALLOC ,輸出資訊還包含當前洩漏記憶體是在那個檔案中被分配的定位資訊。檔名後圓括弧中的數字是行數。如果你雙擊這行資訊,
  
  C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} 
  normal block at 0x00780E80, 64 bytes long.
  
  游標就會跳轉到原檔案中分配這個記憶體的行前。選擇Output中的題是行,按F4能達到同樣的效果。
  
  使用Using _CrtSetDbgFlag:
  如果你的程式的退出點只有一個的話,呼叫_CrtDumpMemoryLeaks將是非常容易。但是,如果你的程式有多個退出點話會是什麼樣一個情況?如果不想在每個退出點都呼叫_CrtDumpMemoryLeaks,你可以在程式的開始包含以下呼叫:
  
  _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
  這個語句會在你的程式結束時自動呼叫_CrtDumpMemoryLeaks,但是你必須象前邊提到的那樣設定_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF這兩個標誌位。
  
  介紹一下記憶體塊的型別:
  就象前面指出的,一個記憶體洩漏資訊指出每個記憶體洩漏塊的型別為普通、客戶端或者CRT型。在實際程式中,普通型和客戶端型式最常見的型別。
  
  普通型記憶體塊是你的程式平常分配的記憶體型別。
  
  客戶端型記憶體塊是MFC程式給需要析構的物件分配的記憶體塊。MFC的new操作可以選擇普通型或客戶端型中合適的一種作為將要被建立的物件的記憶體塊型別。
  
  CRT記憶體塊是CRT庫為自己使用而分配的記憶體塊。CRT在處理自己的釋放記憶體操作時使用這些塊,所以在記憶體洩漏報告中這種型別並不常見,除非發生嚴重異常(例如:CRT庫出錯)。
  
  還有兩種型別你在記憶體洩漏資訊中看不到:
  
  自由塊,它是已經被釋放的記憶體塊;
  忽略塊,它是已經被特殊標示的記憶體塊。
  
  設定CRT報告的格式:
  在預設情況下,_CrtDumpMemoryLeaks輸出的記憶體洩漏資訊就象前邊描述的那樣。你可以使用_CrtSetReportMode讓這些輸出資訊輸出到其他地方。如果你使用一個庫,它可能要使輸出資訊到其他的地方,在這種情況下,你可以使用_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );語句使輸出資訊重新定位到Output視窗。
  
  根據記憶體分配編號設定斷點:
  記憶體洩漏報告中的檔名和行數告訴你記憶體洩漏的位置,但是知道記憶體洩漏位置不是總是能找到問題所在。在一個執行的程式中一個記憶體分配操作可能被呼叫多次,但是記憶體洩漏可能只發生在其中的某次操作中。為了確認問題所在,你除了知道洩漏的位置之外,你還必須要知道發生洩漏的條件。記憶體分配編號使得解決這個問題成為可能。這個數字就在檔名、行數之後的大括弧內。例如,在上面的輸出中“18”就是記憶體分配編號,它的意思是你程式中的記憶體洩漏發生在第18次分配操作中。
  
  CRT庫對正在執行程式中所有的記憶體塊分配進行計數,包括自身的記憶體分配,或者其他庫(象MFC)。一個物件的分配編號是n表示第n個物件被分配,但是它可能並不表示第N個物件通過程式碼被分配(在大多數情況下它們並不相同)。
  
  你可以根據記憶體分配編號在記憶體被分配的位置設定斷點。先在程式開始部分附近設定一個斷點,當你的程式在斷點處停止後,你可以通過QuickWatch對話方塊或者Watch視窗來設定記憶體分配斷點。在Watch視窗中的Name列中輸入_crtBreakAlloc,如果你使用的是多執行緒DLL版本的CRT庫的話你必須包含上下文轉換 {,,msvcrtd.dll}_crtBreakAlloc。完成後按回車,debugger處理這次呼叫,並且把返回值顯示在value列中。如果你沒有設定記憶體分配斷點的話返回值是-1。在value列中輸入你想設定的分配數,例如18。
  
  你在自己感興趣的記憶體分配位置設定斷點後,你可以繼續debugging。細心的執行你的程式在相同的條件下,這樣才能保證記憶體分配的順序不致發生變化。當程式在特定的記憶體分配處停下來後, 你可以檢視Call 視窗和其他的debugger資訊來分析此次記憶體分配的條件。如果有必要你可以繼續執行程式,看一看這個物件有什麼變化,或許可以得知為什麼記憶體沒有被正確的釋放。
  
  儘管這個操作非常容易,但是如果你高興的話也可以在程式碼中設定斷點。在程式碼中增加一行程式碼_crtBreakAlloc = 18;另外也可以通過_CrtSetBreakAlloc(18)來完成設定。
  
  比較記憶體狀態
  另一個定位記憶體洩漏的方法是在重要位置捕捉應用程式的“記憶體快照”。CRT庫提供了一個結構體型別 _CrtMemState,使用它你可以儲存記憶體狀態的快照(當前狀態)。
  
  _CrtMemState s1, s2, s3;
  
  為了得到一個快照,可以把一個_CrtMemState 結構體傳給_CrtMemCheckpoint 函式,這個函式可以把當前的記憶體狀態填充在結構體中:
  
  _CrtMemCheckpoint( &s1 );
  
  你可以通過把結構體_CrtMemState 傳給_CrtMemDumpStatistics函式來輸出結構體中的內容。
  _CrtMemDumpStatistics( &s3 );( &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把程式分成兩半分別使用上述方法來檢測記憶體洩漏,這樣就是使用二分法來檢查記憶體洩漏。

////////////////////

今天除錯程式,發現有記憶體洩漏但是沒有提示具體是哪一行,搞得我很頭疼。結果在網上搜尋了一些資料,經自己實踐後整理如下:

    第一種:通過"OutPut視窗"定位引發記憶體洩漏的程式碼(下面轉,我寫的沒原文好,也懶得寫)。

我們知道,MFC程式如果檢測到存在記憶體洩漏,退出程式的時候會在除錯視窗提醒記憶體洩漏。例如:
class CMyApp : public CWinApp
{
public:
   BOOL InitApplication()
   {
       int* leak = new int[10];
       return TRUE;
   }
};
產生的記憶體洩漏報告大體如下:
Detected memory leaks!
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++程式設計師,都可以很方便地檢測記憶體洩漏。我們還是給個樣例:
#include <crtdbg.h>
inline void EnableMemLeakCheck()
{
   _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
void main()
{
   EnableMemLeakCheck();
   int* leak = new int[10];
}
執行(提醒:不要按Ctrl+F5,按F5),你將發現,產生的記憶體洩漏報告與MFC類似,但有細節不同,如下:
Detected memory leaks!
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做的事情。看下例: 
inline void EnableMemLeakCheck()
{
   _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生成的記憶體洩漏報告: 
Detected memory leaks!
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)。我們加上它:
inline void EnableMemLeakCheck()
{
   _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要求你的程式執行過程是可還原的(多次執行過程的記憶體分配順序不會發生變化)。這個假設在多數情況下成立。不過,在多執行緒的情況下,這一點有時難以保證。

個人心得:我在用這種方法時開始沒看懂,後來在MSDN中也找到了這方面相關的資訊,後來才會用。我感覺在這方面網上介紹的不夠詳細,下面我就相對詳細地解釋一下(為什麼用“相對詳細”?本人比較懶)。首先說明一下,下面的函式不需要上面所新增的巨集定義和"crtdbg.h"標頭檔案,也不需要EnableMemLeakCheck()函式。只需在main函式一開始執行 _CrtSetBreakAlloc(long (4459))函式。其中4459是申請記憶體的序號(上面有說明),然後F5執行(不需要設斷點),然後會出現“Find Source”這個對話方塊,點選“取消”。然後會出現“User breakpoint called from code at xxxx”的對話方塊,點選“確定”,會看到一些彙編的程式碼(不要怕,其實我也看不懂,算然原來學過點彙編),調出堆疊視窗(call stack),在其中的“main() line xxx + xxx bytes”上雙擊(或它的上一行雙擊,我的上一行是一個自定義函式,雙擊後直接定位到我new的地方,定位還是很準的,開始我懷疑,但最後檢查果然是這地方沒釋放)會定位到錯誤行。

第三種:用Ctrl+B來設定,不過現在好像忘了。效果根第二種方法基本一樣。

有人會問,既然第一種方法定位沒問題,為什麼還要介紹第二種?

其實在實際應用中,某些記憶體洩漏它沒有定位到哪一行的,只有記憶體塊的序號(有可能我用的不太會用),這個時候就需要用第二種方法。


本文轉載自:http://hailang19821213.blog.163.com/blog/static/30679461201011162425197/


====總結要點如下====總結要點如下====總結要點如下==========================

// 一般在入口函式cpp中新增以下定義和標頭檔案   CRT庫
#define _CRTDBG_MAP_ALLOC   
#include <stdlib.h>   
#include <crtdbg.h>  


// 記憶體洩露資訊中顯示檔名和程式碼行號
#ifdef _DEBUG
#define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif


static void Debug()
{
// 一般在入口函式一開始新增以下程式碼   
_CrtDumpMemoryLeaks();   
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );   
};


// 根據記憶體分配編號設定斷點:
static void Debug(unsigned int num)
{
//num就是剛剛檢測出來的記憶體洩露的地方大括號內的數字,跳轉到記憶體洩露的地方 
_CrtSetBreakAlloc(num);   
}


可以封裝在一個類中,程式中直接呼叫: 

CDebug::Debug();

===========================================================================

// Debug.h: interface for the CDebug class.
//---------------------------------------------------------------------------------
// 記憶體洩露資訊示例 : 
// {49} normal block at 0x00382F78, 40 bytes long.
// Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
//---------------------------------------------------------------------------------
// 顯示資訊包含:
//  1.記憶體分配的編號(大括弧中的數字);
//  2.記憶體快的型別(普通型、客戶端型、CRT型);
// <1>普通型記憶體塊是你的程式平常分配的記憶體型別。
// <2>客戶端型記憶體塊是MFC程式給需要析構的物件分配的記憶體塊。
// <3>CRT記憶體塊是CRT庫為自己使用而分配的記憶體塊。
// <4>自由塊,它是已經被釋放的記憶體塊;
// <5>忽略塊,它是已經被特殊標示的記憶體塊。
//  3.16進製表示的記憶體位置;
//  4.記憶體快的大小;
//  5.前16bytes的內容。
//////////////////////////////////////////////////////////////////////


#if !defined(AFX_DEBUG_H__6B201A16_E36F_4830_A4F5_BD2207106871__INCLUDED_)
#define AFX_DEBUG_H__6B201A16_E36F_4830_A4F5_BD2207106871__INCLUDED_


#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000


// 一般在入口函式cpp中新增以下定義和標頭檔案   CRT庫
#define _CRTDBG_MAP_ALLOC   
#include <stdlib.h>   
#include <crtdbg.h>  


// 記憶體洩露資訊中顯示檔名和程式碼行號
#ifdef _DEBUG
#define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif


class CDebug  
{
private:
CDebug();
virtual ~CDebug();
public:
static void Debug()
{
// 一般在入口函式一開始新增以下程式碼   
_CrtDumpMemoryLeaks();   
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );   
};


// 根據記憶體分配編號設定斷點:
static void Debug(unsigned int num)
{
//num就是剛剛檢測出來的記憶體洩露的地方大括號內的數字,跳轉到記憶體洩露的地方 
_CrtSetBreakAlloc(num);   
}
};


#endif // !defined(AFX_DEBUG_H__6B201A16_E36F_4830_A4F5_BD2207106871__INCLUDED_)

相關文章