萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 網路程式設計 >> 程式語言綜合

C++ 輕量級記憶體洩漏偵測機制實作(單執行緒適用)

記憶體洩漏是 C/C++ 開發的常見痛點。本文介紹一個簡易、可直接嵌入專案的記憶體洩漏自動偵測機制:利用雙向鏈表串聯所有動態配置的記憶體區塊,在程式結束時自動報告未釋放區塊的檔案名、行號、大小,快速定位洩漏來源。

核心設計

  • 全域標記結構:每個配置區塊前置 MEMORY_TAG,記錄大小、來源檔案、行號、前後指標。
  • 全域鏈表頭:g_header 串聯所有存活區塊。
  • 多載 operator new/delete:攔截全域 new/delete,自動維護鏈表。
  • 巨集 NEW:#define NEW new(__FILE__, __LINE__) 自動帶入檔案/行號。
  • 程式結束檢查:atexit 或手動呼叫 DumpUnAllocatedMem() 列印洩漏報告。

關鍵程式碼片段

1. 記憶體標記結構與全域鏈表

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}; // 哨兵節點

2. 多載 operator new(含檔案/行號)

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__);
}

3. 多載 operator delete

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); }

4. 洩漏報告與統計函數

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)。
  • 大型專案建議整合 Valgrind (Linux)、Dr. Memory / Visual Studio CRT Debug Heap (Windows)、AddressSanitizer (ASan) 等專業工具,功能更完整(偵測越界、使用已釋放記憶體、執行緒競爭等)。
  • 本機制適合快速原型、嵌入式、無法引入重型工具的環境作為第一道防線。

結語

不到 100 行程式碼,即可建立一個功能完整的記憶體洩漏偵測器。理解其標記+鏈表+多載 new/delete的核心模式,不僅能立即解決洩漏問題,更能深化對 C++ 記憶體管理機制的掌握。

記憶體洩漏偵測機制示意圖

copyright © 萬盛學電腦網 all rights reserved