More effective c++ 條款10(上) (轉)

gugu99發表於2008-07-25
More effective c++ 條款10(上) (轉)[@more@]

條款10:在構造中防止資源洩漏(上):namespace prefix = o ns = "urn:schemas--com::office" />

如果你正在開發一個具有多功能的通訊錄。這個通訊錄除了能通常的文字資訊如姓名、地址、電話號碼外,還能儲存照片和(可以給出他們名字的正確發音)。

為了實現這個通訊錄,你可以這樣設計:

class Image {  // 用於影像資料


public:


  Image(const string& imageDataFileName);


  ...


};


 


class AudioCl{   // 用於聲音資料


public:


  AudioClip(const string& audioDataFileName);


  ...


};


 


class PhoneNumber {  ... };  // 用於儲存電話號碼


 


 


class BookEntry {  // 通訊錄中的條目


public:


 


BookEntry(const string& name,


  const string& address = "",


 const string& imageFileName = "",


  const string& audioClipFileName = "");


~BookEntry();


 


// 透過這個函式加入電話號碼


void addPhoneNumber(const PhoneNumber& number);


...


 


private:


  string theName;  // 人的姓名


  string theAddress;   // 他們的地址


  list thePhones;  // 他的電話號碼


  Image *theImage;  // 他們的影像


  AudioClip *theAudioClip;  // 他們的一段聲音片段


};


通訊錄的每個條目都有姓名資料,所以你需要帶有引數的建構函式(參見條款3),不過其它內容(地址、影像和聲音的名)都是可選的。注意應該使用連結串列類(list)儲存電話號碼,這個類是標準C++類庫(STL)中的一個容器類(container classes)。(參見Effective C++條款49 和本書條款35)

編寫BookEntry 建構函式和解構函式,有一個簡單的方法是:

BookEntry::BookEntry(const string& name,


  const string& address,


  const string& imageFileName,


  Const string& audioClipFileName)


: theName(name), theAddress(address),


  theImage(0), theAudioClip(0)


{


  if (imageFileName != "") {


  theImage = new Image(imageFileName);


  }


 


if (audioClipFileName != "") {


theAudioClip = new AudioClip(audioClipFileName);


  }


}


 


BookEntry::~BookEntry()


{


  delete theImage;


  delete theAudioClip;


}


建構函式把指標theImage和theAudioClip初始化為空,然後如果其對應的建構函式引數不是空,就讓這些指標指向真實的。解構函式負責刪除這些指標,確保BookEntry物件不會發生資源洩漏。因為C++確保刪除空指標是的,所以BookEntry的解構函式在刪除指標前不需要檢測這些指標是否指向了某些物件。

看上去好像一切良好,在正常情況下確實不錯,但是在非正常情況下(例如在有異常發生的情況下)它們恐怕就不會良好了。

請想一下如果BookEntry的建構函式正在中,一個異常被丟擲,會發生什麼情況呢?:

if (audioClipFileName != "") {


  theAudioClip = new AudioClip(audioClipFileName);


}


一個異常被丟擲,可以是因為operator new(參見條款8)不能給AudioClip分配足夠的,也可以因為AudioClip的建構函式自己丟擲一個異常。不論什麼原因,如果在BookEntry建構函式內丟擲異常,這個異常將傳遞到建立BookEntry物件的地方(在建構函式體的外面。  譯者注)。

現在假設建立theAudioClip物件建立時,一個異常被丟擲(而且傳遞程式控制權到BookEntry建構函式的外面),那麼誰來負責刪除theImage已經指向的物件呢?答案顯然應該是由BookEntry來做,但是這個想當然的答案是錯的。BookEntry根本不會被,永遠不會。

C++僅僅能刪除被完全構造的物件(fully contructed s), 只有一個物件的建構函式完全執行完畢,這個物件才能被完全地構造。所以如果一個BookEntry物件b做為區域性物件建立,如下:

void testBookEntryClass()


{


  BookEntry b("Addison-Wesley Publishing Company",


  "One Jacob Way, Reading, MA 01867");


 


...


 


}


並且在構造b的過程中,一個異常被丟擲,b的解構函式不會被呼叫。而且如果你試圖採取主動手段處理異常情況,即當異常發生時呼叫delete,如下所示:

void testBookEntryClass()


{


  BookEntry *pb = 0;


 


  try {


  pb = new BookEntry("Addison-Wesley Publishing Company",


  "One Jacob Way, Reading, MA 01867");


  ...


  }


  catch (...) {  // 捕獲所有異常


 


  delete pb;  // 刪除pb,當丟擲異常時


 


  throw;  // 傳遞異常給呼叫者


  }


 


  delete pb;  // 正常刪除pb


}


你會發現在BookEntry建構函式里為Image分配的記憶體仍舊被丟失了,這是因為如果new操作沒有成功完成,程式不會對pb進行賦值操作。如果BookEntry的建構函式丟擲一個異常,pb將是一個空值,所以在catch塊中刪除它除了讓你自己感覺良好以外沒有任何作用。用靈巧指標(smart pointer)類auto_ptr參見條款9代替raw BookEntry*也不會也什麼作用因為new操作成功完成前,也沒有對pb進行賦值操作。

C++拒絕為沒有完成構造操作的物件呼叫解構函式是有一些原因的,而不是故意為你製造困難。原因是:在很多情況下這麼做是沒有意義的,甚至是有害的。如果為沒有完成構造操作的物件呼叫解構函式,解構函式如何去做呢?僅有的辦法是在每個物件里加入一些位元組來指示建構函式執行了多少步?然後讓解構函式檢測這些位元組並判斷該執行哪些操作。這樣的記錄會減慢解構函式的執行速度,並使得物件的尺寸變大。C++避免了這種開銷,但是代價是不能自動地刪除被部分構造的物件。(類似這種在程式行為與這間進行折衷處理的例子還可以參見Effective C++條款13)

因為當物件在構造中丟擲異常後C++不負責清除物件,所以你必須重新設計你的建構函式以讓它們自己清除。經常用的方法是捕獲所有的異常,然後執行一些清除程式碼,最後再重新丟擲異常讓它繼續轉遞。如下所示,在BookEntry建構函式中使用這個方法:

BookEntry::BookEntry(const string& name,


     const string& address,


     const string& imageFileName,


       const string& audioClipFileName)


: theName(name), theAddress(address),


  theImage(0), theAudioClip(0)


{


  try {  // 這try block是新加入的


  if (imageFileName != "") {


  theImage = new Image(imageFileName);


  }


 


  if (audioClipFileName != "") {


  theAudioClip = new AudioClip(audioClipFileName);


  }


  }


  catch (...) {  // 捕獲所有異常


 


 


  delete theImage;  // 完成必要的清除程式碼


  delete theAudioClip;


 


 


  throw;   // 繼續傳遞異常


  }


}

不用為BookEntry中的非指標資料成員操心,在類的建構函式被呼叫之前資料成員就被自動地初始化。所以如果BookEntry建構函式體開始執行,物件的theName, theAddressthePhones資料成員已經被完全構造好了。這些資料可以被看做是完全構造的物件,所以它們將被自動釋放,不用你介入操作。當然如果這些物件的建構函式呼叫可能會丟擲異常的函式,那麼哪些建構函式必須去考慮捕獲異常,在允許它們繼續傳遞之前完成必需的清除操作。 

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

相關文章