內存泄漏定位(依賴MFC)

    今天調試程序,發現有內存泄漏但是沒有提示具體是哪一行,搞得我很頭疼。結果在網上搜索了一些資料,經自己實踐後整理如下:

 

    第一種:通過"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 0x003C441040 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 0x003C441040 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 0x003C441040 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來設定,不過現在好像忘了。效果根第二種方法基本一樣。

 

有人會問,既然第一種方法定位沒問題,爲什麼還要介紹第二種?其實在實際應用中,某些內存泄漏它沒有定位到哪一行的,只有內存塊的序號(有可能我用的不太會用),這個時候就需要用第二種方法。

發佈了34 篇原創文章 · 獲贊 5 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章