共用方式為


MFC 偵錯技術

如果您要偵錯 MFC 程式,這些偵錯技術可能很有用。

AfxDebugBreak

MFC 為原始程式碼中的硬式編碼斷點提供特殊的 AfxDebugBreak 函式:

AfxDebugBreak( );

在 Intel 平臺上,AfxDebugBreak 會產生以下程式碼,這些程式碼會在原始程式碼中發生中斷,而不是在核心程式碼中:

_asm int 3

在其他平臺上, AfxDebugBreak 只呼叫 DebugBreak

當您建立發行組建時,請務必移除 AfxDebugBreak 語句,或使用 #ifdef _DEBUG 將它們括住。

TRACE 巨集

若要在調試程式 [輸出] 視窗中顯示來自程式的訊息,您可以使用 ATLTRACE 巨集或 MFC TRACE 巨集。 如同 判斷提示,追蹤巨集只會在程序的偵錯版本中作用中,並在發行版本中編譯時消失。

下列範例顯示您可以使用 TRACE 巨集的一些方式。 如同 printfTRACE 巨集可以處理許多自變數。

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 取代 新的 運算符,協助找出記憶體流失。 在程式的偵錯版本中,DEBUG_NEW 會追蹤每個被配置物件的檔案名稱和行號。 當您編譯程式的 Release 版本時,會解析為簡單的DEBUG_NEW作業,而不需要檔名和行號資訊。 因此,您在程序的發行版本中不支付任何速度的懲罰。

如果您不想重寫整個程式來使用DEBUG_NEW取代new,您可以在原始程式檔中定義此巨集:

#define new DEBUG_NEW

當您執行 物件傾印時,每個使用 DEBUG_NEW 配置的物件都會顯示其配置所在的檔案和行號,讓您找出記憶體洩漏的來源。

MFC 架構的偵錯版本會自動使用 DEBUG_NEW ,但您的程式代碼不會使用。 如果您想要DEBUG_NEW的優點,您必須明確使用DEBUG_NEW#define new,如上所示。

啟用記憶體診斷

您必須先啟用診斷追蹤,才能使用記憶體診斷設備。

啟用或停用記憶體診斷

  • 呼叫全域函式 AfxEnableMemoryTracking 來啟用或停用診斷記憶體配置器。 由於偵錯連結庫中的記憶體診斷預設為開啟,因此您通常會使用此函式暫時關閉它們,這會增加程序執行速度,並減少診斷輸出。

    若要選取具有 afxMemDF 的特定記憶體診斷功能

  • 如果您想要更精確地控制記憶體診斷功能,您可以藉由設定MFC全域變數 afxMemDF 的值,選擇性地開啟和關閉個別記憶體診斷功能。 此變數可以具有下列值,如列舉型別 afxMemDF 所指定。

    價值 說明
    allocMemDF 的 開啟診斷記憶體配置器 (預設值)。
    延遲FreeMemDF 呼叫 deletefree 直到程序結束時,延遲釋放記憶體。 這會導致程式配置最大可能記憶體數量。
    檢查始終記憶體 DF 每次配置或釋放記憶體時,呼叫 AfxCheckMemory

    這些值可以藉由執行邏輯 OR 作業來搭配使用,如下所示:

    afxMemDF = allocMemDF | delayFreeMemDF | checkAlwaysMemDF;
    

擷取記憶體快照

  1. 建立 CMemoryState 物件並呼叫 CMemoryState::Checkpoint 成員函式。 這會建立第一個記憶體快照集。

  2. 在您的程式執行完記憶體配置和解除分配作業之後,請建立另一個 CMemoryState 物件,然後為該物件呼叫 Checkpoint。 這會取得記憶體使用量的第二個快照集。

  3. 建立第三個 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

大多數行開頭的大括號中的數字指示對象被配置的順序。 最近配置的物件具有最高的數目,並出現在傾印頂端。

若要從物件傾印中取得最大資訊量,您可以覆寫任何從 Dump 衍生之對象的 CObject 成員函式,以自定義物件傾印。

您可以將全域變數 _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構函式會採用三個指向char的參數,這些參數被用來初始化CString成員變數。 在記憶體轉儲中,您可以看到 CPerson 物件以及三個非物件區塊(3、4 和 5)。 這些會保存 CString 成員變數的字元,並且在呼叫 CPerson 對象解構函式時不會被刪除。

區塊編號 2 是 CPerson 物件本身。 $51A4 代表 區塊的位址,後面接著 對象的內容,其輸出方式 CPerson為 ::DumpDumpAllObjectsSince 呼叫時。

您可以猜測區塊數位 1 與 CString 框架變數相關聯,因為它的序號和大小符合框架 CString 變數中的字元數。 框架超出範圍時,會自動取消配置框架上配置的變數。

框架變數

一般而言,您不應該擔心與框架變數相關聯的堆積對象,因為它們會在框架變數超出範圍時自動解除分配。 若要避免記憶體診斷傾印中的雜亂,您應該將呼叫定位到 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 型別,例如 charintlong。 如果 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 衍生類別時,您可以在使用 Dump 將對象傾印至 [輸出] 視窗時,覆寫成員函式以提供其他資訊。

函數 Dump 會將對象的成員變數的文字表示寫入轉儲內容(CDumpContext)。 轉儲上下文類似於 I/O 流。 您可以使用 append 運算子 (<<) 將資料傳送至 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 區塊括住。

在下列範例中,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 應用程式的偵錯資訊可能會佔用大量的磁碟空間。 您可以使用下列其中一個程式來減少大小:

  1. 使用 /Z7、/Zi、/ZI (偵錯資訊格式) 選項來重建 MFC 連結庫,而不是 /Z7。 這些選項會建置單一程式資料庫 (PDB) 檔案,其中包含整個連結庫的偵錯資訊,減少備援並節省空間。

  2. 在沒有偵錯資訊的情況下重建 MFC 連結庫(無 /Z7、/Zi、/ZI(偵錯資訊格式) 選項。 在此情況下,缺少偵錯資訊會防止您在 MFC 連結庫程式碼中使用大部分的調試程式設施,但由於 MFC 連結庫已徹底偵錯,因此可能不是問題。

  3. 僅使用所選模組的偵錯資訊建置您自己的應用程式,如下所述。

使用所選取模組的偵錯資訊建置 MFC 應用程式

使用 MFC 偵錯庫建置選取的模組,可讓您在這些模組中使用逐步執行和其他偵錯功能。 此程序同時使用專案的「偵錯」和「發行」組態,因此需要進行下列步驟中所述的變更(當需要完整發行組建時,也需要進行「全部重建」)。

  1. 在 [方案總管] 中,選取 專案。

  2. 檢視功能表中,選取屬性頁

  3. 首先,您將建立新的項目組態。

    1. 在 [ <專案> 屬性頁] 對話框中,按兩下 [組態管理員 ] 按鈕。

    2. 在 [ 組態管理員] 對話框中,於方格中找出您的專案。 在 [ 組態] 數據行中,選取 [ <新增...>]。

    3. 在 [ 新增專案組態] 對話框中,於 [專案組態 名稱 ] 方塊中輸入新組態的名稱,例如 [部分偵錯]。

    4. 在 [ 複製設定] 列表中,選擇 [ 發行]。

    5. 按兩下 [確定 ] 關閉 [ 新增項目組態 ] 對話框。

    6. 關閉 [組態管理員] 對話框。

  4. 現在,您將設定整個項目的選項。

    1. 在 [ 屬性頁] 對話方塊的 [ 組態屬性 ] 資料夾下,選取 [ 一般] 類別。

    2. 在 [項目設定] 方格中,展開 [ 項目預設值 ] (如有必要)。

    3. [項目預設值] 底下,尋找 [MFC 的使用]。 目前的設定會出現在方格的右欄中。 按一下現有的設定,並將其更改為 使用靜態庫中的MFC

    4. 在 [ 屬性頁 ] 對話框的左窗格中,開啟 C/C++ 資料夾,然後選取 [預處理器]。 在屬性方格中,尋找 預處理器定義 ,並將 「NDEBUG」 取代為 「_DEBUG」。。

    5. 在 [ 屬性頁 ] 對話框的左窗格中,開啟 [鏈接器 ] 資料夾,然後選取 [輸入 類別]。 在 [屬性] 方格中,尋找 [其他相依性]。 在 其他相依性 設定中,輸入「NAFXCWD.LIB」和「LIBCMT」。

    6. 按兩下 [確定 ] 以儲存新的組建選項,然後關閉 [ 屬性頁] 對話框。

  5. 建置功能表中,選取重建。 這會從模組中移除所有偵錯資訊,但不會影響MFC 連結庫。

  6. 現在,您必須將偵錯資訊新增回應用程式中選取的模組。 請記住,您可以設定斷點,並只在您使用偵錯資訊編譯的模組中執行其他調試程式函式。 針對您想要包含偵錯資訊的每個項目檔,請執行下列步驟:

    1. 在 [方案總管] 中,開啟位於您專案底下的 [原始程序檔] 資料夾。

    2. 選取您要設定偵錯資訊的檔案。

    3. 檢視功能表中,選取屬性頁

    4. 在 [ 屬性頁] 對話方塊的 [ 組態設定 ] 資料夾底下,開啟 C/C++ 資料夾,然後選取 [ 一般 ] 類別。

    5. 在屬性方格中,尋找 [ 偵錯資訊格式]。

    6. 按兩下 [ 偵錯資訊格式] 設定,然後選取偵錯資訊所需的選項 (通常是 /ZI)。

    7. 如果您使用應用程式精靈產生的應用程式或具有先行編譯的標頭,您必須先關閉先行編譯的標頭,或在編譯其他模組之前重新編譯它們。 否則,您會收到警告 C4650 和錯誤訊息 C2855。 您可以在 [>,以關閉先行編譯標頭(組態屬性資料夾、C/C++子資料夾、先行編譯標頭類別)。

  7. 從 [ 建置] 功能表中,選取 [ 置] 以重建過期的項目檔。

    除了本主題所述的技術,您可以使用外部makefile來定義每個檔案的個別選項。 在此情況下,若要與 MFC 偵錯連結庫連結,您必須為每個模組定義 _DEBUG 旗標。 如果您想要使用 MFC 發布版本的庫,則必須定義 NDEBUG。 如需撰寫外部 makefiles 的詳細資訊,請參閱 NMAKE 參考