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在物理學、電工學上也翻譯做“退繞”、“退卷”。
異常類
所有的異常類都是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之後跟著一對圓括弧括住的異常類型列表。如:
void foo(int) throw(bad_alloc, invalid_argument){/*函式體*/}
異常列表還可以為空:
表示該函式不拋出任何異常。
如果函式內拋出的異常的類型不在該函式的異常規格中,則
系統函式unexpected()被調用。如果在unexpected()中拋出的異常出現在該函式的異常規格中,則在該函式被調用處恢復對異常的catch處理。如果在unexpected()中拋出的異常
不在該函式的異常規格中,則調用系統函式terminate()以終止程式。
標準異常類中的構造函式、析構函式和what()虛函式都承諾不拋出異常。如what的完整聲明為:virtual const char* what() const throw();
派生類中的虛函式不能拋出基類虛函式中沒有聲明的新異常。
使用函式的異常規格的好處:
Microsoft Visual C++接受但暫不支持C++標準中的函式的異常規格。即使使用了編譯器選項/D1ESrt,函式拋出不在其異常規格中的其他類型異常時,不會自動調用unexpected(),而是在該函式調用點處的try...catch...處理。在Visual C++的函式名字修飾(name mangling)中,函式的形參的類型都編碼入被修飾後的函式名字中;但是函式的異常說明中的類型都沒有編碼入被修飾後的函式名字中。
關鍵字
事實上,異常規格這一特性在程式中很少被使用,因此在
C++11中被棄用。C++11定義了新的noexcept關鍵字。如果在函式聲明後直接加上noexcept關鍵字,表示函式不會拋出異常。另外一種形式是noexcept關鍵字後跟常量表達式,其值轉為布爾值,如果為真表示函式不會拋出異常,反之,則有可能拋出異常。
returnType funcDeclaration (args) noexcept(常量表達式) ;
如果保證不拋出異常的函式卻實際上拋出異常,則會直接調用std::terminate中斷程式的執行。
noexcept關鍵字還可以用作運算符,其後的運算元表達式如果有可能拋出異常,則運算符返回為false;如果運算元表達式保證不拋出異常,則運算符返回為true。這一運算符用於在定義模板函式時可以根據模板參數類型來確定是否傳出異常。
對類析構函式,使用noexcept關鍵字也可以顯式指明不剖出異常。類析構函式默認不拋出異常。如果聲明為(或默認)不拋出異常的類析構函式在運行時拋出了異常,將導致調用std::terminate中斷程式的執行。
運用
構造函式
構造函式沒有返回值,所以應該用異常來報告發生的問題。構造函式拋出異常就意味著該構造函式沒有執行完,所以其對應的析構函式不會被自動調用,因此構造函式應該先析構所有已初始化的基對象、成員對象,再拋出異常。
析構函式
析構函式被期望不向函式外拋出異常。析構函式中向函式外拋出異常,將直接調用terminator()系統函式終止程式。如果一個析構函式內部拋出了異常,就應該在該析構函式內部捕獲、處理了該異常,不能讓異常被拋出析構函式之外。
構造函式初始化列表的異常機制
C++類構造函式初始化列表的異常機制,稱為function-try block。一般形式為:
myClass::myClass(type1 pa1) try: _myClass_val (初始化值) { /*構造函式的函式體 */} catch ( exception& err ) { /* 構造函式的異常處理部分 */};