C++异常处理

**異常處理**（exception handling）是 C++ 的一項語言機制，用於在程序能處理異常事件。

異常事件在 C++ 中表示為**異常對象**（exception object）。異常事件發生時，由作業系統為程序設置當前異常對象，然後執行程序的當前異常處理代碼塊，在包含了異常出現點的最內層的 `try` 塊，依次匹配同級的 `catch` 語句。如果匹配 `catch` 語句成功，則在該 catch 塊內處理異常；然後執行當前 `try...catch...` 塊之後的代碼。如果在當前的 `try...catch...` 塊沒有能匹配該異常對象的 `catch` 語句，則由更外一層的 `try...catch...` 塊處理該異常；如果當前函數內的所有 `try...catch...` 塊都不能匹配該異常，則遞歸回退到調用棧的上一層函數去處理該異常。如果一直回退到主函數 `main()` 都不能處理該異常，則調用系統函數 `terminate()` 終止程序。

## throw

`throw` 是一個 C++ 關鍵字，與其後的操作數構成了 `throw` 語句，語法上類似於 `return` 語句。`throw` 語句必須被包含在 `try` 塊之中；可以是被包含在調用棧的外層函數的 `try` 中。

執行 `throw` 語句時，其操作數的結果作為對象被複製構造為一個新的對象，放在內存的特殊位置（既不是堆也不是棧，Windows 上是放在「執行緒信息塊 TIB」中）。這個新的對象由本級的 `try` 所對應的 `catch` 語句逐個做類型匹配；如果匹配不成功，則與本函數的外層 `catch` 語句依次做類型匹配；如果在本函數內不能與 `catch` 語句匹配成功，則遞歸回退到調用棧的上一層函數內從函數調用點開始繼續與 `catch` 語句匹配。重複這一過程直到與某個 `catch` 語句匹配成功或者直到主函數 main () 都不能處理該異常。

因此，`throw` 語句拋出的異常對象不同於一般的局部對象。一般的局部對象會在其作用域結束時被析構。而 `throw` 語句拋出的異常對象駐留在所有可能被激活的 `catch` 語句都能訪問到的內存空間中。

`throw` 語句拋出的異常對象在匹配成功的 `catch` 語句的結束處被析構（即使該 catch 語句使用的是非「引用」的傳值參數類型）。

由於 `throw` 語句都進行了一次副本拷貝，因此異常對象應該是可以 copy 構造的。但對於 Microsoft Visual C++ 編譯器，異常對象的複製構造函數即使私有的情形，異常對象仍然可以被 `throw` 語句正常拋出；但在 catch 語句的參數是傳值時，在 catch 語句處編譯報錯：「 cannot be caught as the destructor and/or copy constructor are inaccessible」。

拋出一個表達式時，被拋出對象的靜態編譯時類型將決定異常對象的類型。

## catch

`catch` 語句匹配被拋出的異常對象時，如果 `catch` 語句的參數是引用型，則該參數直接引用到 throw 語句拋出的異常對象上；如果 `catch` 參數是傳值的，則拷貝構造一個新的對象作為 `catch` 語句的參數的值。在該 catch 語句結束時，先析構 `catch` 的傳值的參數對象，然後析構 throw 語句拋出的異常對象。

`catch` 語句匹配異常對象時，不會做任何隱式類型轉換（implicit type conversion），包括類型提升（promotion）。 異常對象與 catch 語句進行匹配的規則很嚴格，一般除了以下幾種情況外，異常對象的類型必須與 catch 語句的聲明類型完全匹配：允許非 const 到 const 的轉換；允許派生類到基類的轉換；將數組和函數類型轉換為對應的指針。

在 catch 塊中可以使用不帶表達式的 throw 語句將捕獲的異常重新拋出：

被重新拋出的異常對象就是當前 catch 語句捕獲時所匹配的，原本由 `throw` 語句拋出的那個異常對象。重新拋出的異常對象與 catch 語句的形參無關。如原來拋出的是派生類 Derived，catch 語句形參是基類 Based，則重新拋出後的異常類型是 Derived。如果 catch 語句形參是引用型，重新拋出的原來的異常對象的內容可能已在 catch 語句內部被修改了。

可以用 catch (...){} 來捕獲所有的異常。通常在 catch (...){ } 中，先執行可做的處理，然後重新拋出異常。

catch 語句內部產生的新異常，或者「重新拋出異常」，均不能被同級的 `try...catch...` 中其他的 catch 語句捕獲、處理。只能由更外層的 catch 語句去捕獲該異常。

## 棧展開

**棧展開**（unwinding）是指當前的 `try...catch...` 塊匹配成功或者匹配不成功異常對象後，從 `try` 塊內異常對象的拋出位置，到 `try` 塊的開始處的所有已經執行了各自構造函數的局部變量，按照構造生成順序的逆序，依次被析構。如果當前函數內對拋出的異常對象匹配不成功，則從最外層的 `try` 語句到當前函數體的起始位置處的局部變量也依次被逆序析構，實現棧展開，然後再回退到調用棧的上一層函數內從函數調用點開始繼續處理該異常。

catch 語句如果匹配異常對象成功，在完成了對 catch 語句的參數的初始化（對傳值參數完成了參數對象的 copy 構造）之後，對同層級的 try 塊執行棧展開。

由於執行緒執行時，被調用的函數的參數、返回地址、局部變量等都是依函數調用次序保存在函數調用棧（即執行緒運行時棧）上。當前被調用函數的參數、局部變量名字可以覆蓋掉早前調用函數的同名變量，看起來就是只有當前函數內的名字可以訪問，早前調用的函數內部的名字都不可訪問，就像磁帶被「捲起」。異常處理時按照函數調用順序的逆序析構，依次析構各個被調函數的局部變量，就類似把已經捲起的「磁帶」再展開，抹去上面記錄的數據，故此「棧展開」得名。unwinding 在物理學、電工學上也翻譯做「退繞」、「退卷」。

## C++ 標準程序庫中定義的異常類

標準異常類定義在 C++ 標準程序庫的四個頭文件中：

* <exception> 中定義了 exception 類
* <new> 中定義了 bad\_alloc 類
* <type\_info> 中定義了 bad\_cast 類
* <stdexcept> 中定義了 runtime\_error、logic\_error 類

所有的異常類都是 exception 類的子類。

runtime\_error 類（表示運行時才能檢測到的異常）包含了 overflow\_error、underflow\_error、range\_error 幾個子類；

logic\_error 類（一般的邏輯異常）包含了 domain\_error、invalid\_argument、out\_of\_range、length\_error 幾個子類；

各種標準異常類都定義了一個接受字符串的構造函數，字符串初始化式用於為所發生的異常提供更多的信息。所有異常類都有一個 what () 虛函數，它返回一個指向 C 風格字符串的指針。

應用程式可以從各種標準異常類派生自己的異常類。

## 函數的異常規格

**異常規格**（exception specification）列出函數可能會拋出的所有異常的類型。異常規格寫在函數的形參表之後的關鍵字 throw 之後跟著一對圓括號括住的異常類型列表。如：

異常列表還可以為空：

表示該函數不拋出任何異常。

如果函數內拋出的異常的類型不在該函數的異常規格中，則系統函數 unexpected () 被調用。如果在 unexpected () 中拋出的異常出現在該函數的異常規格中，則在該函數被調用處恢復對異常的 catch 處理。如果在 unexpected () 中拋出的異常**不**在該函數的異常規格中，則調用系統函數 terminate () 以終止程序。

標準異常類中的構造函數、析構函數和 what () 虛函數都承諾不拋出異常。如 what 的完整聲明為：virtual const char\* what () const throw ();

派生類中的虛函數不能拋出基類虛函數中沒有聲明的新異常。

使用函數的異常規格的好處：

* 拋出一個對象而不是用函數返回值判斷是否出錯；
* 函數調用序列中間的函數可以不考慮異常處理，由某一層函數調用撲捉異常；
* unwinding 自動發生。

Microsoft Visual C++ 接受但暫不支持 C++ 標準中的函數的異常規格。即使使用了編譯器選項 / D1ESrt，函數拋出不在其異常規格中的其他類型異常時，不會自動調用 unexpected ()，而是在該函數調用點處的 `try...catch...` 處理。在 Visual C++ 的函數名字修飾（name mangling）中，函數的形參的類型都編碼入被修飾後的函數名字中；但是函數的異常說明中的類型都沒有編碼入被修飾後的函數名字中。

## noexcept 關鍵字

事實上，異常規格這一特性在程序中很少被使用，因此在 C++11 中被棄用。C++11 定義了新的 noexcept 關鍵字。如果在函數聲明後直接加上 noexcept 關鍵字，表示函數不會拋出異常。另外一種形式是 noexcept 關鍵字後跟常量表達式，其值轉為布爾值，如果為真表示函數不會拋出異常，反之，則有可能拋出異常。

```
returnType funcDeclaration (args) noexcept(常量表达式) ;
```

如果保證不拋出異常的函數卻實際上拋出異常，則會直接調用 std::terminate 中斷程序的執行。

noexcept 關鍵字還可以用作運算符，其後的操作數表達式如果有可能拋出異常，則運算符返回為 false；如果操作數表達式保證不拋出異常，則運算符返回為 true。這一運算符用於在定義模板函數時可以根據模板參數類型來確定是否傳出異常。

對類析構函數，使用 noexcept 關鍵字也可以顯式指明不剖出異常。類析構函數默認不拋出異常。如果聲明為（或默認）不拋出異常的類析構函數在運行時拋出了異常，將導致調用 std::terminate 中斷程序的執行。

## 構造函數、析構函數與異常

構造函數沒有返回值，所以應該用異常來報告發生的問題。構造函數拋出異常就意味著該構造函數沒有執行完，所以其對應的析構函數不會被自動調用，因此構造函數應該先析構所有已初始化的基對象、成員對象，再拋出異常。

析構函數被期望不向函數外拋出異常。析構函數中向函數外拋出異常，將直接調用 terminator () 系統函數終止程序。如果一個析構函數內部拋出了異常，就應該在該析構函數內部捕獲、處理了該異常，不能讓異常被拋出析構函數之外。

### 構造函數初始化列表的異常機制

C++ 類構造函數初始化列表的異常機制，稱為 function-try block。一般形式為：

## 資源獲取即初始化

**資源獲取即初始化**（Resource acquisition is initialization，RAII）是指：為了更為方便、魯棒地釋放已獲取的資源，避免資源死鎖，一個辦法是把資源數據用對象封裝起來。程序發生異常，執行**棧展開**時，封裝了資源的對象會被自動調用其析構函數以釋放資源。

## 例子

## C++ 異常與 Windows 作業系統異常處理機制

在 Windows 作業系統上，編譯器實現的 C++ 異常一般是基於作業系統的異常處理機制。

## 參見

異常處理

## 參考文獻

* 《ISO/IEC 14882:2011 C++ Standard》 第 15 章 「Exception handling」