operator new在C++中的各种写法
乍一看,在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++領域的完整性。混合使用new和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.
?
?
| [1] | 我仍會在operator?new前面顯式地寫::(雙冒號),雖然這里并不是必須的。恕我直言,這是一個很好的做法,特別當在重載operator?new的類中,可以避免二義性。 |
| [2] | 注意到這里是檢查是否為NULL。這樣做使delete?p?很安全,即使p是NULL。 |
| [3] | 對傳給placement?new的指針確保有足夠的內存分配給對象,并且確保它們正確地對齊,這都是你的應該做的。 |
| [4] | 內存池本身是一個很大且迷人的話題。我并不打算在這里擴展,所以我鼓勵你自己上網找些信息,WIKI如往常一樣是個好地方(good?start)。 |
| [5] | 事實上,標準的vector容器用這種方法去析構它保存的數據。 |
總結
以上是生活随笔為你收集整理的operator new在C++中的各种写法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入C++的new
- 下一篇: C++中placement new操作符