# [17] 異常和錯誤處理
## FAQs in section [17]:
* [17.1] `try` / `catch` / `throw` 通過哪些方法來改善軟件質量?
* [17.2] 如何處理構造函數的失敗?
* [17.3] 如何處理析構函數的失敗?
* [17.4] 如果構造函數會拋出異常,我該怎樣處理資源?
* [17.5] 當別人拋出異常時,我如何改變字符數組的字符串長度來防止內存泄漏?
## 17.1 `try` / `catch` / `throw` 通過哪些方法來改善軟件質量?
通過排除使用`if`語句的一個理由。
代替 `try` / `catch` / `throw` 的通常做法是返回一個返回代碼(有時稱為錯誤代碼),調用者通過諸如`if`的條件語句明確地測試。例如,`printf()`, `scanf()` 和 `malloc()`就是這樣工作的:調用者被假定為會測試返回值來判斷函數是否成功。
盡管返回代碼技術有時是最適當的錯誤處理技術,但會產生增加不必要的`if`語句這樣的令人討厭的效果。
* **質量降級:**眾所周知,條件語句可能包含的錯誤大約十倍于其他類型的語句。因此,在其他都相同時,如果你能從代碼中消除條件語句,你會得到更健壯的代碼。
* **推遲面市:**由于條件語句是分支點,而它們關系到白盒法測試時的測試條件的個數,因此不必要的條件語句會增加測試的時間總量。如果你沒有走過每個分支點,那么你的代碼中就會有在測試中沒有被執行過的指令,直到用戶/客戶發現它,那就糟了。
* **增加開發成本:**不必要的控制流程的復雜性增加了尋找bug,修復bug,和測試的工作。
因此,相對于通過返回代碼和`if`來報告錯誤,使用`try` / `catch` / `throw`所產生更少有bug,更低的開發成本和更快面市的代碼。當然,如果你的團隊沒有任何使用`try` / `catch` / `throw`的經驗,你也許想先在一個玩具性的項目上使用一下,以便確定你明白正在做的事情——在把武器拿上真槍實彈的前線前,總應該演練一下吧。
## 17.2 如何處理構造函數的失敗?
拋出一個異常。
構造函數沒有返回類型,所以返回錯誤代碼是不可能的。因此拋出異常是標記構造函數失敗的最好方法。
如果你沒有或者不愿意使用異常,這里有一種方法。如果構造函數失敗了,構造函數可以把對象帶入一種“僵尸”狀態。你可以通過設置一個內部狀態位使對象就象死了一樣,即使從技術上來說,它仍然活著。然后加入一個查詢(“檢察員”)成員函數,以便類的用戶能夠通過檢查這個“僵尸位”來確定對象是真的活著還是已經成為僵尸(也就是一個“活著的死對象”)。你也許想有另一個成員函數來檢查這個僵尸位,并且當對象并不是真正活著的時候,執行一個 no-op(或者是更令人討厭的如 `abort()`)。這樣做真的不漂亮,但是如果你不能(或者不想)使用異常的話,這是最好的方法了。
## 17.3 如何處理析構函數的失敗?
往log文件中寫一個消息。或打電話給Tilda舅媽。但不要拋出異常!
以下是為什么(扣好你的安全帶):
C++的規則是你絕對不可以在另一個異常的被稱為“棧展開(stack unwinding)”的過程中時,從析構函數拋出異常。舉例來說,如果某人寫了`throw?Foo()`,棧會被展開,以至`throw?Foo()`和 `}?catch?(Foo?e)?{` 之間的所有的棧頁面被彈出。這被稱為_棧展開(statck unwinding)_
在棧展開時,棧頁面中的所有的局部對象會被析構。如果那些析構函數之一拋出異常(假定它拋出一個`Bar`對象),C++運行時系統會處于無法決斷的境遇:應該忽略`Bar`并且在`}?catch?(Foo?e)?{` 結束?應該忽略`Foo`并且尋找 `}?catch?(Bar?e)?{`?沒有好的答案——每個選擇都會丟失信息。
因此C++語言擔保,當處于這一點時,會調用`terminate()`來殺死進程。突然死亡。
防止這種情況的簡單方法是_不要從析構函數中拋出異常_。但如果你真的要聰明一點,你可以說_當處理另一個異常的過程中時,不要從析構函數拋出異常_。但在第二種情況中,你處于困難的境地:析構函數本身既需要代碼處理拋出異常,還需要處理一些“其他東西”,調用者沒有當析構函數檢測到錯誤時會發生什么的擔保(可能拋出異常,也可能做一些“其他事情”)。因此完整的解決方案非常難寫。因此索性就做一些“其他事情”。也就是,_不要從析構函數中拋出異常_。
當然,由于總有一些該規則無效的境況,這些話不應該被“引證”。但至少99%的情況下,這是一個好規則。
## 17.4 如果構造函數會拋出異常,我該怎樣處理資源?
對象中的每個數據成員應該清理自己。
如果構造函數拋出異常,對象的析構函數將不會運行。如果你的對象需要撤銷一些已經做了的動作(如分配了內存,打開了一個文件,或者鎖定了某個信號量),這些需要被撤銷的動作必須被對象內部的一個數據成員記住。
例如,應該將分配的內存賦給對象的一個“智能指針”成員對象`Fred`,而不是分配內存給未被初始化的`Fred*` 數據成員。這樣當該智能指針消亡時,智能指針的析構函數將會刪除`Fred`對象。標準類`auto_ptr`就是這種“智能指針”類的一個例子。你也可以寫你自己的引用計數智能指針。你也可以用智能指針來指向磁盤記錄或者其它機器上的對象。
## 17.5 當別人拋出異常時,我如何改變字符數組的字符串長度來防止內存泄漏?
如果你要做的確實需要字符串,那么不要使用`char`數組,因為數組會帶來麻煩。應該用一些類似字符串類的對象來代替。
例如,假設你要得到一個字符串的拷貝,隨意修改這個拷貝,然后在修改過的拷貝的字符串末尾添加其它的字符串。字符數組方法將是這樣:
```
?void?userCode(const?char*?s1,?const?char*?s2)
?{
???//?制作s1的拷貝:
???char*?copy?=?new?char[strlen(s1)?+?1];
???strcpy(copy,?s1);
???//?現在我們有了一個指向分配了的自由存儲的內存的指針,
//?w我們需要用一個try塊來防止內存泄漏:
???try?{
?????//?...?n現在我們隨意亂動這份拷貝...
//?將s2 添加到被修改過的 copy 末尾:
//?...?[在此處重分配 copy]?...
?????char*?copy2?=?new?char[strlen(copy)?+?strlen(s2)?+?1];
?????strcpy(copy2,?copy);
?????strcpy(copy2?+?strlen(copy),?s2);
?????delete[]?copy;
?????copy?=?copy2;
?????//?...?最后我們再次隨意亂動拷貝...
???}?catch?(...)?{
?????delete[]?copy;???//?得到一個異常時,防止內存泄漏
?????throw;???????????//?重新拋出當前的異常
???}
???delete[]?copy;?????//?沒有得到異常時,防止內存泄漏
?}
```
象這樣使用`char*`s是單調的并且容易發生錯誤。為什么不使用一個字符串類的對象呢?你的編譯器也許提供了一個字符串類,而且它可能比你自己寫的`char*`s更快,當然也更簡單、更安全。例如,如果你使用了標準化委員會的字符串類`std::string`,你的代碼看上去就會象這樣:
```
?#include?<string>???????????//?讓編譯器找到 std::string 類
?void?userCode(const?std::string&?s1,?const?std::string&?s2)
?{
???std::string?copy?=?s1;????//?制作s1的拷貝
//?...?現在我們隨意亂動這份拷貝...
???copy?+=?s2;???????????????//?A將 s2 添加到被修改過的拷貝末尾
//?...?最后我們再次隨意亂動拷貝...
?}
```
函數體中總共只有兩行代碼,而前一個例子中有12行代碼。節省來自內存管理,但也有一些是來自于我們不必先式的調用`str_xxx_()`例程。這里有一些重點:
* 由于`std::string`自動處理了內存管理,當增長字符串時,我們不需要先式地寫任何分配內存的代碼。
* 由于`std::string`自動處理了內存管理,在結束時不需要 `delete[]` 任何東西。
* 由于`std::string`自動處理了內存管理,在第二個例子中不需要 `try` 塊,即使某人會在某處拋出異常。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 &lt;iostream&gt; 和 &lt;cstdio&gt;輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫