有時候我們覺得,C++的術語仿佛是要故意讓人難以理解似的。
這里就有一個例子:請說明new operator 和operator new 之間的差異(譯注:本書所說的new operator,即某些C++教程如C++ Primer 所謂的new expression)
當你寫出這樣的代碼:
string *ps= new string("Memory Management");
你所使用的 new 是所謂的new operator。
這個操作符是由語言內建的,就像sizeof那樣,不能被改變意義,總是做相同的事情。
它的動作分為兩方面。
- 第一,它分配足夠的內存,用來放督某類型的對象。以上例而言,它分配足夠放置一個string 對象的內存。
- 第二,它調用一個constructor,為剛才分配的內存中的那個對象設定初值。
new operator總是做這兩件事,無論如何你不能夠改變其行為。
你能夠改變的是用來容納對象的那塊內存的分配行為。也就是上面的第一步
new operator 調用某個函數,執行必要的內存分配動作,你可以重寫或重載那個函數,改變其行為。這個函數的名稱叫做operator new。頭昏了嗎?真的,我說的是真的。
函數operator new 通常聲明如下:
void * operator new(size_t size);
其返回值類型是void*。此函數返回一個指針,指向一塊原始的、未設初值的內存(如果你喜歡,可以寫一個新版的operator new,在其返回內存指針之前先將那塊內存設定初值。只不過這種行為頗為罕見就是了)。
函數中的size_t 參數表示需要分配多少內存。
你可以將operator new 重載,加上額外的參數,但第一參數的類型必須總是 size_t(如何撰寫 operator new,相關信息請參考條款E8~E10)
吸中協從不想到要直接調用operator new,但如果你要,你可以像調用任何其他函數一樣地調用它:
void *rawMemory = operator new(sizeof(string));
這里的operator new將返回指針,指向一塊足夠容納一個string對象的內存。
和malloc一樣,operator new的唯一任務就是分配內存。它不知道什么是constructors, operator new只負責內存分配。
取得 operator new 返回的內存并將之轉換為一個對象,是new operator 的責任。
當你的編譯器看到這樣一個句子:
string *ps= new string("Memory Management");
它必須產生一些代碼,或多或少會反映以下行為(見條款E8和條款E10,以及發表于C/C++ Users Journal, April1998 的文章《Counting Objects inC++》中的方塊內容)
void *memory?=
operator new(sizeof(string));//取得原始內存(raw memory)。用來放置一個string對象。call string::string("Memory Management")//將內存中的對象初始化。
on *memorystring *ps=
static_cast<string*>(memory);//讓ps指向新完成的對象。
注意上述第二步驟涉及“調用一個constructor”,身為程序員的你沒有權力這么做。
然而你的編譯器百無禁忌,可以為所欲為。這就是為什么如果你想要做出一個heap-based object,一定得使用new operator 的原因:你無法直接調用“對象初始化所必需的constructor”
(尤其它可能得為重要成分vtbl設定初值,見條款24)。
Placement new
有時候你真的會想直接調用一個constructor。
針對一個已存在的對象調用其constructor 并無意義,因為 constructors 用來將對象初始化,而對象只能被初始化一次。
但是偶爾你會有一些分配好的原始內存,你需要在上面構建對象。有一個特殊版本的operator new,稱為placement new,允許你那么做。
下面示范如何使用 placement new:
class Widget {
public:
widget(int widgetsize);
//...
}widget * constructWidgetInBuffer (void *buffer, int widgetSize)
{
return new (buffer) Widget(widgetsize);
}
此函數返回指針,指向一個widget object,它被構造于傳遞給此函數的一塊內存緩沖區上。
當程序運行到shared memory 或memory-mapped I/O,這類函數可能是有用的,因為在那樣的運用中,對象必須置于特定地址,或是置于以特殊函數分配出來的內存上(條款4列有placement new的另一個運用實例)
在 constructWidgetInBuffer 函數內部,唯一一個表達式是,
new (buffer) Widget(widgetSize)
乍見之下有點奇怪,其實不足為奇,這只是new operator 的用法之一,其中指定一個額外自變量(buffer)作為 new operator “隱式調用operator new”時所用。
于是,被調用的operator new除了接受“一定得有的size_t 自變量”之外,還接受了一個void*參數,指向一塊內存,準備用來接受構造好的對象。這樣的operator new 就是所謂的placement new,看起來像這樣:
void * operator new (size_t,void *location)//注意size_t后面沒名字
{
return location;
}
似乎比你預期得更簡單,但這便是placement new必須做的一切。
畢竟operator new的目的是要為對象找到一塊內存,然后返回一個指針指向它。
在placement new的情況下,調用者已經知道指向內存的指針了,因為調用者知道對象應該放在哪里。因此placement new 唯一需要做的就是將它獲得的指針再返回。
至于沒有用到(但一定得有)的size_t參數,之所以不賦予名稱,為的是避免編譯器發出“某物未被使用”的警告(見條款6)Placement new 是C++標準程序庫(見條款E49)的一部分。
欲使用 placement new,你必須用#include <new>。如果你的編譯器尚未支持新式頭文件名稱的話(見條款E49),就用#include<new.h>。
花幾分鐘回頭想想 placement new,我們便能了解 new operator 和 operator new之間的關系,兩個術語雖然表面上令人迷惑,概念上卻十分直接易懂。
- 如果你希望將對象產生于heap,請使用 new operator。它不但分配內存而且為該對象調用一個constructor。
- 如果你只是打算分配內存,請調用 operator new,那就沒有任何constructor會被調用。
- 如果你打算在heap objects 產生時自己決定內存分配方式,請寫一個自己的 operator new,并使用 new operator,它將會自動調用你所寫的operator new。
- 如果你打算在已分配(并擁有指針)的內存中構造對象,請使用placement new(若想更深入地了解new和delete,請見條款E7及發表于C/C++Users Journal, April1998的文章《Counting Objects in C++》)
刪除(Deletion)與內存釋放(Deallocation)
為了避免resource leaks(資源泄漏),每一個動態分配行為都必須匹配一個相應但相反的釋放動作。函數operator delete 對于內建的delete operator,就好像operator new對于new operator 一樣。當你寫出這樣的代碼:
string *ps;
//。。。
delete ps;// 使用 delete operator。
你的編譯器必須產生怎樣的代碼?
它必須既能夠析構 ps所指對象,又能夠釋放被該對象占用的內存。
內存釋放動作是由函數operator delete 執行,通常聲明如下:
void operator delete(void *memoryToBeDeallocated);
因此,下面這個動作:
delete ps;
會造成編譯器產生近似這樣的代碼:
ps->~string();// 調用對象的dtoroperator。operator delete (ps);//釋放對象所占用的內存。
這里呈現的一個暗示就是,如果你只打算處理原始的、未設初值的內存,應該完全回避new operator 和 delete operators,改調用operator new取得內存并以operator delete 歸還給系統:
void *buffer=operator new(50*sizeof(char));//分配足夠的內存,放置50個 chars;沒有調用任何ctorsoperator delete(buffer);//釋放內存,沒有調用任何dtors。
這組行為在C++中相當于調用malloc和free。
如果你使用placement new,在某內存塊中產生對象,你應該避免對那塊內存使用delete opcrator.因為 delete operator會調用 operator delete來釋放內存,但是該內存內含的對象最初并非是由 operator new分配得來的。
畢竟placemen new只是返回它所接收的指針而已,誰知道那個指針從哪里來呢?所以為了抵消該對象的constructor的影響,你應該直接調用該對象的destructor:
//以下函數用來分配及釋放 shared memory 中的內存。
void* mallocshared(size_t size);
void freeShared(void *memory);void*sharedMemory=mallocShared(sizeof(Widget));//和先前相同,運用Widget*pw=constructWidgetInBuffer(sharedMemory, 10); // placement new。
//..
delete pw;//無定義!因為sharedMemory 來自mallocshared,不是來自 operator new。pw->~Widget();
//可!析構 pw 所指的widget 對象,
//但并未釋放widget占用的內存。freeShared(pw);
//可!釋放 pw所指的內存,
//不調用任何 destructor。
如此例所示,如果交給placement new 的原始內存(raw memory)本身是動態分配而得(通過某種非傳統做法),那么你最終還是得釋放那塊內存,以免遭受內存泄漏(memory leak)之苦(請參考文章《Counting Objects inC++》之中的方塊內容,其中對所謂的“placement delete”有些介紹)。
數組(Arrays)
目前為止一切都好,但我們還有更遠的路要走。截至目前我們考慮的每件事情都只在單一對象身上打轉。面對數組怎么辦?下面會發生什么事情;
string *ps= new string[10);//分配一個對象數組
上述使用的new 仍然是那個new operator,但由于誕生的是數組,所以new operator的行為與先前產生單一對象的情況略有不同。
是的,內存不再以operator new分配,而是由其“數組版”兄弟,一個名為operator new[]的函數負責分配(通常被稱為“aray new”)。
和operator new一樣,operator new[]也可以被重載。這使你得奪取數組的內存分配權,就像你可以控制單一對象的內存分配一樣(不過條款E8對此有些警告)。
operatornew[]是相當晚的時候才加入C++的一個特性,所以你的編譯器或許尚未支持它。
如果是這樣,全局operator new會被用來為每個數組分配內存一不論數組中的對象類型是什么。
在這樣的編譯器下定制“數組內存分配行為”很困難,因為你得改寫全局版的 operator new才行。這可不是件容易的工作。
默認情況下全局版的operator new負責程序中所有的動態內存分配,所以其行為的任何改變都可能帶來劇烈而普遍的影響。此外,全局版本的operator new,其正規形式的型構(eignature)(我的意思是,只有唯一size_t參數的那個,見條款E9)只有一個,所以如果你決定聲稱它為你所擁有,你的軟件便立刻不容于任何做了相同決定的程序庫(見條款 27)。
多方考慮之下,如果你面對的是尚未支持 。perator new[]的編譯器,定制“數組內存管理行為”往往不是個理想的決定。
“數組版”與“單一對象版”的newoperator的第二個不同是,它所調用的constructor數量。數組版new operator 必須針對數組中的每個對象調用一個constructor:
string *ps =//調用operator new[]以分配足夠容納new string[10];//10個 string 對象的內存,然后
//針對每個元素調用 string default ctor。
同樣道理,當delete operator 被用于數組,它會針對數組中的每個元素調用其 destructor,然后再調用 operator delete[]釋放內存:
delete []?ps;
//為數組中的每個元素調用 string dtor.
然后調用 operator delete[]以釋放內存。
就好像你可以取代或重載 operator delete一樣,你也可以取代或重載operator delete[]。不過兩者的重載有著相同的限制。請你找一本好的C++教程,查閱其細節。說到好的C++教程,本書p285列有我的一份推薦名單。
現在,你有了完整的知識。
new operator 和delete operator 都是內建操作符,無法為你所控制,但是它們所調用的內存分配/釋放函數則不然。
當你想要定制new operator 和 delete operator的行為,記住,你其實無法真正辦到。你可以修改它們完成任務的方式,至于它們的任務,已經被語言規范固定死了。