設計一個通訊錄類別 BookEntry,除基本文字資料外,還包含圖片 (Image*) 與音訊 (AudioClip*) 指標。建構函數根據檔案名決定是否 new 這兩個資源,解構函數負責 delete。看似完美,實則暗藏異常安全陷阱。
BookEntry::BookEntry(...) : theImage(0), theAudioClip(0) {
if (imageFileName != "") theImage = new Image(imageFileName);
if (audioClipFileName != "") theAudioClip = new AudioClip(audioClipFileName); // 可能拋出 bad_alloc 或 AudioClip 建構例外
}
若第二個 new 拋出例外,theImage 已指向有效物件,但 BookEntry 建構未完成,C++ 不會呼叫解構函式,導致 Image 物件洩漏。C++ 規定:只有完全建構完成的物件才會被解構,避免在解構函式中需追蹤建構進度的額外開銷。
BookEntry::BookEntry(...) {
try {
if (imageFileName != "") theImage = new Image(imageFileName);
if (audioClipFileName != "") theAudioClip = new AudioClip(audioClipFileName);
} catch (...) {
delete theImage; delete theAudioClip; // 清理已配置資源
throw; // 重新拋出
}
}
可行但程式碼重複(解構函式也要做同樣清理),應提煉為私有 cleanup()。
若指標宣告為 Image* const theImage,必須在成員初始化列表初始化:
BookEntry::BookEntry(...)
: theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0) {}
初始化列表不允許 try-catch 語句(只能含表達式),若 theAudioClip 初始化拋出例外,theImage 又洩漏了。
Image* BookEntry::initImage(const string& f) {
return f != "" ? new Image(f) : 0; // 第一個初始化,失敗無洩漏
}
AudioClip* BookEntry::initAudioClip(const string& f) { // 第二個初始化
try { return f != "" ? new AudioClip(f) : 0; }
catch (...) { delete theImage; throw; } // 必須清理先前資源
}
雖解決洩漏,但建構邏輯分散在多個函數,維護困難。
將原始指標改為智慧指標 std::auto_ptr(C++11 後建議 std::unique_ptr),利用 RAII 機制:
class BookEntry {
// ...
const std::auto_ptr<Image> theImage; // 自動管理生命週期
const std::auto_ptr<AudioClip> theAudioClip;
public:
BookEntry(...)
: theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0) {}
~BookEntry() {} // 空函式,auto_ptr 自動釋放
};
優勢:
theAudioClip 初始化失敗,theImage (已完全建構的 auto_ptr) 會自動被解構釋放資源。delete,甚至可省略解構函式。const auto_ptr。建構函數中的資源管理是異常安全的核心挑戰。以 RAII 物件(如 auto_ptr/unique_ptr)取代原始指標,能將資源釋放責任轉移給編譯器自動管理的物件生命週期,徹底解決建構失敗時的洩漏問題,同時簡化程式碼結構。這是現代 C++ 資源管理的基石模式。