C/C++ 最常被詬病的缺點,莫過於缺乏標準的內存垃圾回收機制。本文將探討如何在 C/C++ 中實現一個極其精簡、功能受限但極具實用價值的垃圾回收器 —— AutoFreeAlloc。它的核心特點在於:
演算法本身並不複雜,技術關鍵一經點破便豁然開朗,其價值或許在於首創性。Boost 庫中的 pool 組件雖也嘗試提供類似的自動內存回收能力,但實現相對複雜且效率較低(基於經典的 mempool 技術)。
理解 AutoFreeAlloc 的關鍵在於明確其目標:為複雜的局部過程(演算法)提供自動內存回收能力。
所謂「局部過程」,指那些演算法複雜度高、但運行時間較短暫的過程,例如:搜尋引擎的查詢過程、讀寫盤過程、繪圖渲染過程等。這類過程通常需要大量申請內存,且分配入口分散(到處調用 new),若逐一考慮 delete 位置,極大消耗腦力,分散了對核心演算法設計的精力。這正是 C/C++ 程式設計師最羨慕擁有 GC 語言的時刻。反之,若演算法不複雜,程式設計師完全能駕馭 new/delete 的配對,且「一切盡在掌握」能帶來安全感與成就感。
因此,AutoFreeAlloc 並不追求理論上功能完備的通用 GC,而是專注於高複雜度的局部過程,提供最務實的內存管理手段:從過程開始,只管申請、使用內存;待演算法結束,該過程申請的絕大多數內存(保留作為結果的例外),無論在哪個步驟申請,均在結束點由垃圾回收器自動銷毀。示意圖如下:

圖 1:局部過程內存自動回收示意
AutoFreeAlloc 僅暴露兩個核心概念:Alloc(分配)與 Clear(清理)。
typedef void (*FnDestructor)(void* pThis);
class AutoFreeAlloc {
public:
~AutoFreeAlloc(); // 析構函數,自動調用 Clear 釋放內存
void* Alloc(size_t cb); // 類似 malloc(cb)
void* Alloc(size_t cb, FnDestructor fn); // 申請內存並指定析構函數
void Clear(); // 析構並釋放所有分配的對象
};
為便於使用,另提供輔助的 New 操作(上一篇已簡介實現):
template <class Type, class AllocType>
Type* New(AllocType& alloc); // 類似 new Type
template <class Type, class ArgType1, class AllocType>
Type* New(ArgType1 arg1, AllocType& alloc); // 類似 new Type(arg1)
template <class Type, class AllocType>
Type* NewArray(size_t count, AllocType& alloc); // 類似 new Type[count]
使用範例:
AutoFreeAlloc alloc;
int* intArray = (int*)alloc.Alloc(sizeof(int)*count);
int* intArray2 = NewArray<int>(count, alloc);
MyClass* obj = New<MyClass>(alloc);
MyClass* objWithArg = New<MyClass>(arg1, alloc);
MyClass* objArray = NewArray<MyClass>(count, alloc);
// ... 演算法邏輯 ...
// 此處無需手動 delete,alloc 析構或 Clear 會自動回收所有內存
// 現在,不能再訪問 intArray, obj, objWithArg, objArray 等數據了。
AutoFreeAlloc 的內存管理變量只有兩個:m_begin、m_end。單看變量定義難以理解,結合示意圖即清晰:

圖 2:_MemBlock 鏈表結構
所有申請的內存通過 _MemBlock 串成鏈表。鏈表頭(_ChainHeader)可由 m_begin 計算得出:
_MemBlock* AutoFreeAlloc::_ChainHeader() const {
return (_MemBlock*)(m_begin - HeaderSize);
}
為使鏈表頭初始為 null,構造函數寫法如下:
AutoFreeAlloc::AutoFreeAlloc() {
m_begin = m_end = (char*)HeaderSize;
}
void* AutoFreeAlloc::Alloc(size_t cb) {
if (m_end - m_begin < cb) {
if (cb >= BlockSize) {
_MemBlock* pHeader = _ChainHeader();
_MemBlock* pNew = (_MemBlock*)m_alloc.allocate(HeaderSize + cb);
if (pHeader) {
pNew->pPrev = pHeader->pPrev;
pHeader->pPrev = pNew;
} else {
m_end = m_begin = pNew->buffer;
pNew->pPrev = NULL;
}
return pNew->buffer;
} else {
_MemBlock* pNew = (_MemBlock*)malloc(sizeof(_MemBlock));
pNew->pPrev = _ChainHeader();
m_begin = pNew->buffer;
m_end = m_begin + BlockSize;
}
}
return m_end -= cb;
}
m_end 前移 cb 位元組。

m_begin/m_end 不變(當前塊不變)。
void AutoFreeAlloc::Clear() {
_MemBlock* pHeader = _ChainHeader();
while (pHeader) {
_MemBlock* pTemp = pHeader->pPrev;
free(pHeader);
pHeader = pTemp;
}
m_begin = m_end = (char*)HeaderSize;
}
C++ 的構造/析構機制保證了對象生命週期內數據的自洽性。垃圾回收器除釋放內存外,必須確保對象銷毀前調用析構函數。為此增加成員:
class AutoFreeAlloc {
struct _DestroyNode {
_DestroyNode* pPrev;
FnDestructor fnDestroy;
};
_DestroyNode* m_destroyChain;
};
需析構的對象在分配時指定析構函數:
void* AutoFreeAlloc::Alloc(size_t cb, FnDestructor fn) {
_DestroyNode* pNode = (_DestroyNode*)Alloc(sizeof(_DestroyNode) + cb);
pNode->fnDestroy = fn;
pNode->pPrev = m_destroyChain;
m_destroyChain = pNode;
return pNode + 1; // 返回用戶數據區指針
}
Clear 中補充析構調用:
void AutoFreeAlloc::Clear() {
while (m_destroyChain) {
m_destroyChain->fnDestroy(m_destroyChain + 1);
m_destroyChain = m_destroyChain->pPrev;
}
// 接著釋放內存塊鏈表...
}
假設對象平均 32 位元組,每 2048/32 = 64 次分配中,僅 1 次需申請新塊(滿足 m_end - m_begin < cb)。因此 98.4% 的情況下,Alloc 僅需一次減法運算。Clear 則按塊釋放,相當於將 64 次 Alloc 合併為 1 次 Free。綜合來看,AutoFreeAlloc 時間性能約為普通 malloc/free 的 64 倍。

傳統分配器為管理每個塊需額外頭部結構(如指針+大小),假設平均 32 位元組,額外開銷 8 位元組,浪費 25%,且產生大量碎片。AutoFreeAlloc 僅在鏈表節點有一個 pPrev 指針,加上 malloc 自身 8 位元組開銷,總浪費 (4+8)/2048 = 0.6%,幾乎可忽略。
AutoFreeAlloc 於 2004 年開發,僅 100 行代碼,卻獲得空前成功,應用範圍遠超預期。其成功不在於極致性能,而在於解決了 C++ 程式設計師最大的痛點 —— 內存管理,雖方案不完整。
它啟示了 C++ new/delete 的不合理;STL allocator 則揭示了內存管理的強區域性 —— 不同局部過程對分配器需求不同。例如文檔編輯過程在 AutoFreeAlloc 視角非局部過程,但換個 allocator 角度或可視為局部過程?
正是針對 AutoFreeAlloc 的缺陷,我們需要更強大的垃圾回收器,那將是下一篇討論的主題。最終結論:我們無需也難以實現如 Java/C# 的通用 GC,提供具備特定內存管理能力的 allocator 才是正道。
完整源碼可見 WINX 庫:https://sourceforge.net/projects/winx。註:後來納入 stdext 庫,函數名改為 allocate/clear 以符合 STL 風格。