More Effective C++ 條款8 (轉)

worldblog發表於2007-12-09
More Effective C++ 條款8 (轉)[@more@]

 

條款8:理解各種不同含義的new和delete

人們有時好像喜歡故意使C++語言的術語難以理解。比如說new運算子(new operator)和operator new的區別。:namespace prefix = o ns = "urn:schemas--com::office" />

當你寫這樣的程式碼:

string *ps = new string("Memory Management");

你使用的new是new運算子。這個運算子就象sizeof一樣是語言內建的,你不能改變它的含義,它的功能總是一樣的。它要完成的功能分成兩部分。第一部分是分配足夠的以便容納所需型別的。第二部分是它構造初始化記憶體中的物件。new運算子總是做這兩件事情,你不能以任何方式改變它的行為。

你所能改變的是如何為物件分配記憶體。new運算子呼叫一個函式來完成必需的記憶體分配,你能夠重寫或過載這個函式來改變它的行為。new運算子為分配記憶體所呼叫函式的名字是operator new。

函式operator new 通常這樣宣告:

void * operator new(size_t size);

返回值型別是void*,因為這個函式返回一個未經處理(raw)的指標,未初始化的記憶體。(如果你喜歡,你能寫一種operator new函式,在返回一個指標之前能夠初始化記憶體以一些數值,但是一般不這麼做。)引數size_t確定分配多少記憶體。你能增加額外的引數過載函式operator new,但是第一個引數型別必須是size_t。(有關operator new更多的資訊參見Effective C++ 條款8至條款10。)

你一般不會直接呼叫operator new,但是一旦這麼做,你可以象呼叫其它函式一樣呼叫它:

void *rawMemory = operator new(sizeof(string));

運算子operator new將返回一個指標,指向一塊足夠容納一個string型別物件的記憶體。

就象malloc一樣,operator new的職責只是分配記憶體。它對建構函式一無所知。operator new所瞭解的是記憶體分配。把operator new 返回的未經處理的指標傳遞給一個物件是new運算子的工作。當你的遇見這樣的語句:

string *ps = new string("Memory Management");

它生成的程式碼或多或少與下面的程式碼相似(更多的細節見Effective C++條款8和條款10,還有我的文章Counting 裡的註釋。):

void *memory =  // 得到未經處理的記憶體

  operator new(sizeof(string));  // 為String物件

call string::string("Memory Management")  //初始化

on *memory;  // 記憶體中

   // 的物件

string *ps =  // 是ps指標指向

  static_cast(memory);  // 新的物件

注意第二步包含了建構函式的呼叫,你做為一個員被禁止這樣去做。你的編譯器則沒有這個,它可以做它想做的一切。因此如果你想建立一個堆物件就必須用new運算子,不能直接呼叫建構函式來初始化物件。

Placement new

有時你確實想直接呼叫建構函式。在一個已存在的物件上呼叫建構函式是沒有意義的,因為建構函式用來初始化物件,而一個物件僅僅能在給它初值時被初始化一次。但是有時你有一些已經被分配但是尚未處理的的(raw)記憶體,你需要在這些記憶體中構造一個物件。你可以使用一個特殊的operator new ,它被稱為placement new。

下面的例子是placement new如何使用,考慮一下:

class Widget {

public:

  Widget(int widgetSize);

  ...

};

Widget * constructWidgetInBuffer(void *buffer,

  int widgetSize)

{

  return new (buffer) Widget(widgetSize);

}

這個函式返回一個指標,指向一個Widget物件,物件在轉遞給函式的buffer裡分配。當程式使用共享記憶體或memory-mapped I/O時這個函式可能有用,因為在這樣程式裡物件必須被放置在一個確定地址上或一塊被例程分配的記憶體裡。(參見條款4,一個如何使用placement new的一個不同例子。)

constructWidgetInBuffer裡面返回的是

new (buffer) Widget(widgetSize)

這初看上去有些陌生,但是它是new運算子的一個用法,需要使用一個額外的變數(buffer),當new運算子隱含呼叫operator new函式時,把這個變數傳遞給它。被呼叫的operator new函式除了待有強制的引數size_t外,還必須接受void*指標引數,指向構造物件佔用的記憶體空間。這個operator new就是placement new,它看上去象這樣:

void * operator new(size_t, void *location)

{

  return location;

}

這可能比你期望的要簡單,但是這就是placement new需要做的事情。畢竟operator new的目的是為物件分配記憶體然後返回指向該記憶體的指標。在使用placement new的情況下,呼叫者已經獲得了指向記憶體的指標,因為呼叫者知道物件應該放在哪裡。placement new必須做的就是返回轉遞給它的指標。(沒有用的(但是強制的)引數size_t沒有名字,以防止編譯器發出警告說它沒有被使用;見條款6。) placement new是標準C++庫的一部分(見Effective C++ 條款49)。為了使用placement new,你必須使用語句#include (或者如果你的編譯器還不支援這新風格的頭名(再參見Effective C++ 條款49),)。

讓我們從placement new回來片刻,看看new運算子(new operator)與operator new的關係,你想在堆上建立一個物件,應該用new運算子。它既分配記憶體又為物件呼叫建構函式。如果你僅僅想分配記憶體,就應該呼叫operator new函式;它不會呼叫建構函式。如果你想定製自己的在堆物件被建立時的記憶體分配過程,你應該寫你自己的operator new函式,然後使用new運算子,new運算子會呼叫你定製的operator new。如果你想在一塊已經獲得指標的記憶體裡建立一個物件,應該用placement new。

(有關更多的不同的new與delete的觀點參見Effective C++ 條款7和我的文章Counting objects。)

Deletion and Memory Deallocation

為了避免記憶體洩漏,每個動態記憶體分配必須與一個等同相反的deallocation對應。函式operator delete與delete運算子的關係與operator new與new運算子的關係一樣。當你看到這些程式碼:

string *ps;

...

delete ps;  // 使用delete 運算子

 

你的編譯器會生成程式碼來析構物件並釋放物件佔有的記憶體。

Operator delete用來釋放記憶體,它被這樣宣告:

void operator delete(void *memoryToBeDeallocated);

因此,

delete ps;

導致編譯器生成類似於這樣的程式碼:

ps->~string();  // call the object's dtor

operator delete(ps);  // deallocate the memory

  // the object occupied

這有一個隱含的意思是如果你只想處理未被初始化的記憶體,你應該繞過new和delete運算子,而呼叫operator new 獲得記憶體和operator delete釋放記憶體給:

void *buffer =  // 分配足夠的

  operator new(50*sizeof(char));  // 記憶體以容納50個char

  //沒有呼叫建構函式

...

operator delete(buffer);  // 釋放記憶體

  // 沒有呼叫解構函式

這與在C中呼叫malloc和free等同。

如果你用placement new在記憶體中建立物件,你應該避免在該記憶體中用delete運算子。因為delete運算子呼叫operator delete來釋放記憶體,但是包含物件的記憶體最初不是被operator new分配的,placement new只是返回轉遞給它的指標。誰知道這個指標來自何方?而你應該顯式呼叫物件的解構函式來解除建構函式的影響:

// 在共享記憶體中分配和釋放記憶體的函式

void * mallocShared(size_t size);

void freeShared(void *memory);

void *sharedMemory = mallocShared(sizeof(Widget));

Widget *pw =  // 如上所示,

  constructWidgetInBuffer(sharedMemory, 10);  // 使用

  // placement new

...

delete pw;  // 結果不確定! 共享記憶體來自

  // mallocShared, 而不是operator new

pw->~Widget();  // 正確。 析構 pw指向的Widget,

  // 但是沒有釋放

  //包含Widget的記憶體

freeShared(pw);  // 正確。 釋放pw指向的共享記憶體

  // 但是沒有呼叫解構函式

 

如上例所示,如果傳遞給placement new的raw記憶體是自己動態分配的(透過一些不常用的方法),如果你希望避免記憶體洩漏,你必須釋放它。(參見我的文章Counting objects裡面關於placement delete的註釋。)

Arrays

到目前為止一切順利,但是還得接著走。到目前為止我們所測試的都是一次建立一個物件。怎樣分配陣列?會發生什麼?

string *ps = new string[10];  // allocate an array of

  // objects

被使用的new仍然是new運算子,但是建立陣列時new運算子的行為與單個物件建立有少許不同。第一是記憶體不再用operator new分配,代替以等同的陣列分配函式,叫做operator new[](經常被稱為array new)。它與operator new一樣能被過載。這就允許你控制陣列的記憶體分配,就象你能控制單個物件記憶體分配一樣(但是有一些限制性說明,參見Effective C++ 條款8)。

(operator new[]對於C++來說是一個比較新的東西,所以你的編譯器可能不支援它。如果它不支援,無論在陣列中的物件型別是什麼,全域性operator new將被用來給每個陣列分配記憶體。在這樣的編譯器下定製陣列記憶體分配是困難的,因為它需要重寫全域性operator new。這可不是一個能輕易接受的任務。預設情況下,全域性operator new處理程式中所有的動態記憶體分配,所以它行為的任何改變都將有深入和普遍的影響。而且全域性operator new有一個正常的簽名(normal signature)(也就單一的引數size_t,參見Effective C++條款9),所以如果你  決定用自己的方法宣告它,你立刻使你的程式與其它庫不相容(參見條款27)基於這些考慮,在缺乏operator new[]支援的編譯器裡為陣列定製記憶體管理不是一個合理的設計。)

第二個不同是new運算子呼叫建構函式的數量。對於陣列,在陣列裡的每一個物件的建構函式都必須被呼叫:

string *ps =  // 呼叫operator new[]為10個

  new string[10];  // string物件分配記憶體,

  // 然後對每個陣列元素呼叫

  // string物件的預設建構函式。

同樣當delete運算子用於陣列時,它為每個陣列元素呼叫解構函式,然後呼叫operator delete來釋放記憶體。

就象你能替換或過載operator delete一樣,你也替換或過載operator delete[]。在它們過載的方法上有一些限制。請參考優秀的C++教材。(有關優秀的C++教材的資訊,參見本書285頁的推薦)

new和delete運算子是內建的,其行為不受你的控制,凡是它們呼叫的記憶體分配和釋放函式則可以控制。當你想定製new和delete運算子的行為時,請記住你不能真的做到這一點。你只能改變它們為完成它們的功能所採取的方法,而它們所完成的功能則被語言固定下來,不能改變。(You can modify how they do what they do, but what they do is fixed by the language)


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990310/,如需轉載,請註明出處,否則將追究法律責任。

相關文章