1. 程式人生 > >Item 8:解構函式不要丟擲異常 Effective C++筆記

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();
    }
    ...

這個常規方法給了客戶自行關閉資料庫並處理異常的機會,當然如果他放棄這個機會, 便不能怪罪於我們讓程式退出或者吞掉異常了。