C++ Gotchas 條款62:替換Global New和Global Delete (轉)

worldblog發表於2007-12-14
C++ Gotchas 條款62:替換Global New和Global Delete (轉)[@more@]

Gotcha #62: Replacing Global New and Delete:namespace prefix = o ns = "urn:schemas--com::office" />

條款62:替換Global New和Global Delete

 

將operator new、operator delete、array new亦或array delete的標準global版本替換為自定製版本,這幾乎從來都不是個好主意——即使C++標準允許你這麼做。這些的標準版本一般都針對通用目的(general-purpose)之管理做了極大,而自定義的替代版本則不大會做得更好了。(然而,針對特定的類別或類別階層體系採用(自定製的)成員函式形式的操作來定製其管理,則通常是合理的。)

 

如果operator new和operator delete針對特定目的之實現版本作出了與標準版本相異的行為,其就可能引入臭蟲,因為許多標準庫和第三方程式庫的正確性皆依賴於這些函式預設的標準實現版本。

 

比較的方案是對global版本的operator new等函式進行過載,而不是替代它們。假設我們要以特定的字元樣式(character pattern)填充新分配的儲存空間:

 

void *operator new( size_t n, const string &pat ) {

  char *p = static_cast(::operator new( n ));

  const char *pattern = pat.c_str();

  if( !pattern || !pattern[0] )

  pattern = ""; // note: two null chars

  const char *f = pattern;

  for( int i = 0; i < n; ++i ) {

  if( !*f )

  f = pattern;

  p[i] = *f++;

  }

  return p;

}

 

該operator new版本接收一個字串樣式作為引數,並將其複製到新分配的儲存空間中。經由過載解析,就可以區分標準operator new與我們自己的“接收兩個引數之版本”。

 

string fill( "" );

string *string1 = new string( "Hello" ); // 標準版本

string *string2 =

  new (fill) string( "World!" ); // 過載的版本

 

標準中還定義了一個過載的operator new版本;除了以size_t作為第一引數之外,該版本還接收一個void*型別作為第二引數。該實現只是簡單的返回第二引數。(其中的throw()語法是一個exception-specification(異常規範),意味該函式不會傳播出任何異常。在後述討論及一般情況下,都可以安然忽略之。)

 

void *operator new( size_t, void *p ) throw()

{ return p; }

 

這就是標準的placement new,用於在特定的位置空間建構一個。(其不同之處在於,標準的“單引數operator new”可以被替換,而試圖替換placement new則是的。)本質上來說,我們會將其用於“讓編譯器誤以為了一個建構函式”的場合。比如說,對於一個嵌入式應用,我們或許想在某個特定的地址上建構一個“status register(狀態暫存器)”物件:

 

class StatusRegister {

// . . .

};

void *regAddr = reinterpret_cast(0XFE0000);

// . . .

// 在regAddr的位置放一個register

StatusRegister *sr = new (regAddr) StatusRegister;

 

自然,經由placement new建立的物件必須在某個時刻被銷燬。然而,由於placement new並未真正的分配記憶體(譯註:其只是在指定位置放入物件,並未進行記憶體分配),因此也必須保證在銷燬時沒有記憶體被刪除。回憶一下,delete operator的行為是:在呼叫operator delete函式(以便歸還儲存空間)之前,首先喚起“欲刪除物件”之解構函式。對於“物件是經由placement new進行‘空間分配’”的情形,為了避免任何嘗試歸還記憶體空間的動作,我們在銷燬物件時必須對解構函式進行顯式的(explicit)呼叫(譯註:這正是delete operator所做的第一步操作,第二步“呼叫operator delete函式”的操作就不用去做了)。

 

sr->~StatusRegister(); // 顯式的呼叫dtor, 不呼叫operator delete函式

 

Placement new和explicit destruction(顯式析構操作)顯然是非常有用的特性,但倘若不保守並謹慎的使用它們,顯然也是非常危險的。(詳見Gotcha條款47中一個來自標準程式庫的例子。)

 

應注意,當我們過載operator delete時,這些過載版本絕不會被“使用標準delete形式的”喚起。

 

void *operator new( size_t n, Buffer &buffer ); // 過載版本的new

void operator delete( void *p,

  Buffer &buffer ); // 對應的過載版本之delete

// . . .

Thing *thing1 = new Thing; // 使用標準的operator new

Buffer buf;

Thing *thing2 = new (buf) Thing; // 使用過載版本的operator new

delete thing2; // 不對, 應該使用過載版本的delete

delete thing1; // 正確, 使用標準的operator delete

 

相應的,對於經由placement new建立的物件,我們不得不顯式的(explicitly)呼叫該物件的解構函式,然後直接明瞭的呼叫適當的operator delete函式,以便顯式的將物件的儲存空間進行去配:

 

thing2->~Thing(); // 正確, 銷燬Thing

operator delete( thing2, buf ); // 正確, 使用過載版本的delete

 

實際當中,經由“global operator new之過載版本”分配的儲存空間經常錯誤的經由“global operator new之標準版本”被去配。一個避免這種錯誤的方法是保證:任何經由“global operator new之過載版本”分配的儲存空間都是經由“global operator new之標準版本”來獲取儲存空間(譯註:意即,在“global operator new之過載版本”的實現中,透過呼叫“global operator new之標準版本”來獲取空間,詳見本條款開頭的示例)。這正是前述第一個過載實現版本(譯註:指的正是本條款開頭那個“以特定的字元樣式(character pattern)填充新分配的儲存空間”的例子)所用的方法,其能與“global operator delete之標準版本”相配合並運作正常:

 

string fill( "" );

string *string2 = new (fill) string( "World!" );

// . . .

delete string2; // 運作正常!

 

一般來說,global operator new的過載版本要麼就不分配任何儲存空間,要麼就應該只簡單的包裹(wrap)global operator new的標準版本(譯註:如本條款開頭那個例子所示,過載版本是標準版本的一個wrapper)。

 

通常情況下,最好的方案是全然避免對“處於global pe的記憶體管理operator functions”做手腳,代之以“operator new、operator delete、array new、array delete的成員函式版本”來定製類別或類別階層體系的記憶體管理操作。

 

在Gotcha條款61的結尾我們提到過,若從new表示式中的初始化操作傳出一個異常,執行期會喚起一個“適當的”operator delete函式:

 

Thing *tp = new Thing( arg );

 

如果Thing的分配動作成功了但建構函式丟擲異常,那麼執行期系統將會喚起一個適當的operator delete函式來歸還tp所指向的未經初始化的記憶體。在上例中,這個“適當的operator delete”要麼是global版本的operator delete(void*),要麼就是一個具有相同形式的成員函式版本。然而不同的operator new即意味著不同的operator delete:

 

Thing *tp = new (buf) Thing( arg );

 

此時,適當的operator delete應該是“雙引數版本”operator delete(void*,Buffer&),與Thing分配操作所使用的“operator new之過載版本”相對應;這正是執行期系統會喚起的版本。

 

C++在定義記憶體管理的行為方面給予了頗大的彈性,伴之以複雜性作為代價。標準的“global operator new”和“global operator delete”便足以滿足多數需求。因此,我們應該僅在確實需要的情況下才採用更復雜的方案。


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

相關文章