記憶體洩漏是 C/C++ 開發的常見痛點。本文介紹一個簡易、可直接嵌入專案的記憶體洩漏自動偵測機制:利用雙向鏈表串聯所有動態配置的記憶體區塊,在程式結束時自動報告未釋放區塊的檔案名、行號、大小,快速定位洩漏來源。
MEMORY_TAG,記錄大小、來源檔案、行號、前後指標。g_header 串聯所有存活區塊。operator new/delete:攔截全域 new/delete,自動維護鏈表。NEW:#define NEW new(__FILE__, __LINE__) 自動帶入檔案/行號。atexit 或手動呼叫 DumpUnAllocatedMem() 列印洩漏報告。struct MEMORY_TAG
{
int nSize;
const char* szFile;
int nLine;
MEMORY_TAG* pPrev;
MEMORY_TAG* pNext;
};
MEMORY_TAG g_header = {0, 0, 0, 0, 0}; // 哨兵節點
void* operator new(size_t size, const char* pszFile, int line)
{
size_t nRealSize = size + sizeof(MEMORY_TAG);
MEMORY_TAG* pTag = (MEMORY_TAG*)malloc(nRealSize);
pTag->nSize = nRealSize;
pTag->szFile = pszFile;
pTag->nLine = line;
// 插入鏈表頭部
if (g_header.pNext) g_header.pNext->pPrev = pTag;
pTag->pNext = g_header.pNext;
g_header.pNext = pTag;
pTag->pPrev = &g_header;
return pTag + 1; // 回傳使用者可用區塊起始位址(跳過 TAG)
}
// 一般 new 轉呼叫帶檔案/行號版本
void* operator new(size_t size)
{
return operator new(size, __FILE__, __LINE__);
}
void operator delete(void* p)
{
if (!p) return;
MEMORY_TAG* pTag = (MEMORY_TAG*)(((char*)p) - sizeof(MEMORY_TAG));
// 從鏈表移除
pTag->pPrev->pNext = pTag->pNext;
if (pTag->pNext) pTag->pNext->pPrev = pTag->pPrev;
free(pTag); // 釋放整個區塊(含 TAG)
}
// 帶檔案/行號版本(符合 C++ 標準簽名)
void operator delete(void* p, const char*, int) { operator delete(p); }
void DumpUnAllocatedMem()
{
for (MEMORY_TAG* p = g_header.pNext; p; p = p->pNext)
{
printf("%s(%d) : leak %d bytes\n",
p->szFile ? p->szFile : "", p->nLine, p->nSize);
}
}
int CountOfAllocatedMem(size_t* pnSize = NULL)
{
int nCount = 0;
size_t allocated = 0;
for (MEMORY_TAG* p = g_header.pNext; p; p = p->pNext)
{
allocated += p->nSize;
nCount++;
}
if (pnSize) *pnSize = allocated;
printf("%d blocks, %zu bytes total\n", nCount, allocated);
return nCount;
}
#define NEW new(__FILE__, __LINE__)
class Object {
public:
Object() { cout << "Object ctor\n"; }
~Object() { cout << "Object dtor\n"; }
char data[1024];
};
int main()
{
ASSERT(CountOfAllocatedMem() == 0); // 初始無配置
Object* pObj = NEW Object; // 一般 new
ASSERT(CountOfAllocatedMem() == 1);
delete pObj; // 正確釋放
ASSERT(CountOfAllocatedMem() == 0);
pObj = NEW Object; // 再次配置
// 故意不 delete,模擬洩漏
char* p = new char[968]; // 陣列 new 也會被追蹤
ASSERT(CountOfAllocatedMem() == 2);
DumpUnAllocatedMem(); // 程式結束前列印洩漏報告
return 0;
}
main.cpp(45) : leak 1048 bytes main.cpp(48) : leak 992 bytes 2 blocks, 2040 bytes total
| 限制 | 說明 | 可能擴充 |
|---|---|---|
| 單執行緒 | 鏈表操作非執行緒安全 | 加入 mutex 保護鏈表,或使用 thread-local 儲存 |
| 不支援 placement new | 範例中 new(buff) Object 繞過偵測 |
多載 operator new(size_t, void*) 標記為不追蹤 |
| 僅追蹤全域 new/delete | 類別專屬 operator new 不會被攔截 |
要求類別繼承基底類別統一多載,或使用巨集強制導向全域 new |
| 無呼叫堆疊 | 只記錄直接呼叫 new 的位置 | 結合 backtrace() (Linux) 或 CaptureStackBackTrace (Windows) 記錄完整堆疊 |
#define NEW new)。不到 100 行程式碼,即可建立一個功能完整的記憶體洩漏偵測器。理解其標記+鏈表+多載 new/delete的核心模式,不僅能立即解決洩漏問題,更能深化對 C++ 記憶體管理機制的掌握。
