如果你正在開發一個具有多媒體功能的通訊錄程序。這個通訊錄除了能存儲通常的文字信息如姓名、地址、電話號碼外,還能存儲照片和聲音。
可以這樣設計:
class Image // 用於圖像數據
{
public:
Image(const string& imageDataFileName);
...
};
class AudioClip // 用於聲音數據
{
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<PhoneNumber> thePhones; // 他的電話號碼
Image *theImage; // 他們的圖像
AudioClip *theAudioClip; // 他們的一段聲音片段
};
編寫 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不能給 AudioClip
分配足夠的內存,也可以因爲 AudioClip
的構造函數自己拋出一個異常。
不論什麼原因,如果在BookEntry
構造函數內拋出異常,這個異常將傳遞到建立 BookEntry
對象的地方。
假設建立 theAudioClip
對象建立時,一個異常被拋出,那麼誰來負責刪除 theImage
已經指向的對象呢?答案顯然應該是由 BookEntry
來做,但是這個想當然的答案是錯的。~BookEntry()
根本不會被調用,永遠不會。
C++僅僅能刪除被完全構造的對象, 只有一個對象的構造函數完全運行完畢,這個對象才被完全地構造。所以如果一個 BookEntry 對象 b 做爲局部對象建立,如下:
void testBookEntryClass()
{
BookEntry b("Addison-Wesley Publishing Company",
"One Jacob Way, Reading, MA 01867");
...
}
並且在構造 b 的過程中,一個異常被拋出,b 的析構函數不會被調用。而且如果你試圖採取主動手段處理異常情況,即當異常發生時調用 delete
,如下所示:
void testBookEntryClass()
{
BookEntry *pb = nullptr;
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 進行賦值操作。
C++拒絕爲沒有完成構造操作的對象調用析構函數是有一些原因的,而不是故意爲你製造困難。
原因是:在很多情況下這麼做是沒有意義的,甚至是有害的。如果爲沒有完成構造操作的對象調用析構函數,析構函數如何去做呢?僅有的辦法是在每個對象里加入一些字節
來指示構造函數執行了多少步?然後讓析構函數檢測這些字節並判斷該執行哪些操作。這樣的記錄會減慢析構函數的運行速度,並使得對象的尺寸變大。C++避免了這種開銷,但是代價是不能自動地刪除被部分構造的對象
因爲當對象在構造中拋出異常後 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
{
if (imageFileName != "")
{
theImage = new Image(imageFileName);
}
if (audioClipFileName != "")
{
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch (...) // 捕獲所有異常
{
delete theImage; // 完成必要的清除代碼
delete theAudioClip;
throw; // 繼續傳遞異常
}
}
你可能已經注意到 BookEntry
構造函數的 catch
塊中的語句與在 BookEntry
的析構函數的語句幾乎一樣。這裏的代碼重複是絕對不可容忍的,所以最好的方法是把通用代碼移入一個私有 helper function
中,讓構造函數與析構函數都調用它。
class BookEntry
{
public:
... // 同上
private:
...
void cleanup(); // 通用清除代碼
};
void BookEntry::cleanup()
{
delete theImage;
delete theAudioClip;
}
BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName) : theName(name), theAddress(address), theImage(0), theAudioClip(0)
{
try
{
... // 同上
}
catch (...)
{
cleanup(); // 釋放資源
throw; // 傳遞異常
}
}
BookEntry::~BookEntry()
{
cleanup();
}
這似乎行了,但是它沒有考慮到下面這種情況。假設我們略微改動一下設計,讓theImage
和 theAudioClip
是常量指針類型:
class BookEntry
{
public:
... // 同上
private:
...
Image * const theImage; // 指針現在是 const 類型
AudioClip * const theAudioClip; // 指針現在是 const 類型
};
必須通過 BookEntry
構造函數的成員初始化表來初始化這樣的指針,因爲再也沒有其它地方可以給 const
指針賦值.
通常會這樣初始化theImage
和theAudioClip
:
BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName) : theName(name), theAddress(address), theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0) {}
這樣做導致我們原先一直想避免的問題重新出現:如果 theAudioClip
初始化時一個異常被拋出,theImage
所指的對象不會被釋放。而且我們不能通過在構造函數中增加 try
和catch
語句來解決問題,因爲 try
和 catch
是語句,而成員初始化表僅允許有表達式(這也是爲什麼我們必須在 theImage
和 theAudioClip
的初始化中使用?:以代替 if-then-else的原因)。
如果我們不能在成員初始化表中放入 try 和 catch 語句,我們把它們移到其它地方。一種可能是在私有成員函數中,用這些函數返回指針指向初始化過的 theImage 和 theAudioClip 對象:
class BookEntry
{
public:
... // 同上
private:
... // 數據成員同上
Image * initImage(const string& imageFileName);
AudioClip * initAudioClip(const string& audioClipFileName);
};
BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName) : theName(name), theAddress(address),
theImage(initImage(imageFileName)),theAudioClip(initAudioClip(audioClipFileName)) {}
// theImage 被首先初始化,所以即使這個初始化失敗也
// 不用擔心資源泄漏,這個函數不用進行異常處理。
Image * BookEntry::initImage(const string& imageFileName)
{
if (imageFileName != "")
return new Image(imageFileName);
else
return nullptr;
}
// theAudioClip 被第二個初始化, 所以如果在 theAudioClip
// 初始化過程中拋出異常,它必須確保 theImage 的資源被釋放。
// 因此這個函數使用 try...catch 。
AudioClip * BookEntry::initAudioClip(const string&
audioClipFileName)
{
try
{
if (audioClipFileName != "")
{
return new AudioClip(audioClipFileName);
}
else return nullptr;
}
catch (...)
{
delete theImage;
throw;
}
}
上面的程序的確不錯,也解決了令我們頭疼不已的問題。不過也有缺點,在原則上應該屬於構造函數的代碼卻分散在幾個函數裏,這令我們很難維護.
更好的解決方法是採用條款 09的建議,把 theImage
和 theAudioClip
指向的對象做爲一個資源,被一些局部對象管理。
class BookEntry
{
public:
... // 同上
private:
...
const unique_ptr<Image> theImage; // 它們現在是
const unique_ptr<AudioClip> theAudioClip; // unique_ptr 對象
};
這樣做使得 BookEntry
的構造函數即使在存在異常的情況下也能做到不泄漏資源,而且讓我們能夠使用成員初始化表來初始化 theImage
和 theAudioClip
,如下所示:
BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName) : theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName)
: nullptr), theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : nullptr){}
在這裏,如果在初始化 theAudioClip
時拋出異常,theImage
已經是一個被完全構造的對象,所以它能被自動刪除掉,就象 theName
, theAddress
和 thePhones
一樣。而且因爲theImage
和 theAudioClip
現在是包含在 BookEntry 中的對象,當 BookEntry
被刪除時它們能被自動地刪除。因此不需要手工刪除它們所指向的對象。可以這樣簡化 BookEntry
的析構函數:
BookEntry::~BookEntry(){}
總結
在對象構造中,處理各種拋出異常的可能,是一個棘手的問題,但是智能指針能化繁爲簡。它不僅把令人不好理解的代碼隱藏起來,而且使得程序在面對異常的情況下也能保持正常運行。