天天看點

VC使用CRT調試功能來檢測記憶體洩漏

資訊來源:csdn

     C/C++ 程式設計語言的最強大功能之一便是其動态配置設定和釋放記憶體,但是中國有句古話:“最大的長處也可能成為最大的弱點”,那麼 C/C++ 應用程式正好印證了這句話。在 C/C++ 應用程式開發過程中,動态配置設定的記憶體處理不當是最常見的問題。其中,最難捉摸也最難檢測的錯誤之一就是記憶體洩漏,即未能正确釋放以前配置設定的記憶體的錯誤。偶爾發生的少量記憶體洩漏可能不會引起我們的注意,但洩漏大量記憶體的程式或洩漏日益增多的程式可能會表現出各種 各樣的征兆:從性能不良(并且逐漸降低)到記憶體完全耗盡。更糟的是,洩漏的程式可能會用掉太多記憶體,導緻另外一個程式垮掉,而使使用者無從查找問題的真正根源。此外,即使無害的記憶體洩漏也可能殃及池魚。

幸運的是,Visual Studio 調試器和 C 運作時 (CRT) 庫為我們提供了檢測和識别記憶體洩漏的有效方法。下面請和我一起分享收獲——如何使用 CRT 調試功能來檢測記憶體洩漏?

一、如何啟用記憶體洩漏檢測機制

  VC++ IDE 的預設狀态是沒有啟用記憶體洩漏檢測機制的,也就是說即使某段代碼有記憶體洩漏,調試會話的 Output 視窗的 Debug 頁不會輸出有關記憶體洩漏資訊。你必須設定兩個最基本的機關來啟用記憶體洩漏檢測機制。

  一是使用調試堆函數:

#define _CRTDBG_MAP_ALLOC 

#include<stdlib.h> 

#include<crtdbg.h>

注意:#include 語句的順序。如果更改此順序,所使用的函數可能無法正确工作。

  通過包含 crtdbg.h 頭檔案,可以将 malloc 和 free 函數映射到其“調試”版本 _malloc_dbg 和 _free_dbg,這些函數會跟蹤記憶體配置設定和釋放。此映射隻在調試(Debug)版本(也就是要定義 _DEBUG)中有效。發行版本(Release)使用普通的 malloc 和 free 函數。#define 語句将 CRT 堆函數的基礎版本映射到對應的“調試”版本。該語句不是必須的,但如果沒有該語句,那麼有關記憶體洩漏的資訊會不全。

  二是在需要檢測記憶體洩漏的地方添加下面這條語句來輸出記憶體洩漏資訊:

_CrtDumpMemoryLeaks();

  當在調試器下運作程式時,_CrtDumpMemoryLeaks 将在 Output 視窗的 Debug 頁中顯示記憶體洩漏資訊。比如: Detected memory leaks!

Dumping objects ->

C:/Temp/memleak/memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.

Data: <AB> 41 42

c:/program files/microsoft visual studio/vc98/include/crtdbg.h(552) : {44} normal 

block at 0x00441BD0, 33 bytes long.

Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD

c:/program files/microsoft visual studio/vc98/include/crtdbg.h(552) : {43} normal 

block at 0x00441C20, 40 bytes long.

Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00

Object dump complete.

如果不使用 #define _CRTDBG_MAP_ALLOC 語句,記憶體洩漏的輸出是這樣的:

Detected memory leaks!

{45} normal block at 0x00441BA0, 2 bytes long.

Data: <AB> 41 42 

{44} normal block at 0x00441BD0, 33 bytes long.

Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD 

{43} normal block at 0x00441C20, 40 bytes long.

Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00 

  根據這段輸出資訊,你無法知道在哪個源程式檔案裡發生了記憶體洩漏。下面我們來研究一下輸出資訊的格式。第一行和第二行沒有什麼可說的,從第三行開始:

xx}:花括弧内的數字是記憶體配置設定序号,本文例子中是 {45},{44},{43};

block:記憶體塊的類型,常用的有三種:normal(普通)、client(用戶端)或 CRT(運作時);本文例子中是:normal block; 

用十六進制格式表示的記憶體位置,如:at 0x00441BA0 等;

以位元組為機關表示的記憶體塊的大小,如:32 bytes long; 

前 16 位元組的内容(也是用十六進制格式表示),如:Data: 41 42 等;

  仔細觀察不難發現,如果定義了 _CRTDBG_MAP_ALLOC ,那麼在記憶體配置設定序号前面還會顯示在其中配置設定洩漏記憶體的檔案名,以及檔案名後括号中的數字表示發生洩漏的代碼行号,比如: 

C:/Temp/memleak/memleak.cpp(15)

  輕按兩下 Output 視窗中此檔案名所在的輸出行,便可跳到源程式檔案配置設定該記憶體的代碼行(也可以選中該行,然後按 F4,效果一樣) ,這樣一來我們就很容易定位記憶體洩漏是在哪裡發生的了,是以,_CRTDBG_MAP_ALLOC 的作用顯而易見。

使用 _CrtSetDbgFlag

  如果程式隻有一個出口,那麼調用 _CrtDumpMemoryLeaks 的位置是很容易選擇的。但是,如果程式可能會在多個地方退出該怎麼辦呢?在每一個可能的出口處調用 _CrtDumpMemoryLeaks 肯定是不可取的,那麼這時可以在程式開始處包含下面的調用:_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );這條語句無論程式在什麼地方退出都會自動調用 _CrtDumpMemoryLeaks。注意:這裡必須同時設定兩個位域标志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。

設定 CRT 報告模式

  預設情況下,_CrtDumpMemoryLeaks 将記憶體洩漏資訊 dump 到 Output 視窗的 Debug 頁, 如果你想将這個輸出定向到别的地方,可以使用 _CrtSetReportMode 進行重置。如果你使用某個庫,它可能将輸出定向到另一位置。此時,隻要使用以下語句将輸出位置設回 Output 視窗即可:

_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );

  有關使用 _CrtSetReportMode 的詳細資訊,請參考 MSDN 庫關于 _CrtSetReportMode 的描述。

二、解釋記憶體塊類型

  前面已經說過,記憶體洩漏報告中把每一塊洩漏的記憶體分為 normal(普通塊)、client(用戶端塊)和 CRT 塊。事實上,需要留心和注意的也就是 normal 和 client,即普通塊和用戶端塊。

  1.normal block(普通塊):這是由你的程式配置設定的記憶體。

  2.client block(客戶塊):這是一種特殊類型的記憶體塊,專門用于 MFC 程式中需要析構函數的對象。MFC new 操作符視具體情況既可以為所建立的對象建立普通塊,也可以為之建立客戶塊。

  3.CRT block(CRT 塊):是由 C RunTime Library 供自己使用而配置設定的記憶體塊。由 CRT 庫自己來管理這些記憶體的配置設定與釋放,我們一般不會在記憶體洩漏報告中發現 CRT 記憶體洩漏,除非程式發生了嚴重的錯誤(例如 CRT 庫崩潰)。 

  除了上述的類型外,還有下面這兩種類型的記憶體塊,它們不會出現在記憶體洩漏報告中:

  1.free block(空閑塊):已經被釋放(free)的記憶體塊。 

  2.Ignore block(忽略塊):這是程式員顯式聲明過不要在記憶體洩漏報告中出現的記憶體塊。

三、如何在記憶體配置設定序号處設定斷點

  在記憶體洩漏報告中,的檔案名和行号可告訴配置設定洩漏的記憶體的代碼位置,但僅僅依賴這些資訊來了解完整的洩漏原因是不夠的。因為一個程式在運作時,一段配置設定記憶體的代碼可能會被調用很多次,隻要有一次調用後沒有釋放記憶體就會導緻記憶體洩漏。為了确定是哪些記憶體沒有被釋放,不僅要知道洩漏的記憶體是在哪裡配置設定的,還要知道洩漏産生的條件。這時記憶體配置設定序号就顯得特别有用——這個序号就是檔案名和行号之後的花括弧裡的那個數字。 

  例如,在本文例子代碼的輸出資訊中,“45”是記憶體配置設定序号,意思是洩漏的記憶體是你程式中配置設定的第四十五個記憶體塊:

......

  CRT 庫對程式運作期間配置設定的所有記憶體塊進行計數,包括由 CRT 庫自己配置設定的記憶體和其它庫(如 MFC)配置設定的記憶體。是以,配置設定序号為 N 的對象即為程式中配置設定的第 N 個對象,但不一定是代碼配置設定的第 N 個對象。(大多數情況下并非如此。)這樣的話,你便可以利用配置設定序号在配置設定記憶體的位置設定一個斷點。方法是在程式起始附近設定一個位置斷點。當程式在該點中斷時,可以從 QuickWatch(快速監視)對話框或 Watch(監視)視窗設定一個記憶體配置設定斷點:

  例如,在 Watch 視窗中,在 Name 欄鍵入下面的表達式:

_crtBreakAlloc

  如果要使用 CRT 庫的多線程 DLL 版本(/MD 選項),那麼必須包含上下文操作符,像這樣: 

{,,msvcrtd.dll}_crtBreakAlloc

  現在按下Enter鍵,調試器将計算該值并把結果放入 Value 欄。如果沒有在記憶體配置設定點設定任何斷點,該值将為 –1。

  用你想要在其位置中斷的記憶體配置設定的配置設定序号替換 Value 欄中的值。例如輸入 45。這樣就會在配置設定序号為 45 的地方中斷。 

  在所感興趣的記憶體配置設定處設定斷點後,可以繼續調試。這時,運作程式時一定要小心,要保證記憶體塊配置設定的順序不會改變。當程式在指定的記憶體配置設定處中斷時,可以檢視 Call Stack(調用堆棧)視窗和其它調試器資訊以确定配置設定記憶體時的情況。如果必要,可以從該點繼續執行程式,以檢視對象發生了什麼情況,或許可以确定未正确釋放對象的原因。

  盡管通常在調試器中設定記憶體配置設定斷點更友善,但如果願意,也可在代碼中設定這些斷點。為了在代碼中設定一個記憶體配置設定斷點,可以增加這樣一行(對于第四十五個記憶體配置設定):

_crtBreakAlloc = 45;

  你還可以使用有相同效果的 _CrtSetBreakAlloc 函數:

_CrtSetBreakAlloc(45);

四、如何比較記憶體狀态

  定位記憶體洩漏的另一個方法就是在關鍵點擷取應用程式記憶體狀态的快照。CRT 庫提供了一個結構類型 _CrtMemState。你可以用它來存儲記憶體狀态的快照:

_CrtMemState s1, s2, s3;

  若要擷取給定點的記憶體狀态快照,可以向 _CrtMemCheckpoint 函數傳遞一個 _CrtMemState 結構。該函數用目前記憶體狀态的快照填充此結構:

_CrtMemCheckpoint( &s1 );

  通過向 _CrtMemDumpStatistics 函數傳遞 _CrtMemState 結構,可以在任意地方 dump 該結構的内容:

_CrtMemDumpStatistics( &s1 );

  該函數輸出如下格式的 dump 記憶體配置設定資訊:

0 bytes in 0 Free Blocks.

75 bytes in 3 Normal Blocks.

5037 bytes in 41 CRT Blocks.

0 bytes in 0 Ignore Blocks.

0 bytes in 0 Client Blocks.

Largest number used: 5308 bytes.

Total allocations: 7559 bytes.

  若要确定某段代碼中是否發生了記憶體洩漏,可以通過擷取該段代碼之前和之後的記憶體狀态快照,然後使用 _CrtMemDifference 比較這兩個狀态:

_CrtMemCheckpoint( &s1 );// 擷取第一個記憶體狀态快照

// 在這裡進行記憶體配置設定

_CrtMemCheckpoint( &s2 );// 擷取第二個記憶體狀态快照

// 比較兩個記憶體快照的差異

if ( _CrtMemDifference( &s3, &s1, &s2) )

_CrtMemDumpStatistics( &s3 );// dump 差異結果

  顧名思義,_CrtMemDifference 比較兩個記憶體狀态(前兩個參數),生成這兩個狀态之間差異的結果(第三個參數)。在程式的開始和結尾放置 _CrtMemCheckpoint 調用,并使用 _CrtMemDifference 比較結果,是檢查記憶體洩漏的另一種方法。如果檢測到洩漏,則可以使用 _CrtMemCheckpoint 調用通過二進制搜尋技術來分割程式和定位洩漏。

五、結論

  盡管 VC ++ 具有一套專門調試 MFC 應用程式的機制,但本文上述讨論的記憶體配置設定很簡單,沒有涉及到 MFC 對象,是以這些内容同樣也适用于 MFC 程式。在 MSDN 庫中可以找到很多有關 VC++ 調試方面的資料,如果你能善用 MSDN 庫,相信用不了多少時間你就有可能成為調試高手。

繼續閱讀