MFC 偵錯技術
如果您正在偵錯 MFC 程式,這些偵錯技術可能很有幫助。
本主題內容
AfxDebugBreak
TRACE 巨集
在 MFC 偵測記憶體遺漏
追蹤記憶體配置
啟用記憶體診斷
取得記憶體快照
檢視記憶體統計資料
將物件傾印
解譯記憶體傾印
自訂物件傾印。
減少 MFC 偵錯組建的大小偵錯組建
- 建立 MFC 應用程式與偵錯選取模組的資訊
AfxDebugBreak
MFC 提供了一個特殊的 AfxDebugBreak 函式,可在原始程式碼中直接撰寫 (硬式編碼) 中斷點:
AfxDebugBreak( );
在 Intel 平台上,AfxDebugBreak 會產生下列會在原始程式碼裡而非在核心 (Kernel) 程式碼中斷的程式碼:
_asm int 3
在其他的平台上,AfxDebugBreak 只會呼叫 DebugBreak。
當您建立發行的組建或使用 #ifdef _DEBUG 來包圍它們時,請務必要移除 AfxDebugBreak 陳述式。
本主題內容
TRACE 巨集
若要在偵錯工具輸出視窗裡顯示程式的訊息,您可以使用 ATLTRACE 巨集或 MFC TRACE 巨集。 像判斷提示一樣,追蹤巨集只有在程式的偵錯版本才會啟動而且在發行版本編譯時會消失。
下列範例顯示一些您可以使用 TRACE 巨集的方式。 就像 printf 一樣,TRACE 巨集可以處理許多引數。
int x = 1;
int y = 16;
float z = 32.0;
TRACE( "This is a TRACE statement\n" );
TRACE( "The value of x is %d\n", x );
TRACE( "x = %d and y = %d\n", x, y );
TRACE( "x = %d and y = %x and z = %f\n", x, y, z );
TRACE 巨集會適當處理 char* 和 wchar_t* 這兩種參數。 下列範例示範搭配不同類型的字串參數來使用 TRACE 巨集。
TRACE( "This is a test of the TRACE macro that uses an ANSI string: %s %d\n", "The number is:", 2);
TRACE( L"This is a test of the TRACE macro that uses a UNICODE string: %s %d\n", L"The number is:", 2);
TRACE( _T("This is a test of the TRACE macro that uses a TCHAR string: %s %d\n"), _T("The number is:"), 2);
如需 TRACE 巨集的詳細資訊,請參閱診斷服務。
本主題內容
在 MFC 偵測記憶體遺漏
MFC 提供類別和函式來偵測已配置但從未解除配置的記憶體。
追蹤記憶體配置
在 MFC 裡,您可以使用 DEBUG_NEW 巨集取代 new 運算子來幫助尋找記憶體遺漏。 在程式的偵錯版本裡,DEBUG_NEW 追蹤每個物件所配置的檔案名稱和行號。 當您編譯程式的發行版本時,DEBUG_NEW 解析成簡單而不具檔名和行號資訊的 new 操作。 因此,在程式的發行版本中不會有速度負擔。
如果您不要以 DEBUG_NEW 取代 new 來重新編寫整個程式,您可以在原始程式檔裡定義這個巨集:
#define new DEBUG_NEW
當您執行物件傾印,每個以 DEBUG_NEW 配置的物件會顯示其配置位置的檔案和行號,讓您可以指出記憶體遺漏的來源。
MFC 架構的偵錯版本會自動使用 DEBUG_NEW,但是您的程式碼不會。 如果您要擁有 DEBUG_NEW 的優點,您必須依照上述方式,明確地使用 DEBUG_NEW 或 #define new。
本主題內容
啟用記憶體診斷
您必須在使用記憶體診斷設施之前啟用診斷追蹤。
若要啟用或停用記憶體診斷
- 呼叫全域函式 AfxEnableMemoryTracking 來啟用或停用診斷記憶體配置器 (Allocator)。 因為記憶體診斷在偵錯程式庫中預設是啟用的,通常您會使用這個函式將它們暫時地關閉,以增加程式執行速度和減少診斷輸出。
若要選取具 afxMemDF 的特定記憶體診斷功能
如果您要更準確地控制記憶體診斷功能,您可以設定 MFC 全域變數 afxMemDF 值,選擇性地開啟和關閉各個記憶體診斷功能。 這個變數可以有下列的值,如同列舉類型 afxMemDF 所指定。
值
描述
allocMemDF
開啟診斷記憶體配置器 (預設)。
delayFreeMemDF
在呼叫 delete 或 free 在程式結束時,請延遲釋放記憶體。 這會造成程式配置可能的最大記憶體量。
checkAlwaysMemDF
每一次記憶體配置或釋放時都會呼叫 AfxCheckMemory。
這些值可以藉由執行邏輯 OR 操作用於結合,如下所示:
afxMemDF = allocMemDF | delayFreeMemDF | checkAlwaysMemDF;
本主題內容
取得記憶體快照
建立 CMemoryState 物件並且呼叫 CMemoryState::Checkpoint 成員函式。 這會建立第一個記憶體快照。
程式執行記憶體配置和解除配置操作之後,會建立另一個 CMemoryState 物件並且呼叫此物件的 Checkpoint。 這會取得記憶體使用的第二個快照。
建立第三個 CMemoryState 物件並且呼叫 CMemoryState::Difference 成員函式,會提供兩個先前的 CMemoryState 物件來當做引數。 如果兩種記憶體狀態之間有差異,Difference 函式會傳回非零值。 這表示有些記憶體區塊沒有解除配置。
這個範例會顯示程式碼看起來的樣子:
// Declare the variables needed #ifdef _DEBUG CMemoryState oldMemState, newMemState, diffMemState; oldMemState.Checkpoint(); #endif // Do your memory allocations and deallocations. CString s("This is a frame variable"); // The next object is a heap object. CPerson* p = new CPerson( "Smith", "Alan", "581-0215" ); #ifdef _DEBUG newMemState.Checkpoint(); if( diffMemState.Difference( oldMemState, newMemState ) ) { TRACE( "Memory leaked!\n" ); } #endif
請注意記憶體檢查的陳述式由 #ifdef_DEBUG、 #endif 區塊括號起來,以便只編譯在偵錯程式版本。
現在您知道有記憶體遺漏存在,您可以使用另一個成員函式,CMemoryState::DumpStatistics,來記憶體統計資料檢視,幫助您尋找遺漏記憶體。
本主題內容
檢視記憶體統計資料
CMemoryState::Difference 函式會查看兩個記憶體狀態物件,並偵測開頭和結尾狀態之間任何沒有從堆積解除配置的物件。 使用 CMemoryState::Difference後,在您採取了記憶體快照並比較之後,您可以呼叫 CMemoryState::DumpStatistics 取得有關尚未解除配置之物件的相關資訊。
參考下列範例:
if( diffMemState.Difference( oldMemState, newMemState ) )
{
TRACE( "Memory leaked!\n" );
diffMemState.DumpStatistics();
}
從此範例的傾印看起來會像這樣:
0 bytes in 0 Free Blocks
22 bytes in 1 Object Blocks
45 bytes in 4 Non-Object Blocks
Largest number used: 67 bytes
Total allocations: 67 bytes
自由區塊是指如果 afxMemDF 設為 delayFreeMemDF 則解除配置會延遲的區塊。
一般物件區塊 (顯示在第二行) 會維持在堆積配置。
非物件區塊會以 new 包含已配置的陣列和結構。 在這種情況下,四個非物件區塊會配置於堆積上而不會解除配置。
Largest number used 提供程式在任何時間可以使用的最大記憶體。
Total allocations 提供程式所使用的總記憶體量。
本主題內容
將物件傾印
在 MFC 程式中,您可以使用 CMemoryState::DumpAllObjectsSince 來傾印堆積上尚未解除配置之所有物件的描述。 DumpAllObjectsSince 會傾印從上一個 CMemoryState::Checkpoint 之後配置的所有物件。 如果沒有發生 Checkpoint 呼叫,DumpAllObjectsSince 會傾印目前在記憶體的所有物件和非物件。
注意事項 |
---|
在您可以使用來傾印前 MFC 的物件,您必須 啟用診斷追蹤。。 |
注意事項 |
---|
MFC 會在程式結束時自動傾印所有的遺漏物件,因此您不需要建立程式碼來傾印這個時候的物件。 |
下列程式碼會比較兩個記憶體狀態來測試記憶體遺漏,如果偵測到遺漏便會傾印所有物件:
if( diffMemState.Difference( oldMemState, newMemState ) )
{
TRACE( "Memory leaked!\n" );
diffMemState.DumpAllObjectsSince();
}
傾印的內容看起來像這樣:
Dumping objects ->
{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4
Last Name: Smith
First Name: Alan
Phone #: 581-0215
{1} strcore.cpp(80) : non-object block at $00A7516E, 25 bytes long
大多數程式碼一開頭括號裡的數字便指定了物件配置的順序。 最近配置的物件的數字最高,且會出現在傾印的最上方。
若要讓最大資訊量逸出離物件傾印,您可以覆寫任何 CObject的 Dump 成員函式 (自訂物件傾印的衍生物件。
您可以將全域變數 _afxBreakAlloc 設定為顯示在括號裡的數字,將中斷點設於特定記憶體配置上。 如果您重新執行該程式,則當此配置發生時,偵錯工具會中斷執行。 然後您可以查看呼叫堆疊來檢視程式如何到達此點。
C 執行階段程式庫中類似此功能的函式是 _CrtSetBreakAlloc,可用於 C 執行階段配置。
本主題內容
解譯記憶體傾印
請看此物件傾印的詳細內容:
{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4
Last Name: Smith
First Name: Alan
Phone #: 581-0215
{1} strcore.cpp(80) : non-object block at $00A7516E, 25 bytes long
產生這種傾印的程式只有兩種明確的配置,一種是在堆疊上,另一種是在堆積上:
// Do your memory allocations and deallocations.
CString s("This is a frame variable");
// The next object is a heap object.
CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
CPerson 建構函式 (Constructor) 需要三個 char 指標的引數 (用來初始化 CString 成員變數)。 在記憶體傾印裡,您可以看到 CPerson 物件和三個非物件區塊 (3、4 和 5)。 這些物件會儲存 CString 成員變數字元,且在不會在叫用 (Invoke) CPerson 物件解構函式 (Destructor) 刪除。
區塊編號 2 是 CPerson 物件本身。 $51A4 表示區塊的位址而且後面跟著物件的內容 (由 DumpAllObjectsSince 呼叫時,由 CPerson::Dump 所輸出)。
您可以由區塊編號 1 的順序號碼和大小猜想出它與 CString 框架變數相關,這些資訊符合 CString 框架變數裡的字元數字。 框架上配置的變數在框架超過範圍 (Scope) 時會自動解除配置。
框架變數
一般來說,您不必擔心與框架變數相關的堆積物件,因為它們會在框架變數超過範圍時自動解除配置。 若要避免在記憶體診斷傾印裡發生混亂,您應該定位 Checkpoint 呼叫,這樣它們會在框架變數範圍的外部。 例如,將範圍括號放在前述配置程式碼的前後,如下所示:
oldMemState.Checkpoint();
{
// Do your memory allocations and deallocations ...
CString s("This is a frame variable");
// The next object is a heap object.
CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
}
newMemState.Checkpoint();
加了範圍括號,這個範例的記憶體傾印如下:
Dumping objects ->
{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4
Last Name: Smith
First Name: Alan
Phone #: 581-0215
非物件配置
請注意有些配置是物件 (例如 CPerson) 而有些則是非物件配置。「 非物件配置」是指不是從 CObject 衍生之物件的配置,或是基本 C 型別 (例如 char、int 或 long) 的配置。 如果 CObject 衍生類別配置額外空間 (例如為內部緩衝區),則該類物件便會顯示物件和非物件配置。
防止記憶體遺漏
請注意,在上面的程式碼中,與 CString 框架變數相關聯的記憶體區塊已經自動解除配置,而且不會顯示為記憶體遺漏。 與範圍規則相關的自動解除配置會處理大多數與框架變數相關的記憶體遺漏。
然而,對於堆積上的物件配置,您必須明確地刪除物件以防止記憶體遺漏。 若要清除先前範例裡最後的記憶體遺漏,請參考下列程式碼,刪除配置在堆積的 CPerson 物件:
{
// Do your memory allocations and deallocations.
CString s("This is a frame variable");
// The next object is a heap object.
CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
delete p;
}
本主題內容
自訂物件傾印。
當您從 CObject 衍生類別時,使用 DumpAllObjectsSince 來傾印物件至輸出視窗時,您可以覆寫 Dump 成員函式以提供額外的資訊。
Dump 函式將物件的成員變數的文字表示寫入傾印內容 (CDumpContext)。 傾印內容類似 I/O 資料流。 您可以使用附加運算子 (<<) 將資料傳送至 CDumpContext。
當您覆寫 Dump 函式時,您應該先呼叫 Dump 的基底類別版本來傾印基底類別物件的內容。 接著輸出衍生類別中每個成員變數的文字說明和值。
Dump 函式的宣告看起來像這樣:
class CPerson : public CObject
{
public:
#ifdef _DEBUG
virtual void Dump( CDumpContext& dc ) const;
#endif
CString m_firstName;
CString m_lastName;
// And so on...
};
因為只有在偵錯程式時,物件傾印才具有意義,Dump 函式的宣告會以 #ifdef _DEBUG / #endif 區塊括號起來。
在下列範例裡,Dump 函式先呼叫基底類別的 Dump 函式。 然後將每個成員變數的簡短說明和成員的值一起寫入至診斷資料流。
#ifdef _DEBUG
void CPerson::Dump( CDumpContext& dc ) const
{
// Call the base class function first.
CObject::Dump( dc );
// Now do the stuff for our specific class.
dc << "last name: " << m_lastName << "\n"
<< "first name: " << m_firstName << "\n";
}
#endif
您必須提供 CDumpContext 引數來指定傾印的輸出位置。 MFC 的偵錯版本提供一個可以傳送輸出至偵錯工具的預先定義 CDumpContext 物件 (名為 afxDump)。
CPerson* pMyPerson = new CPerson;
// Set some fields of the CPerson object.
//...
// Now dump the contents.
#ifdef _DEBUG
pMyPerson->Dump( afxDump );
#endif
本主題內容
減少 MFC 偵錯組建的大小偵錯組建
大型 MFC 應用程式的偵錯資訊可能需要大量的磁碟空間。 您可以使用下列其中一項程序減少:
使用 /Z7、/Zi、/ZI (偵錯資訊格式) 選項重建 MFC 程式庫,而非 /Z7。 這些選項會建置包含整個程式庫的偵錯資訊,以降低重複性並且節省空間的單一程式資料庫 (PDB) 檔。
重建不具偵錯資訊的 MFC 程式庫 (沒有 /Z7、/Zi、/ZI (偵錯資訊格式) 選項)。 在這個範例裡,缺乏偵錯資訊讓您無法在 MFC 程式庫程式碼裡使用大多數的偵錯工具設施,然而由於 MFC 程式庫已經充分偵錯過了,所以這不是問題。
建立您的應用程式只使用如下所述偵錯選取模組的資訊。
本主題內容
建立 MFC 應用程式與偵錯選取模組的資訊
以 MFC 偵錯程式庫建置選取模組,可以讓您在這些模組裡使用逐步執行的方法和其他偵錯設施。 這個程序利用 Visual C++ Makefile 的偵錯和發行模式,因此必要的變更說明如下列步驟 (此外在需要完整發行組建時,「全部重建」是必須的):
在 [方案總管] 中選取專案。
從 [檢視] 功能表中,選取 [屬性頁]。
首先,您要建立新專案組態。
在 [<專案> 屬性頁] 對話方塊裡,按一下 [組態管理員] 按鈕。
在組態管理員對話方塊裡,在方格中尋找專案。 在 [組態] 欄裡,選取 [<新增...>]。
在新增專案組態對話方塊裡,於 [專案組態名稱] 方塊內輸入新組態的名稱,例如「部分偵錯」。
在 [複製設定值] 清單裡,選擇 [發行]。
按一下關閉 [新的專案組態]對話方塊中的 [好] 。
關閉 [組態管理員] 對話方塊。
現在,您要為整個專案設定選項。
在 [屬性頁] 對話方塊裡,[組態屬性] 資料夾底下,選取 [一般] 分類。
在專案設計方格裡,展開 [專案預設值] (如果有需要)。
在 [專案預設值] 底下,尋找 [MFC 用法]。 目前的設定會出現在方格的右欄。 按一下目前設定並且將它變更為 [使用 MFC 的靜態程式庫]。
在 [屬性頁] 對話方塊的左窗格裡,開啟 [C/C++] 資料夾並且選取 [前置處理器]。 在屬性方格裡,尋找 [前置處理器定義],並以 "_DEBUG" 取代 "NDEBUG"。
在 [屬性頁] 對話方塊的左窗格裡,開啟 [連結器] 資料夾並且選取 [輸入] 分類。 在屬性方格裡,尋找 [其他相依性]。 在 [其他相依性] 設定裡,輸入「NAFXCWD.LIB」和「LIBCMT」。
按一下 [確定] 以儲存新組建選項,並且關閉 [屬性頁] 對話方塊。
從 [建置] 功能表選取 [重建]。 這會從模組移除所有的偵錯資訊,但是不會影響 MFC 程式庫。
現在您必須將偵錯資訊加回至應用程式裡的選取模組。 記住您只能在以偵錯資訊編譯的模組裡設定中斷點並且執行其他偵錯工具功能。 對於您要包含偵錯資訊的每個專案檔,執行下列步驟:
在 [方案總管] 裡,開啟位於專案下的 [原始程式檔] 資料夾。
選取您要設定偵錯資訊的檔案。
從 [檢視] 功能表中,選取 [屬性頁]。
在 [屬性頁] 對話方塊的 [組態設定] 資料夾底下,開啟 [C/C++] 資料夾,然後選取 [一般] 分類。
在屬性方格裡,尋找 [偵錯資訊格式]。
按一下 [偵錯資訊格式] 設定並且選取偵錯資訊需要的選項 (通常是 [/ZI])。
如果您要使用應用程式精靈所產生的應用程式,或者您有先行編譯的標頭,則必須關閉先行編譯的標頭,或在編譯其他模組之前重新編譯這些標頭。 否則,您會收到警告 C4650 和錯誤訊息 C2855。 您可以變更 [<專案> 屬性] 對話方塊裡的 [建立/使用預先編譯的標題] 設定 (在 [組態屬性] 資料夾下開啟 [C/C++] 子資料夾,然後選取 [先行編譯標頭] 分類),以關閉先行編譯的標頭。
從 [建置] 功能表,選取 [建置] 來重建過期的專案檔案。
除了本主題所說明的技巧以外,您可以使用外部 Makefile 來定義每個檔案的個別選項。 在此情況中,若要連結 MFC 偵錯程式庫,您必須定義每個模組的 _DEBUG 旗標。 如果您要使用 MFC 發行程式庫,您必須定義 NDEBUG。 如需撰寫外部 Makefile 的詳細資訊,請參閱 NMAKE 參考。
本主題內容