指標指向已釋放記憶體 (Dangling Pointer) 是 C++ 最難除錯的問題之一。症狀:指標非 NULL、位址對齊、記憶體內容為 `0xFEEEFEEE` (MSVC Debug 填充模式)。本文介紹一種**僅限偵錯期**的技巧:重載全域 `operator new/delete`,在配置/釋放前後記錄呼叫堆疊位址,事後透過記憶體視窗追溯來源。
#ifdef _DEBUG
#include
void* operator new(size_t size) {
int stackVar; // 區域變數,用於計算堆疊位址
unsigned long stackVarAddr = (unsigned long)&stackVar;
unsigned long argAddr = (unsigned long)&size;
// 返回位址通常在 (stackVarAddr + argAddr)/2 + 2 處 (視編譯器/呼叫慣例調整)
void** retAddrAddr = (void**)(stackVarAddr/2 + argAddr/2 + 2);
void* retAddr = *retAddrAddr;
unsigned char* buf = (unsigned char*)malloc(size + 16);
memset(buf, 0, 16);
memcpy(buf, &retAddr, sizeof(retAddr)); // 0~3: new 呼叫者位址
memcpy(buf + 4, &size, sizeof(size)); // 4~7: 配置大小
return buf + 16; // 返回使用者區塊
}
void operator delete(void* ptr) {
if (!ptr) return;
int stackVar;
unsigned long stackVarAddr = (unsigned long)&stackVar;
unsigned long argAddr = (unsigned long)&ptr;
void** retAddrAddr = (void**)(stackVarAddr/2 + argAddr/2 + 2);
void* retAddr = *retAddrAddr;
unsigned char* buf = (unsigned char*)ptr;
buf -= 8; // 指向大小欄位
size_t size;
memcpy(&size, buf, sizeof(size)); // 讀取原始大小
buf += 8; // 指向使用者資料起始 (即原 ptr)
memcpy(buf, &retAddr, sizeof(retAddr)); // 8~11: delete 呼叫者位址
buf[4] = 0xDE; buf[5] = 0xAD; buf[6] = 0xBE; buf[7] = 0xEF; // 12~15: DEADBEEF
memset(buf + 8, 0x77, size); // 填充使用者區塊
// 故意不 free,保留記錄供事後分析 → 偵錯期記憶體洩漏可接受
}
#endif
Address Value (Little Endian) Meaning
ptr-10 XX XX XX XX new 呼叫者返回位址 (需位元組反轉)
ptr-C YY YY YY YY 配置大小
ptr-8 ZZ ZZ ZZ ZZ delete 呼叫者返回位址 (需反轉)
ptr-4 DE AD BE EF 釋放標記
ptr+0 77 77 77 77 ... 已釋放資料區填充
修改 `delete` 真正 `free(buf - 16)`,程式結束前遍歷堆疊 (`_heapwalk`),收集所有未釋放區塊的 `new` 位址,產生洩漏報表。
// stdafx.h 或專案屬性 C/C++ → Preprocessor → _DEBUG 定義下
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
輸出範例:
Detected memory leaks!
D:\Project\MyClass.cpp(42) : {123} client block at 0x004A3B20, 40 bytes long.
Data: 4D 79 43 6C 61 73 73 00 CD CD CD CD CD CD CD CD
直接顯示**檔案名、行號、類別名**,無需手動反組譯。