8. C++ 異常原理及使用

8.1 基本用法

對異常的處理:

  • 引發異常;
  • 使用異常處理程序(exception handler)捕捉異常;
  • 使用try塊。

要在C++使用異常機制,可以在程序的try塊內出現問題時使用throw語句拋出異常(一個變量/常量)。
try塊中拋出的異常可以利用其後的catch塊(異常處理程序)捕捉。
catch塊通過識別拋出的異常類型捕捉異常(catch塊類似於函數定義),並在執行完後返回到拋出異常的語句的下一個語句。

如果程序引發了異常而沒有在try塊內或沒有匹配的處理程序(catch塊)函數會調用abort()函數,不過這一行爲可以修改。

8.2 將對象用作異常類型

通常,將對象作爲異常拋出。這樣做的重要優點之一是可以使用不同的異常類型來區分引發的問題。

8.3 棧解退

C++通常將調用函數的指令的地址(返回地址)放在棧中放在棧中。當被調用函數執行完畢(return)時,程序利用此地址確定從哪裏繼續執行。
此外,若在函數中創建了自動變量,後者也會被放進棧中,若調用了一個函數,被調用函數的信息也會被存入棧中。每個函數在執行完後都會按壓棧的逆序釋放棧中的信息,直到釋放了第一個返回地址。

然而如果函數是因爲異常而非return語句返回,則不會在釋放了第一個返回地址後停止釋放,而是會繼續釋放,直到釋放了第一個在try塊內的返回地址。
然後會將控制權交給緊跟其後的catch塊,而非接着運行函數調用的下一條語句。

在棧解退過程中,自動變量會被自動釋放,若自動變量是一個對象,那麼對象的析構函數也會被自動調用。

8.4 異常規範(不提倡使用)

double harm(double a) throw(bad_thing);
double marm(double a) throw();

異常規範出現於函數原型或定義中,可以包含類型列表(指出可能發生的異常,可以有多個),也可不包含(不會發生異常)。
要指出不會引發異常,還可以用C++11中的關鍵字noexcept代替空類型列表的異常規範。
另外,還有運算符noexcept(),可以判斷操作數是否會發生異常。

原則上,函數的異常規範應包含函數調用的函數的異常規範。

8.4 其他異常特徵

8.4.1 catch 塊使用引用

引發異常時編譯器總是會創建一個異常的臨時變量拷貝。這樣,catch捕捉到的異常變量不是異常變量本身而只是一個副本,即使使用了引用。

但是使用引用有其他好處:

  • 使用引用可以避免創建另一個臨時變量,減少不必要的開支。
  • 如果使用基類的引用,那麼這個catch塊就還可以捕捉所有派生類類型的異常。不過如果要這麼做要將處於繼承層次上部的類型(如果想要特殊處理的話)放在靠後的位置。

8.4.2 默認異常處理程序

如果要捕捉未知類型的異常,使用類似以下這樣的try塊:

catch(...)
{
    cout << "undetermined error";
}

8.4.3 由於異常而引發的問題

  1. 異常在帶有異常規範的函數中引發,如果引發的異常不能與異常規範中的任何一個類型匹配,則稱這個異常爲意外異常(unexpected exception)
  2. 異常不是在函數中引發的、函數沒有異常規範或異常符合異常規範,這時異常如果沒有被捕捉,則稱這個異常爲未捕捉異常(uncaught exception)

上述函數是指與catch塊在同一層及更深層的函數調用。

通常情況下,意外異常和未捕捉異常都會導致異常終止,但是可以修改這一行爲。
程序在發現以上兩種異常時並不會直接結束程序,而是會分別調用unexpected()terminate()函數。默認情況下,terminate()函數都會調用abort()函數,而unexpected()函數會調用terminate()函數。
兩個函數聲明在頭文件exception中。頭文件exception中還聲明瞭set_unexpected()set_terminate()函數,兩個函數都接受一個返回值類型爲void,參數列表爲空的函數指針作爲參數。
函數指針也在exception頭文件中進行了定義:

typedef void (*terminate_handler) ();
typedef void (*unexpected_handler) ();

相比於terminate_handler函數,unexpected_handler函數的行爲受到更嚴格的限制:

  • 通過調用abort()exit()terminate()終止程序。
  • 修改unexpected(),在unexpected()中引發異常。
    • 引發的異常與原來的異常規範匹配,則函數按普通的方式正常運行,即尋找與新異常匹配的catch塊。
    • 如果不匹配,且原來的異常規範中沒有包括bad_exception,異常程序會調用terminate()函數,而不是再次調用unexpected()函數。
    • 如果·不匹配,但原來的異常規範中包括bad_exception,那麼不匹配的異常會自動被bad_exception代替。

8.5 exception 類

異常 描述
std::exception 該異常是所有標準 C++ 異常的父類。
std::bad_alloc 該異常可以通過 new 拋出。
std::bad_cast 該異常可以通過 dynamic_cast 拋出。
std::bad_exception 這在處理 C++ 程序中無法預期的異常時非常有用。
std::bad_typeid 該異常可以通過 typeid 拋出。
std::logic_error 理論上可以通過讀取代碼來檢測到的異常。
std::domain_error 當使用了一個無效的數學域時,會拋出該異常。
std::invalid_argument 當使用了無效的參數時,會拋出該異常。
std::length_error 當創建了太長的 std::string 時,會拋出該異常。
std::out_of_range 該異常可以通過方法拋出,例如 std::vectorstd::bitset<>::operator[]()
std::runtime_error 理論上不可以通過讀取代碼來檢測到的異常。
std::overflow_error 當發生數學上溢時,會拋出該異常。
std::range_error 當嘗試存儲超出範圍的值時,會拋出該異常。
std::underflow_error 當發生數學下溢時,會拋出該異常。

異常具有這樣的一種特性:類似於類,可以改變你的編程方式。

exception頭文件中定義了exception類,類中有虛方法what()(除了構造函數和析構函數外,只有這個成員),what()返回一個字符串。
因此可以以其爲基類派生出擁有用戶定義what()函數的派生類。

8.5.1 stdexcept 異常類

頭文件stdexcept定義了其他幾個異常類。

首先以公有繼承的方式從exception派生出了logic_errorruntime_error類。
這些類的構造函數接受一個string對象作爲參數,其what函數接受一個C字符串。
ISO C++爲新版本的C++定義了一個接受C字符串的構造函數。
另外的信息可以參考頭文件。以下爲內容節選:

explicit
    logic_error(const string& __arg) _GLIBCXX_TXN_SAFE;

#if __cplusplus >= 201103L
    explicit
    logic_error(const char*) _GLIBCXX_TXN_SAFE;
#endif

#if _GLIBCXX_USE_CXX11_ABI || _GLIBCXX_DEFINE_STDEXCEPT_COPY_OPS
    logic_error(const logic_error&) _GLIBCXX_USE_NOEXCEPT;
    logic_error& operator=(const logic_error&) _GLIBCXX_USE_NOEXCEPT;
#endif
// ...
virtual const char*
    what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT;

logic_error又派生出了更具體的異常類:

  1. domain_error:建議用來處理數學的定義域問題。
  2. invalid_argument:建議用來處理函數參數問題(內容問題)。
  3. length_error:建議用來處理數組、動態分配的內存等長度不足的問題。
  4. out_of_range:建議用來處理索引超出範圍的問題。

runtime_error派生出了以下異常類:

  1. range_error:計算結果可能不在函數允許的範圍內但沒有發生上溢下溢
  2. overflow_error:發生在浮點數計算中,一般來說,存在浮點類型可以表示的最大(絕對值)非零值,如果計算結果比這個值還大的話會發生下溢
  3. underflow_error:發生在浮點數計算中,一般來說,存在浮點類型可以表示的最小(絕對值)非零值,如果計算結果比這個值還小的話會發生下溢

8.5.2 bad_alloc 異常和 new

在以前,new請求內存出錯(通常是內存不足)時,new返回一個空指針nullptr
但現在,new會引發bad_alloc異常。bad_alloc異常和nothrow都定義在頭文件new中。
另外,在頭文件new中還定義了bad_array_new_length異常類型。

爲了儘量向下兼容,C++提供了返回空指針的new

int * pi = new (nothrow) int;
int * pi = new (nothrow) int[500];

8.6 有關異常的注意事項

  1. 應該在設計程序時就加入異常處理功能,而不是以後再添加。
  2. 異常規範不適用於模板,因爲模板函數引發的異常可能隨特定的具體化而異。
  3. 異常和動態內存分配並非總能協調工作。

對於第三點,一般有兩種解決方案:

  1. catch塊中添加相應地delete語句。
  2. 使用智能指針取代普通指針來管理動態分配的內存。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章