乍一看,在C++中動態分配內存很簡單:new是分配,delete是釋放,就這么簡單。然而,這篇文章講得要復雜一點,并且要考慮到自定義層次。這也許對簡單的程序并不重要,但對你在代碼中控制內存卻是十分必要的,是否能寫一個自定義的分配器,某種高級內存管理表或一個特定的垃圾回收機制。
這篇文章并不是一個綜合的手冊,而是一個C++中各種內存分配方法的概述。它面向已經很熟悉C++語言的讀者。
原生operator new
我們先從原生operator new開始??紤]如下代碼,它用來分配5個int型的空間并返回指向他們的指針[1]:
int* v = static_cast<int*>(::operator new(5 * sizeof(*v)));
當像如上的調用,operator new扮演原生的內存分配角色,類似malloc。上面等價于:
int* v = static_cast<int*>(malloc(5 * sizeof(*v)));
釋放用operator new分配的內存用operator delete:
::operator delete(v);
你愿意永遠用原生new和delete函數嗎?是,只在極少數不用,我在下面的文章中會論證的。為什么用它們而不用原來的可信的malloc和free呢?一個很充分的原因就是你想保持代碼在C++領域的完整性?;旌鲜褂胣ew和free(或malloc和delete)是很不可取的(big NO NO)。用new和delete的另一個原因是你可以重載(overload)或重寫(override)這些函數,只要你需要。下面是個例子:
void* operator new(size_t sz) throw (std::bad_alloc)
{
cerr << "allocating " << sz << " bytesn";
void* mem = malloc(sz);
if (mem)
return mem;
else
throw std::bad_alloc();
}
void operator delete(void* ptr) throw()
{
cerr << "deallocating at " << ptr << endl;
free(ptr);
}
通常,注意到new被用來給內置類型,不包含用戶自定義new函數的類的對象,和任意類型的數組分配空間,使用的都是全局的運算符new。當new被用來為已經被重定義new的類實例化時,用的就是那個類的new函數。
下面來看下帶new函數的類。
特定類的operator new
大家有時很好奇"operator new"和"new operator"的區別。前者可以是一個重載的operator new,全局的或者特定類或者原生的operator new。后者是你經常用來分配內存的C++內置的new operator,就像:
Car* mycar = new Car;
C++支持操作符重載,并且我們可以重載的其中一個就是new。
下面是個例子:
class Base
{
public:
void* operator new(size_t sz)
{
cerr << "new " << sz << " bytesn";
return ::operator new(sz);
}
void operator delete(void* p)
{
cerr << "deleten";
::operator delete(p);
}
private:
int m_data;
};
class Derived : public Base
{
private:
int m_derived_data;
vector<int> z, y, x, w;
};
int main()
{
Base* b = new Base;
delete b;
Derived* d = new Derived;
delete d;
return 0;
}
打印結果:
new 4 bytes
delete
new 56 bytes
delete
在基類被重載的operator new和operator delete也同樣被子類繼承。如你所見,operator new得到了兩個類的正確大小。注意實際分配內存時使用了::operator new,這是前面所描述過的原生new。在調用前面的兩個冒號很關鍵,是為了避免進行無限遞歸(沒有它函數將一直調用自己下去)。
為什么你要為一個類重載operator new?這里有許多理由。
性能:默認的內存分配算符被設計成通用的。有時你想分配給一個非常特殊的對象,通過自定義分配方式可以明顯地提高內存管理。許多書和文章都討論了這種情況。尤其是"Modern C++ Design"的第4章展示了一個為較小的對象的非常好的設計并實現了自定義的分配算符。
調試 & 統計:完全掌握內存的分配和釋放為調試提供了很好的靈活性,統計信息和性能分析。你可將你的分配算符插入進專門用來探測緩沖區溢出的守衛,通過分配算符和釋放算符(deallocations)的比較來檢測內存泄漏,為統計和性能分析積累各種指標,等等。
個性化:對于非標準的內存分配方式。一個很好的例子是內存池或arenas,它們都使得內存管理變得更簡單。另一個例子是某個對象的完善的垃圾回收系統,可以通過為一個類或整個層面寫你自己的operators new和delete。
研究在C++中new運算符是很有幫助的。分配是分兩步進行:
1. 首先,用全局operator new指導系統請求原生內存。
2. 一旦請求內存被分配,一個新的對象就在其中開始構造。
The C++ FAQ給出一個很好的例子,我很愿意在這里這出來:
當你寫下這段代碼:
Foo* p = new Foo();
編譯器會生成類似這種功能的代碼:
Foo* p;
// don't catch exceptions thrown by the allocator itself
//不用捕捉分配器自己拋出的異常
void* raw = operator new(sizeof(Foo));
// catch any exceptions thrown by the ctor
//捕捉ctor拋出的任何異常
try {
p = new(raw) Foo(); // call the ctor with raw as this 像這樣用raw調用ctor分配內存
}
catch (...) {
// oops, ctor threw an exception 啊哦,ctor拋出了異常
operator delete(raw);
throw; // rethrow the ctor's exception 重新拋出ctor的異常
}
其中在try中很有趣的一段語法被稱為"placement new",我們馬上就會討論到。為了使討論完整,我們來看下用delete來釋放一個對象時一個相似的情況,它也是分兩步進行:
1.首先,將要被刪除對象的析構函數被調用。
2.然后,被對象占用的內存通過全局operator delete函數返還給系統。
所以:
delete p;
等價于[2]:
if (p != NULL) {
p->~Foo();
operator delete(p);
}
這時正適合我重復這篇文章第一段提到的,如果一個類有它自己的operator new或 operator delete,這些函數將被調用,而不是調用全局的函數來分配和收回內存。
Placement new
現在,回來我們上面看到樣例代碼中的"placement new"問題。它恰好真的能用在C++代碼中的語法。首先,我想簡單地解釋它如何工作。然后,我們將看到它在什么時候有用。
直接調用 placement new會跳過對象分配的第一步。也就是說我們不會向操作系統請求內存。而是告訴它有一塊內存用來構造對象[3]。下面的代碼表明了這點:
int main(int argc, const char* argv[])
{
// A "normal" allocation. Asks the OS for memory, so we
// don't actually know where this ends up pointing.
//一個正常的分配。向操作系統請求內存,所以我們并不知道它指向哪里
int* iptr = new int;
cerr << "Addr of iptr = " << iptr << endl;
// Create a buffer large enough to hold an integer, and
// note its address.
//創建一塊足夠大的緩沖區來保存一個整型,請注意它的地址
char mem[sizeof(int)];
cerr << "Addr of mem = " << (void*) mem << endl;
// Construct the new integer inside the buffer 'mem'.
// The address is going to be mem's.
//在緩沖區mem中構造新的整型,地址將變成mem的地址
int* iptr2 = new (mem) int;
cerr << "Addr of iptr2 = " << iptr2 << endl;
return 0;
}
在我的機器上輸出如下:
Addr of iptr = 0x8679008
Addr of mem = 0xbfdd73d8
Addr of iptr2 = 0xbfdd73d8
如你所見,placement new的結構很簡單。而有趣的問題是,為什么我需要用這種東西?以下顯示了placement new在一些場景確實很有用:
· 自定義非侵入式內存管理。當為一個類重載 operator new 同時也允許自定義內存管理,這里關鍵概念是非侵入式。重載一個類的 operator new需要你改變一個類的源代碼。但假設我們有一個類的代碼不想或者不能更改。我們如何仍能控制它的分配呢? Placement new就是答案。這種用 Placement new達到這個目的的通用編程技術叫做內存池,有時候也叫arenas[4]。
· 在一些程序中,在指定內存區域的分配對象是很必要的。一個例子是共享內存。另一個例子是嵌入式程序或使用內存映射的周邊驅動程序,這些都可以很方便地在它們的“領地”分配對象。
· 許多容器庫預先分配很大一塊內存空間。當一個對象被添加,它們就必須在這里構造,因此就用上了placement new。典型的例子就是標準vector容器。
刪除用placement new 分配的對象
一條C++箴言就是一個用new創建的對象應該用delete來釋放。這個對placement new 同樣適用嗎?不完全是:
int main(int argc, const char* argv[])
{
char mem[sizeof(int)];
int* iptr2 = new (mem) int;
delete iptr2; // Whoops, segmentation fault! 嗚啊,段錯誤啦!
return 0;
}
為了理解上面代碼片段為什么delete iptr2會引起段錯誤(或某種內存異常,這個因操作系統而異),讓我們回想下delete iptr2實際干了什么:
1. First, the destructor of the object that's being deleted is called.
首先,調用將要被刪除的對象的析構函數。
2. Then, the memory occupied by the object is returned to the OS, represented by the global operator delete function.
然后,這個對象在操作系統中占用的內存用全局operator delete函數收回。
對于用placement new分配的對象,第一步是沒有問題的,但第二步就可疑了。嘗試釋放一段沒有被分配算符實際分配的內存就不對了,但上面的代碼確實這么做了。iptr2指向了一段并沒有用全局operator new分配的棧中的一段位置。然而,delete iptr2將嘗試用全局operator delete來釋放內存。當然會段錯誤啦。
那么我們應該怎么辦?我們應該怎樣正確地刪除iptr2?當然,我們肯定不會認為編譯器怎么會解決怎么翻譯內存,畢竟,我們只是傳了一個指針給placement new,那個指針可能是從棧里拿,從內存池里或者別的地方。所以必須手動根據實際情況來釋放。
事實上,上面的placement new用法只是C++的new指定額外參數的廣義placement new語法的一種特例。它在標準頭文件中定義如下:
inline void* operator new(std::size_t, void* __p) throw()
{
return __p;
}
C++一個對應的帶有相同參數的delete也被找到,它用來釋放一個對象。它在頭文件中定義如下:
inline void operator delete (void*, void*) throw()
{
}
的確,C++運行并不知道怎么釋放一個對象,所以delete函數沒有操作。
怎么析構呢?對于一個int,并不真的需要一個析構函數,但假設代碼是這樣的:
char mem[sizeof(Foo)];
Foo* fooptr = new (mem) Foo;
對于某個有意義的類Foo。我們一旦不需要fooptr了,應該怎么析構它呢?我們必須顯式調用它的析構函數:
fooptr->~Foo();
對,顯式調用析構函數在C++中是合法的,并且這也是唯一一種正確的做法[5]。
結論
這是一個復雜的主題,并且這篇文章只起到一個介紹的作用,對C++的多種內存分配方法給出了一種“嘗鮮”。一旦你研究一些細節會發現還有許多有趣的編程技巧(例如,實現一個內存池分配)。這些問題最好是在有上下文的情況下提出,而不是作為一個普通的介紹性文章的一部分。如果你想知道得更多,請查閱下面的資源列表。
資源
· C++ FAQ Lite, especially items 11.14 and 16.9
· "The C++ Programming Language, 3rd edition" by Bjarne Stroustrup – 10.4.11
· "Effective C++, 3rd edition" by Scott Myers – item 52
· "Modern C++ Design" by Andrei Alexandrescu – chapter 4
· Several StackOverflow discussions. Start with this one and browse as long as your patience lasts.
|
我仍會在operator |
[2] |
注意到這里是檢查是否為NULL。這樣做使delete |
[3] |
對傳給placement |
[4] |
內存池本身是一個很大且迷人的話題。我并不打算在這里擴展,所以我鼓勵你自己上網找些信息,WIKI如往常一樣是個好地方(good |
[5] |
事實上,標準的vector容器用這種方法去析構它保存的數據。 |