MFC 檢測內存泄漏的方法

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_)

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