記憶體管理是 C++ 程式穩定性的基石。MFC 在 Win32 堆、C Runtime、虛擬記憶體等多層分配機制之上,建構了完整的調試支援體系:DEBUG_NEW 追蹤分配位置、CMemoryState 快照比對、CObject::Dump/AssertValid 物件自省、TRACE/ASSERT 診斷輸出。本文將系統剖析各層分配 API、MFC 調試宏實現、洩漏偵測流程與物件診斷機制。
| 層級 | API 族 | 特性 | 適用場景 |
|---|---|---|---|
| Win32 堆 | HeapCreate/Alloc/Free/Destroy | 私有堆、不可移動、支援查詢大小 | 大量小塊分配、自訂分配器 |
| 全域/區域 | GlobalAlloc/LocalAlloc 等 | Win32 下同義、平坦記憶體、向後相容 | 舊碼移植、DDE/剪貼簿介面 |
| C Runtime | malloc/calloc/realloc/free | 映射至 HeapAlloc、不可移動、調試版 _dbg 追蹤檔案/行號 | 標準 C/C++ 程式、跨平台 |
| 虛擬記憶體 | VirtualAlloc/Ex/Free/Protect/Query/Lock/Unlock | 頁粒度 (4KB)、保護屬性、保留/認交分離 | 大塊記憶體、記憶體映射檔、JIT 編譯 |
| C++ 運算子 | ::operator new/delete, Class::operator new/delete | 呼叫 malloc/free 或 _malloc_dbg/_free_dbg | 物件建構/解構、型別安全 |
// 全域
void* __cdecl ::operator new(size_t nSize);
void __cdecl ::operator delete(void* p);
// 調試版
void* __cdecl ::operator new(size_t nSize, int nType, LPCSTR lpszFile, int nLine);
// 類別專屬 (static member)
void* PASCAL CMyClass::operator new(size_t nSize);
void PASCAL CMyClass::operator delete(void* p);
PASCAL (__stdcall) 慣例用於類別運算子,__cdecl 用於全域運算子。
_CrtDbgReport:統一報告入口,可導向除錯視窗、檔案、訊息盒。_CrtSetReportMode/File:設定輸出目標。assert:標準斷言,失敗呼叫 _CrtDbgReport。_CrtIsValidHeapPointer(p):指標是否在本地堆。_CrtIsValidPointer(p, size, read/write):範圍可讀寫。_CrtIsMemoryBlock(p, &size, &file, &line):區塊是否在堆中,回傳分配位置。_CrtMemCheckpoint(&state); // 保存快照
_CrtMemDifference(&diff, &s1, &s2); // 比較差異
_CrtMemDumpStatistics(&state); // 輸出統計
_CrtMemDumpAllObjectsSince(&state); // 輸出所有物件詳情
_CrtDumpMemoryLeaks(); // 程式結束自動呼叫,輸出洩漏報告
// 標頭檔自動插入 (AppWizard 產生)
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#define new DEBUG_NEW
#endif
// afx.h 定義
#define DEBUG_NEW new(THIS_FILE, __LINE__)
// 實際調用 _malloc_dbg(nSize, _NORMAL_BLOCK, file, line)
// _NORMAL_BLOCK: 一般分配,_CLIENT_BLOCK: MFC 物件 (附帶 CObject*)
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...)
{
#ifdef _DEBUG
if (!afxTraceEnabled) return;
va_list args; va_start(args, lpszFormat);
TCHAR szBuf[512]; _vstprintf(szBuf, lpszFormat, args);
if ((afxTraceFlags & traceMultiApp) && AfxGetApp())
afxDump << AfxGetApp()->m_pszExeName << ": ";
afxDump << szBuf;
va_end(args);
#endif
}
// 追蹤旗標
enum AfxTraceFlags {
traceMultiApp = 1, traceAppMsg = 2, traceWinMsg = 4,
traceCmdRouting = 8, traceOle = 16, traceDatabase = 32, traceInternet = 64
};
控制方式:afxTraceEnabled 總開關,afxTraceFlags 位元組合過濾類別。
#define ASSERT(f) ((void)((f) || (AfxAssertFailedLine(THIS_FILE, __LINE__), 0)))
#define ASSERT_VALID(p) do { if(!(p) || !(p)->AssertValid()) AfxAssertFailedLine(...); } while(0)
// CObject 虛擬函式
virtual void AssertValid() const; // 檢查 this 指標有效性、vptr、成員一致性
virtual void Dump(CDumpContext& dc) const; // 輸出物件狀態
CDumpContext:無基類,提供 operator<< 重載輸出至除錯視窗。afxDump:全域預定義物件,TRACE、Dump 皆輸出至此。void AfxDoForAllObjects(void (*pfn)(CObject* pObj, void* pContext), void* pContext);
// 遍歷所有 new 分配的 CObject 衍生物件 (排除全域/堆疊/嵌入物件)
// 典型用途:洩漏追蹤、狀態檢查、序列化驗證
#ifdef _DEBUG
CMemoryState oldState, newState, diffState;
oldState.Checkpoint(); // 1. 基準快照
// ... 執行測試代碼,分配/釋放記憶體 ...
newState.Checkpoint(); // 2. 結束快照
if (diffState.Difference(oldState, newState)) {
TRACE("Memory leaked!\n");
diffState.DumpStatistics(); // 統計差異
// diffState.DumpAllObjectsSince(); // 詳細物件列表
}
#endif
自動化:MFC 調試版程式結束時自動呼叫 _CrtDumpMemoryLeaks,輸出洩漏資訊至「輸出」視窗。
AfxEnableMemoryTracking(TRUE/FALSE); // 開啟/關閉,關閉可提速
// afxMemDF 旗標 (位元組合)
allocMemDF = 1; // 基本診斷輸出
delayFreeMemDF = 2; // 程式結束才 free,最大化記憶體使用以暴露越界
checkAlwaysMemDF = 4; // 每次分配/釋放後呼叫 AfxCheckMemory (極慢,深度檢查)
AfxIsValidAddress(p, size, bReadWrite):位址範圍有效性。AfxIsMemoryBlock(p, size, &file, &line):封裝 _CrtIsMemoryBlock。AfxIsString(psz, nMaxLen):字串指標有效性。_DEBUG,連結 libcmtd.lib (MTd) 或 mfcxxxd.lib,否則 DEBUG_NEW 失效。DEBUG_NEW,其洩漏不會顯示檔案/行號,建議重新編譯或使用 _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)。new(ptr) T() 會被宏展開導致錯誤,必須寫 ::new(ptr) T()。operator new 時務必提供對應 operator delete,並考慮對齊需求 (C++17 std::align_val_t)。checkAlwaysMemDF 會使分配速度下降 10-100 倍,僅在嚴重越界懷疑時短暫開啟。MFC 記憶體調試體系形成完整閉環:
DEBUG_NEW → _malloc_dbg 記錄檔案/行號/區塊類型。CMemoryState 快照比對、AfxDoForAllObjects 遍歷、AfxCheckMemory 完整性檢查。TRACE/ASSERT 即時輸出、Dump/AssertValid 物件自省、_CrtDumpMemoryLeaks 退出報告。善用此體系,能在開發早期發現 90% 以上的記憶體問題,大幅降低上線風險。