引言
C/C++語言的內存管理經歷了幾次變革,但至今仍未能趨於成熟。這幾次變革主要包括:
1. 從malloc/free到new/delete。這場變革是OOP技術興起的產物。C++是強類型語言,new/delete的主要成果也就是加強了類型觀念,減少了強制類型轉換的需求。但是從內存管理角度看,這個變革並沒有多少的突破性。
2. 從new/delete到內存配置器(allocator)。自從STL被納入C++標准庫後,C++世界產生了巨大的變化。而從內存管理角度來看,allocator的引入也是C++內存管理一個突破。留意一下你就可以發現,整個STL所有組件的內存均從allocator分配。也就是說,STL並不推薦使用new/delete進行內存管理,而是推薦使用allocator。
然而,STL的allocator並沒有導致C++語言在內存管理上發生巨大的變化。除了STL本身外,並沒有多少人使用allocator,甚至是意識到allocator的重要性。所以C++程序員在使用STL的同時,依舊在使用new/delete進行煩瑣的內存分配/釋放過程。
究其原因,主要有二。一是allocator的引入,STL設計者主要可能還是出於將內存管理從容器的實現獨立出來的設計理念作用,讓STL使用者在內存管理算法上有選擇的余地。設計者本身都可能也沒有意識到allocator的重要性。二是allocator本身也只是側重於關注效率上,而沒有側重於C++語言使用者對內存管理觀念的變革上。
總之,在我看來,STL的引入allocator,是一件了不起的事情。但是這場變革被忽視了,沒有得到貫徹。當然,這也與STL的allocator本身的缺陷有關。
本文要討論的,正是如何貫徹STL的allocator思想,對其進行適當的改進,以期在C++內存管理觀念上產生變革性的突破,徹底淘汰傳統的new/delete內存管理方法。
垃圾回收器
幾乎所有目前流行的垃圾回收器,均傾向於將使用者當作一個傻瓜,期望能夠讓使用者在完全不理解內存管理的情況下,可以很好的使用它。應該說這它們基本上都也做到了(雖然使用者有時也有這樣那樣的煩惱,但總體來說情況確實得到了很大程度的改善)。然而這一設計理念我並不十分認同。
首先,可以在一個提供垃圾回收器的語言中自如的工作,沒有被垃圾回收器所困擾,本身已經是很了不起的事情,他們絕對是非常聰明的人,而不是傻瓜。他們理解垃圾回收器的工作原理,選擇它並且讓它為他們工作,只是因為還有更重要的事情等著他們去做。必要的時候,他們需要有辦法控制垃圾回收器,使它按照他們的意願工作。因此,垃圾回收器的設計要點在於把使用者從煩瑣的內存管理中解脫出來,使得他們可以將全部精力投入到本身的業務邏輯上,而不是讓垃圾回收器看起來更傻瓜式。
其次,使用一個全自動的垃圾回收器,在內存回收的時機不明確的情況下,垃圾回收器的工作過程有很大的不確定性,這給使用者帶來煩惱。例如C#在調用非管制代碼(如調用Win32 api)時,這些問題變得突出。一個不小心,就有可能出現Win32 api還在使用一塊內存,而垃圾回收器已經把它回收了的情形。在小心翼翼的避開這些陷阱時,這種感覺其實與C/C++程序員遺憾語言沒有垃圾回收器的感覺有點類似。
因此,最理想的情況,是內存管理器提供垃圾回收的能力,但是它也只是提供這個能力而已,至於什麼時候進行垃圾回收,完全可以由用戶自己控制。另外,用戶也可以強制釋放一塊內存,而不是完全被動的等待垃圾回收過程決策何時回收該內存。對於客戶來說,他有權掌控一切,只是如果萬一他確實疏忽了,垃圾回收器能夠為他護航。
將垃圾回收器引入C++,有沒有這種可能呢?我認為,如果我們試圖提供一個全自動的垃圾回收器,這相當困難。我們看到以Microsoft之能,仍然無法把這件事做好。或許,我們需要改變一下觀念:一個半自動的垃圾回收器,也許就可能可以和C++融洽相處了呢?
初識allocator
allacator中文稱為“內存配置器”,通常它是一個類,負責提供內存管理(可能包含內存分配、釋放、自動回收等能力)相關的服務。例如,我們通過C提供的malloc/free即刻提供一個allocator實作出來:
class SimpleAlloc
{
public:
//注意這裡提供的參數fnDestroy,它是為那些具備垃圾回收能力的allocator需要提供。
void* Alloc(size_t cb, FnDestructor fnDestroy = NULL)
{
return malloc(cb);
}
//注意這裡有看似多余的參數cb,這完全是為了和後續提供的allocator規格一致的需要。
void Free(void* data, size_t cb)
{
free(data);
}
};
有了allocator,我們可以申請內存了,但是我們還不能用它創建一個C++對象。為了方便創建C++對象,我們提供了輔助的New操作,原型大體如下:
template
Type* New(AllocType& alloc); // 類似於new Type
template
Type* New(ArgType1 arg1, AllocType& alloc); // 類似於new Type(arg1)
template
Type* NewArray(size_t count, AllocType& alloc);// 類似於new Type[count]
有了這些輔助函數,我們就可以創建對象了。使用樣例:
SimpleAlloc alloc;
int* intArray = NewArray(count, alloc);
MyClass* obj = New(alloc);
MyClass* objWithArg = New(arg1, alloc);
MyClass* objArray = NewArray(count, alloc);
這裡我們雖然使用SimpleAlloc創建對象,但是需要提醒的是,這些New操作對所有的allocator有效。如果你關心New函數的代碼,先不急,下面我們馬上就可以看到了。但是首先我們要繼續討論一下allocator。
allocator引起的觀念變化
接觸allocator,你可以體會到了它與C++傳統的new/delete觀念的不同。這主要有以下幾點:
1. 每個類(或者算法)本身,均有最合適它的內存管理機制,並不是向C++傳統的做法那樣,使用一個全局的new/delete。也許你會說,C++不也允許一個類定義自己的new和delete嗎?是的,C++的確支持類定義自己的new/delete,但注意,它的理念和allocator完全不同。我不認為它是C++的一個優秀之作,相反,它起到了誤導作用。
因為,決定一個類對象怎麼去new出來,並不是取決於該類本身,而相反是取決於使用該類的人。一個類不需要關心自身被如何創造出來,更不能假定。它需要關心的是它自己的類成員如何被創建出來,它的算法(你可以把類看做一個算法集合)涉及到的所有組件如何被創建出來。而這,才是allocator帶來的觀念。
讓各種各樣的allocator創建同一個類的不同實例,這些實例甚至可能在一起工作,相互協作。從STL的角度講,這完全是最正常不過的事情了。
2. 重要的是由allocator創建管理對象,避免在你的代碼中使用new/delete。如果可能,你可以如STL那樣,將allocator作為模板參數,不綁定具體的某個內存管理器。但是,如果你的算法依賴了某個allocator的實現特有的功能,這也並不要緊。你的目的不是要做到allocator的可替換,不是嗎?重要的是使用了這個allocator了,它給你在內存管理上帶來了益處。
但是,應該看到,STL實作的各種allocator,目前來看除了最簡單使用malloc/free實現的外,主要就是基於mempool技術。而該技術的目標,不是讓內存使用者更加方便有效地進行內存管理,而更多的是關注於內存分配的時間性能。為了讓C++程序員從內存管理中解脫出來,我們需要實作新的alloctor,需要新的突破!
新視角:具垃圾回收能力的Allocator
對,我設想的一個做法是,貫徹STL的allocator觀念,並且提供具備特定的內存管理能力(例如垃圾回收)的各種allocator。讓C++社區廣泛接受allocator觀念,並且從中受益。C++程序員是時候拋棄傳統的new/delete,讓他們退出歷史舞台了。
我接下來會實作兩個具體的allocator(均屬原創)。相信它們會讓你耳目一新,讓你不禁想到:哦,原來在C++中,我還可以這樣進行內存管理。
當然,我最大的希望就是,這兩個allocator能夠起到拋磚引玉的作用,讓大家也清楚地意識到allocator的重要性,可以出現更多的具備各種能力的allocator,解脫C++程序員一直以來的苦難(可能是最大苦難)。
這兩個allocator均具備一定程度的垃圾回收能力。只是觀念上各有各的側重。我們接下來會分為兩個專題專門對它們進行闡述。
輔助的New過程
我們終於可以開始討論前文提到的New函數的實現上了。以不帶參數的New為例,它的代碼如下,可能並沒有你想象的那麼復雜:
#include
template
inline Type* New(AllocType& alloc)
{
void* obj = alloc.Alloc(sizeof(Type), DestructorTraits::Destruct);
return new(obj) Type;
}
其中DestructorTraits是一個根據類型Type萃取[4]析構函數的萃取器。它看起來是這樣的:
template
struct DestructorTraits
{
static void Destruct(void* pThis)
{
((Type*)pThis)->~Type();
}
};
這樣,你就可以通過以下代碼new出對象了:
MyClassA* obj = New(alloc);
MyClassB* obj = New(alloc);
特別提醒:這裡New函數在VC++ 6.0下編譯通過,但是產生的執行代碼存在嚴重bug。如果你只New一類對象,沒有問題,但在New了多種對象後,似乎VC++對MyClassA、MyClassB 兩者混淆起來了。為了支持VC++ 6.0,你需要對這裡的New做出調整。
COM技術與內存管理
已經准備結束這篇短文的時候,忽然想到了長久以來使用COM技術形成的一些感想,這些想法恰恰與內存管理緊密相關。故此想就這個問題陳述一下。
從COM的IUnknown接口看,它主要關注兩個問題:一個是QueryInterface,一個是引用計數(AddRef/Release)。COM組件很講究信息的屏蔽,使用者對組件的認識有限,這就給組件升級、擴充功能提供了可能。QueryInterface是一個很好的概念,需要發揚光大。
COM的引用計數則關注的是組件的生命期維護問題。換句話說,就是組件如何銷毀的問題。誠然,組件對象的銷毀問題,是內存管理的關鍵。無論是COM的引用計數,還是垃圾回收技術,均是要解決對象的銷毀問題。只是兩者的側重點不太一樣,COM引用計數更關注“確保組件不會被提前銷毀了,確保組件訪問的安全性”,而垃圾回收器則關注“不管怎樣確保組件最終被銷毀,沒有內存洩漏”。
在COM中,確保組件訪問的安全性(避免非法訪問),這個觀點太重要了,以至於它甚至不惜加重程序員的內存管理負擔。所以,在COM程序中,出現內存洩漏太正常了,而且一旦洩漏通常就是大片大片內存的漏。更加要命的是,你甚至不能有一個很簡單有效的方法確認這個洩漏是由於哪段代碼引起。因為組件所有的客戶都是平等的,任何一個客戶代碼存在問題均將導致內存的洩漏。
剛開始接觸COM技術的時候,我對引用計數持的是比較正面的態度。但是隨著部門逐步加大COM技術的使用力度後,四五年下來,我漸漸開始迷惑起來。一切並不如想象的那樣。這個引用計數的背後,需要我們付出多少額外的代價!
而這個迷惑、思索,可能就是本文以及後續相關內容的成因吧。