C與C++中的異常處理5 (轉)

worldblog發表於2007-12-12
C與C++中的異常處理5 (轉)[@more@]

:namespace prefix = o ns = "urn:schemas--com::office" />

  今天,我們開始學習C++的new和delete操作時的異常處理。首先,我將介紹標準C++執行庫對new和delete操作的支援。然後,介紹伴隨著這些支援的異常。

1.1  New和Delete

  當寫

B *p = new D;

這裡,B和D是class型別,並且有構造和析構,實際產生的程式碼大約是這樣的:

B *p = operator new(sizeof(D));

D::D(p);

  過程是:

l  new操作接受D的大小(位元組為單位)作為引數。

l  new操作返回一塊大小足以容納一個D物件的的地址。

l  D的預設建構函式被。這個建構函式傳入的this指標就是剛剛返回的記憶體地址。

l  最終結果:*p是個完整構造了的物件,靜態型別是B,動態型別是D。

 

  相似的,語句

delete p;

差不多被編譯為

D::~D(p);

operator delete(p);

  D的解構函式被呼叫,被傳入的this指標是p;然後delete操作釋放被分配的記憶體。

  new操作和delete操作其實是函式。如果你沒有提供自己的版本,編譯器會使用標準C++執行庫頭中申明的版本:

void *operator new(std::size_t);

void operator delete(void *);

  和其它標準執行庫函式不同,它們不在名稱空間std內。

  因為編譯器隱含地呼叫這些函式,所以它必須知道如何尋找它們。如果編譯器將它們放在特別的空間內(如名稱空間std),你就無法申明自己的替代版本了。因此,編譯器按絕對名字從裡向外進行搜尋。如果你沒有申明自己的版本,編譯器最終將找到在中申明的全域性版本。

  這個標頭檔案包含了8個new/delete函式:

//

// new and delete

//

void *operator new(std::size_t);

void delete(void *);

//

// array new and delete

//

void *operator new[](std::size_t);

void delete[](void *);

//

// placement new and delete

//

void *operator new(std::size_t, void *);

void operator delete[](void *, void *);

//

// placement array new and delete

//

void *operator new[](std::size_t, void *);

void operator delete[](void *, void *);

 

  前兩個我已經介紹了。接下來兩個分配和釋放陣列物件,而最後四個根本不分配和釋放任何東西!

1.2  陣列new和陣列delete

  new[]操作被這樣的表示式隱含呼叫:

B *p = new D[N];

編譯器對此的實現是:

B *p = operator new[](sizeof(D) * N + _v);

for (std::size_t _i(0); _i < N; ++_i)

  D::D(&p[_i]);

 

  前一個例子分配和構造單個D物件,這個例子分配和構造一個有N個D物件的陣列。注意,傳給new[]操作的位元組大小是sizeof(D)*N + _v,所有物件的總大小加_v。在這裡, _v是陣列分配時的額外開銷。

  如你所想,

delete[] p;

實現為:

for (std::size_t _i(_N_of(p)); _i > 0; --_i)

  D::~D(&p[i-1]);

operator delete[](p);

 

  這裡,_N_of(p)是個假想詞,它依賴於你的編譯器在檢測*p中的元素個數時的實現體系。

  和p = new D[N]不同(它明確說明了*p包含N個元素),delete[] p沒有在編譯期明確說明*p元素個數。你的必須在執行期推算元素個數。C++標準沒有強制規定推算的實現體系,而我所見過的編譯器共有兩種實現方法:

l  在*p前面的位元組中儲存元素個數。其空間來自於new[]操作時_v位元組的額外開銷。

l  由標準執行庫維護一個私有的N對p的對映表。

1.3  Placement New 和 Placement Delete

  關鍵字new可以接受引數:

p = new(arg1, arg2, arg3) D;

  (C++標準稱這樣的表示式為 “new with placement”或“placement new”,我馬上會簡單地解釋原因。)這些引數會被隱含地傳給new操作函式:

p = operator new(sizeof(D), arg1, arg2, arg3);

  注意,第一個引數仍然是要生成物件的位元組數,其它引數總是跟在它後面。

  標準執行庫定義了一個new操作的特別過載版本,它接受一個額外引數:

void *operator new(std::size_t, void *);

  這種形式的new操作被如下的語句隱含呼叫:

p = new(addr) D;

這裡,addr是某些資料區的地址,並且型別相容於void *。

  addr傳給這個特別的new操作,這個特別的new操作和其它new操作一樣返回將被構造的記憶體的地址,但不需要在自由記憶體區中再申請記憶體,它直接將addr返回:

void *operator new(std::size_t, void *addr)

  {

  return addr;

  }

 

這個返回值然後被傳給D::D作建構函式的this指標。

  就這樣,表示式

p = new(addr) D;

在addr所指的記憶體上構造了一個D物件,並將p賦為addr的值。這個方法讓你有效地指定新生成物件的位置,所以被叫作“placement new”。

  這個new的額外引數形式最初被設計為控制物件的位置的,但是C++標準委員會認識到這樣的傳參體系可以被用於任意用途而不僅是控制物件的位置。不幸的是,術語“placement”已經被根據最初目的而制訂,並適用於所有new操作的額外引數的形式,即使它們根本不試圖控制物件的位置。

  所以,下面每個表示式都是placement new的一個例子:

new(addr) D;  // calls operator new(std::size_t, void *)

new(addr, 3) D; // calls operator new(std::size_t, void *, int)

new(3) D;  // calls operator new(std::size_t, int)

即使只有第一個形式是一般被用作控制物件位置的。

1.4  placement Delete

  現在,只要認為 placement delete 是有用處的就行了。我肯定會講述理由的,可能就在接下來的兩篇內。

  Placement new操作和placement delete操作必須成對出現。一般來說,每一個

void *operator new(std::size_t, p1, p2, p3, ..., pN);

都對應一個

void operator delete(void *, p1, p2, p3, ..., pN);

  根據這條原則,標準執行庫定義了

void operator delete(void *, void *);

以對應我剛講的placement new操作。

1.5  陣列New和陣列Delete

  基於對稱,標準執行庫也申明瞭placement new[]操作和placement delete[]操作:

void *operator new[](std::size_t, void *);

void operator delete[](void *, void *);

  如你所料:placement new[]操作返回傳入的地址,而placement delete[]操作的行為和我沒有細述的placement delete操作行為幾乎一樣。

1.6  異常

  現在,我們把這些new/delete和異常結合起來。再次考慮這條語句:

B *p = new D;

當其呼叫new操作而沒有分配到足夠記憶體時將發生什麼?

  在C++的黑暗年代(1994年及以前),對大部分編譯器而言,new操作將返回NULL。這曾經是對C的malloc函式的合理擴充套件。幸運的是,我們現在生活在光明的年代,編譯器強大了,類被設計得很漂亮,而編譯執行庫的new操作會拋異常了。

  前面,我展示了在中出現的8個函式的申明。那時,我做了些小手腳;這裡是它們的完整形式:

namespace std

   {

  class bad_alloc

  {

  // ...

  };

  }

 

//

// new and delete

//

void *operator new(std::size_t) throw(std::bad_alloc);

void operator delete(void *) throw();

//

// array new and delete

//

void *operator new[](std::size_t) throw(std::bad_alloc);

void operator delete[](void *) throw();

//

// placement new and delete

//

void *operator new(std::size_t, void *) throw();

void operator delete(void *, void *) throw();

//

// placement array new and delete

//

void *operator new[](std::size_t, void *) throw();

void operator delete[](void *, void *) throw();

 

  在這些new操作族中,只有非placement形式的會拋異常(std::bad_alloc)。這個異常意味著記憶體耗盡狀態,或其它記憶體分配失敗。你可能奇怪為什麼placement形式不拋異常;但記住,這些函式實際上根本不分配任何記憶體,所以它們沒有分配問題可報告。

  沒有delete操作拋異常。這不奇怪,因為delete不分配新記憶體,只是將舊記憶體還回去。

1.7  異常消除

  相對於會拋異常的new操作形式,中也申明瞭不拋異常的過載版本:

namespace std

  {

  struct nothrow_t

  {

  // ...

  };

  extern const nothrow_t nothrow;

  }

 

//

// new and delete

//

void *operator new(std::size_t, std::nothrow_t const &) throw();

void operator delete(void *, std::nothrow_t const &) throw();

//

// array new and delete

//

void *operator new[](std::size_t, std::nothrow_t const &) throw();

void operator delete[](void *, std::nothrow_t const &) throw();

 

  這幾個函式也被認為是new操作和delete操作的placement形式,因為它們也接收額外引數。和前面的控制物件分配位置的版本不同,這幾個只是讓你分辨出拋異常的new和不拋異常的new。

#include

#include

using namespace std;

 

int main()

  {

  int *p;

  //

  // 'new' that can throw

  //

  try

  {

  p = new int;

  }

  catch(bad_alloc &)

  {

  cout << "'new' threw an exception";

  }

  //

  // 'new' that can't throw

  //

  try

  {

  p = new(nothrow) int;

  }

  catch(bad_alloc &)

  {

  cout << "this line should never appear";

  }

  //

  return 0;

  }

 

  注意兩個new表示式的重要不同之處:

p = new int;

在分配失敗時拋std::bad_alloc,而

p = new(nothrow) int;

在分配失敗時不拋異常,它返回NULL(就象malloc和C++黑暗年代的new)。

  如果你不喜歡nothrow的語法,或你的編譯器不支援,你可以這樣達到同樣效果:

#include

 

//

// function template emulating 'new(std::nothrow)'

//

template

T *new_nothrow() throw()

  {

  T *p;

  try

  {

  p = new T;

  }

  catch(std::bad_alloc &)

  {

  p = NULL;

  }

  return p;

  }

 

//

// example usage

//

int main()

  {

  int *p = new_nothrow(); // equivalent to 'new(nothrow) int'

  return 0;

  }

 

  這個模板函式與它效仿的new(nothrow)表示式同有一個潛在的異常。現在,我將它作為習題留給你去找出來。(恐怕沒什麼用的提示:和placement delete有關。)

1.8  小結

  new和delete是怪獸。和typeid一起,它們是C++中僅有的會呼叫標準執行庫中函式的關鍵字。即使程式除了main外不明確呼叫或定義任何函式,new和delete語句的出現就會使程式呼叫執行庫。如我在這兒所示範的,呼叫執行庫將經常可能拋異常或處理異常。

  本篇的例程中的程式碼和註釋是用於我對C++標準的解釋的。不幸的是,如我以前所說,Microsoft的Visual C++經常不遵守C++標準。在下一篇中,我將揭示Visual C++的執行庫對new和delete的支援在什麼地方背離了C++標準。我將特別注意在對異常的支援上的背離,並且將展示怎麼繞過它們。


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

相關文章