Item 8:解構函式不要丟擲異常 Effective C++筆記
Item 8: Prevent exceptions from leaving destructors.
解構函式不要丟擲異常
由於解構函式常常被自動呼叫,在解構函式中丟擲的異常往往會難以捕獲,引發程式非正常退出或未定義行為。 例如,物件陣列被析構時,會丟擲多於一個的異常,然而同時存在的異常在C++標準中是禁止的, 因此程式會非正常退出:
class Widget {
public:
~Widget() { ... } // assume this might emit an exception
};
void doSomething(){
std::vector <Widget> v;
} // v is automatically destroyed here
其實,容器中的物件在析構時丟擲異常還會引起後續的物件無法被析構,導致資源洩漏。 這裡的資源可以是記憶體,也可以是資料庫連線,或者其他型別的計算機資源。
解構函式是由C++來呼叫的,原始碼中不包含對它的呼叫,因此它丟擲的異常不可被捕獲。 對於棧中的物件而言,在它離開作用域時會被析構;對於堆中的物件而言,在它被delte
時析構。
請看:
class C{
public:
~C(){ throw 1;}
};
void main(){
try{
C c;
}
catch(int e){}
}
析構的異常並不會被捕獲,因為try{}
程式碼塊中只有一行程式碼C
c
,它並未丟擲異常。 經Homebrew gcc 5.1.0編譯後,執行時會產生這樣的錯誤輸出:
libC++abi.dylib: terminating with uncaught exception of type int
也許你覺得在try
中用delete
手動釋放堆物件就可以捕獲異常。我們來試試:
C *p = new C;
try{
delete p;
}
catch(int e){}
上述程式碼會給出同樣的錯誤輸出:
libC++abi.dylib: terminating with uncaught exception of type int
這隻能說明delete
並不是對解構函式的直接呼叫,它只是一個關鍵字。。解構函式還是由C++呼叫的。
事實上,如果上面不delete
的話,程式不會產生錯誤,此時p
屬於記憶體洩露,
這些記憶體是在程式退出後由作業系統來回收的。
那麼在解構函式中,應處理掉可能的異常,保證物件能夠被完整地釋放。 因為解構函式中總會出現非安全的程式碼,我們只能吞掉異常,或者退出程式。這樣:
class DBConn{
public:
~DBConn{
if(!closed){
try{
db.close();
}
catch(...){
cerr<<"資料庫關閉失敗"<<endl;
// 或者直接退出程式
// std::abort();
}
}
}
private:
DBConnection db;
};
另外值得一提的是,上述
catch(...)
中的...
並不是省略號,它是合法識別符號,表示不確定的形參。
但是對於一個完善的設計,我們需要讓客戶知道這裡發生了異常。 在此只需為不安全語句提供一個新的函式;在解構函式中我們還是執行預設操作(忽略、記錄、或者結束程式)。
class DBConn{
public:
void close(){
db.close();
}
...
這個常規方法給了客戶自行關閉資料庫並處理異常的機會,當然如果他放棄這個機會, 便不能怪罪於我們讓程式退出或者吞掉異常了。