More Effective C++ 條款4 (轉)

gugu99發表於2008-06-16
More Effective C++ 條款4 (轉)[@more@]

 條款4:避免無用的預設構造:namespace prefix = o ns = "urn:schemas--com::office" />

預設建構函式(指沒有引數的建構函式)在C++語言中是一種讓你無中生有的方法。建構函式能初始化,而預設建構函式則可以不利用任何在建立物件時的外部資料就能初始化物件。有時這樣的方法是不錯的。例如一些行為特性與數字相仿的物件被初始化為空值或不確定的值也是合理的,還有比如連結串列、雜湊表、圖等等資料結構也可以被初始化為空容器。

 

但不是所有的物件都屬於上述型別,對於很多物件來說,不利用外部資料進行完全的初始化是不合理的。比如一個沒有輸入姓名的地址簿物件,就沒有任何意義。在一些公司裡,所有的裝置都必須標有一個公司ID號碼,所以在建立物件以模型化一個裝置時,不提供一個合適的ID號碼,所建立的物件就根本沒有意義。

 

在一個完美的世界裡,無需任何資料即可建立物件的類可以包含預設建構函式,而需要資料來建立物件的類則不能包含預設建構函式。唉!可是我們的現實世界不是完美的,所以我們必須考慮更多的因素。特別是如果一個類沒有預設建構函式,就會存在一些使用上的限制。

 

請考慮一下有這樣一個類,它表示公司的裝置,這個類包含一個公司的ID程式碼,這個ID程式碼被強制做為建構函式的引數:

 

class EquipmentPiece {

public:

  EquipmentPiece(int IDNumber);

  ...

};

 

因為EquipmentPiece類沒有一個預設建構函式,所以在三種情況下使用它,就會遇到問題。第一中情況是建立陣列時。一般來說,沒有一種辦法能在建立物件陣列時給建構函式傳遞引數。所以在通常情況下,不可能建立EquipmentPiece物件陣列:

 

EquipmentPiece bestPieces[10];  // 錯誤!沒有正確

   // EquipmentPiece 建構函式

 EquipmentPiece *bestPieces =

  new EquipmentPiece[10];  // 錯誤!與上面的問題一樣

 

不過還是有三種方法能迴避開這個限制。對於使用非堆陣列(non-heap arrays)(即不在堆中給陣列分配。譯者注)的一種解決方法是在陣列定義時提供必要的引數:

 

int ID1, ID2, ID3, ..., ID10;  // 裝置ID號的

  // 變數

...

 

EquipmentPiece bestPieces[] = {  // 正確, 提供了構造

  EquipmentPiece(ID1),  // 函式的引數

  EquipmentPiece(ID2),

  EquipmentPiece(ID3),

  ...,

  EquipmentPiece(ID10)

};

 

不過很遺憾,這種方法不能用在堆陣列(heap arrays)的定義上。

 

一個更通用的解決方法是利用指標陣列來代替一個物件陣列:

 

typedef EquipmentPiece* PEP;  //  PEP 指標指向

  //一個EquipmentPiece物件

 

PEP bestPieces[10];  // 正確, 沒有呼叫建構函式

PEP *bestPieces = new PEP[10];  // 也正確

 

在指標陣列裡的每一個指標被重新賦值,以指向一個不同的EquipmentPiece物件:

 

for (int i = 0; i < 10; ++i)

  bestPieces[i] = new EquipmentPiece( ID Number );

 

不過這中方法有兩個缺點,第一你必須刪除陣列裡每個指標所指向的物件。如果你忘了,就會發生記憶體洩漏。第二增加了記憶體分配量,因為正如你需要空間來容納EquipmentPiece物件一樣,你也需要空間來容納指標。

 

如果你為陣列分配raw memory,你就可以避免浪費記憶體。使用placement new方法(參見條款8)在記憶體中構造EquipmentPiece物件:

 

// 為大小為10的陣列 分配足夠的記憶體

// EquipmentPiece 物件; 詳細情況請參見條款8

// operator new[] 函式

void *rawMemory =

  operator new[](10*sizeof(EquipmentPiece));

// make bestPieces point to it so it can be treated as an

// EquipmentPiece array

EquipmentPiece *bestPieces =

  static_cast(rawMemory);

// construct the EquipmentPiece s in the memory

// 使用"placement new" (參見條款8)

for (int i = 0; i < 10; ++i)

  new (&bestPieces[i]) EquipmentPiece( ID Number );

 

注意你仍舊得為每一個EquipmentPiece物件提供建構函式引數。這個技術(也稱為陣列到指標的思想array-of-pointers)允許你在沒有預設建構函式的情況下建立一個物件陣列。它沒有繞過對建構函式引數的需求,實際上也做不到。如果能做到的話,就不能保證物件被正確初始化。

使用placement new的缺點除了是大多數員對它不熟悉外(能使用它就更難了),還有就是當你不想讓它繼續存在使用時,必須手動呼叫陣列物件的解構函式,呼叫運算子delete[]來釋放 raw memory(請再參見條款8):

 

// 以與構造bestPieces物件相反的順序

// 解構它。

for (int i = 9; i >= 0; --i)

  bestPieces[i].~EquipmentPiece();

 

// deallocate the raw memory

operator delete[](rawMemory);

 

如果你忘記了這個要求或沒有用這個陣列刪除方法,那麼你程式的執行將是不可預測的。這是因為直接刪除一個不是用new運算子來分配的記憶體指標,其結果沒有被定義的。

 

delete [] bestPieces;   // 沒有定義! bestPieces

  //不是用new運算子分配的。

 

有關new、placement new和它們如何與建構函式、解構函式一起使用的更多資訊,請見條款8。

對於類裡沒有定義預設建構函式所造成的第二個問題是它們無法在許多基於模板(template-based)容器類裡使用。因為例項化一個模板時,模板的型別引數應該提供一個預設建構函式,這是一個常見的要求。這個要求總是來自於模板內部,被建立的模板引數型別陣列裡。例如一個陣列模板類:

 

template

class Array {

public:

  Array(int size);

  ...

 

private:

  T *data;

};

 

template

Array::Array(int size)

{

  data = new T[size];  // 為每個陣列元素

 ...  //依次呼叫 T::T()

}

 

在多數情況下,透過仔細設計模板可以杜絕對預設建構函式的需求。例如標準的vector模板(生成一個類似於可擴充套件陣列的類)對它的型別引數沒有必須有預設建構函式的要求。不幸的是,很多模板類沒有以仔細的態度去設計,這樣沒有預設建構函式的類就不能與許多模板相容。當C++程式設計師深入領會了模板設計以後,這樣的問題應該不再那麼突出了。這會花多長時間,完全在於個人的造化。

 

最後講一下在設計虛基類時所面臨的是要提供預設建構函式還是不提供預設建構函式的兩難決策。不提供預設建構函式的虛基類很難與其進行工作。因為幾乎所有的派生類在例項化時鬥必須給虛基類建構函式提供引數。這就要求所有從沒有預設建構函式的虛基類繼承下來的派生類(無論有多遠)都必須知道並理解提供給虛基類建構函式的引數含義。派生類的作者是不會企盼和喜歡這種規定的。

 

因為這些強加於沒有預設建構函式的類上的種種限制,一些人認為所有的類都應該有預設建構函式,即使預設建構函式沒有足夠的資料來初始化一個物件。比如這個原則的擁護者會這樣修改EquipmentPiece類:

 

class EquipmentPiece {

public:

  EquipmentPiece(  int IDNumber = UNSPECIFIED);

  ...

private:

  static const int  UNSPECIFIED;  // ID值不確定。

}; 

 

這允許這樣建立EquipmentPiece物件

 

EquipmentPiece e;  //這樣合法

 

這樣的修改使得其他成員函式變得複雜,因為不再能確保EquipmentPiece物件進行有意義的初始化。假設它建立一個因沒有ID而沒有意義的EquipmentPiece物件,那麼大多數成員函式必須檢測ID是否存在。如果不存在ID,它們將必須知道怎麼犯的錯誤。不過這經常是不明晰的,很多程式碼實現什麼也沒有提供,只是丟擲一個異常或呼叫一個函式終止程式。當這種情形發生時,很難說提供預設建構函式而放棄了一種保證機制這種做法是否能提高的總體質量。

 

提供無意義的預設建構函式也會影響類的工作。如果成員函式必須測試所有的部分是否都被正確地初始化,那麼這些函式的呼叫者就得為此付出更多的時間。而且還得付出更多的程式碼,因為這使得可或庫變得更大。它們也得在測試失敗的地方放置程式碼來處理錯誤。如果一個類的建構函式能夠確保所有的部分被正確初始化,所有這些弊病都能夠避免。預設建構函式一般不會提供這種保證,所以在它們能使類變得沒有意義時,儘量去避免使用它們。使用這種類的確有一些限制,但是當你使用它時,它也給你提供了一種保證,你能相信這個類被正確地建立和高效地實現。


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

相關文章