前言
解構函式的作用在於完成物件銷燬的一些“善後工作”,然而,某些不科學的設計會產生一些問題。
本文將說明其中的一種不科學設計 - "將異常處理機制設定在解構函式中" 會產生的問題,以及解決方案。
問題描述
首先,請看一下一段程式碼:
1 class Widget { 2 public: 3 //...... 4 ~Widget() { // 假定這個解構函式可能會吐出異常 5 //...... 6 } 7 //...... 8 }; 9 10 void doSomething() 11 { 12 //...... 13 std::vector<Widget> v; 14 //...... 15 }
當 doSomething 函式執行完畢的時候,會開始析構容器 v 中存放的各個 Widget 物件。
然而,該物件解構函式可能會吐出異常。在這種情況下,將會吐出容器中物件個數的異常,這就會導致程式執行的不確定性。
例項分析
如果在物件結束的時候,必須執行一個可能丟擲異常的動作,那該怎麼辦呢?
下面通過一個實際應用中的例子,來說明如何解決這個問題。
首先,定義兩個類分別負責資料庫連線以及該類資源的管理:
1 // 該類負責資料庫的連線 2 class DBConnection { 3 public: 4 //...... 5 static DBConnection create(); // 建立一個DBConnection物件 6 //...... 7 void close(); 8 //...... 9 }; 10 11 // 該類負責類DBConnection的資源管理 12 class DBConn { 13 public: 14 //...... 15 ~DBConn() { // 解構函式確保資料庫連線總是會被關閉 16 db.close(); 17 } 18 //...... 19 private: 20 //...... 21 DBConnection db; 22 //...... 23 };
這樣,客戶可以寫出如下程式碼以管理資料庫連線:
1 { 2 //...... 3 DBConn dbc(DBConnection::create()); 4 //...... 5 }
該段程式碼結束時,DBConn 物件可能會丟擲異常,而一旦異常丟擲,就有可能產生問題描述中所說的那種問題現象。
正確的做法是:將 close() 的執行移交給使用者,然後再在解構函式中try and catch,請看解決方案程式碼:
1 // 修改後的DBConn類實現 2 class DBConn { 3 public: 4 //...... 5 void close() { // 要求使用者自己關閉資料庫物件 6 db.close(); 7 closed = true; 8 } 9 //...... 10 ~DBConn() { 11 if (!closed) { // 如果使用者忘記了這麼做,就採用 try catch 機制吞下異常。 12 try { 13 db.close(); 14 } 15 catch (...) { 16 // 記錄此次 close 失敗 17 //...... 18 } 19 } 20 } 21 //...... 22 private: 23 //...... 24 DBConnection db; 25 bool closed; // 增設此變數用以判斷使用者是否已經自行呼叫 close(),使用者也可根據此變數判斷 close() 是否順利執行並作出相應的異常處理。 26 //...... 27 };
這種做法實質是種“雙保險”:首先要求使用者自行呼叫 close()。如果使用者忘了,也會有 try catch吞掉異常。
最好的情況是使用者按照類使用手冊的要求自行呼叫 close()。使用者要是實在粗心大意,忘記了,那就只能讓 catch 去吞掉異常,在這種情況下,客戶沒有資格抱怨,說直接吞掉異常不好。
小結
要多多結合實際應用,合理使用 C++ 提供的 try 和 catch 機制,寫出健康而美妙的程式碼。