十點詳解C++異常處理 一文助你全面剖析C++異常處理機制
一,什么是異常處理
一句話:異常處理就是處理程序中的錯誤,比如嘗試除以零的操作。
二,為什么需要異常,以及異常處理的基本思想
C++之父Bjarne Stroustrup在《The C++ Programming Language》中講到:一個庫的作者可以檢測出發生了運行時錯誤,但一般不知道怎樣去處理它們(因為和用戶具體的應用有關);另一方面,庫的用戶知道怎樣處理這些錯誤,但卻無法檢查它們何時發生(如果能檢測,就可以在用戶的代碼里處理了,不用留給庫去發現)。
Bjarne Stroustrup說:提供異常的基本目的就是為了處理上面的問題?;舅枷胧牵鹤屢粋€函數在發現了自己無法處理的錯誤時拋出(throw)一個異常,然后它的(直接或者間接)調用者能夠處理這個問題。
也就是《C++ primer》中說的:將問題檢測和問題處理相分離。
一種思想:在所有支持異常處理的編程語言中,要認識到的一個思想:在異常處理過程中,由問題檢測代碼可以拋出一個對象給問題處理代碼,通過這個對象的類型和內容,實際上完成了兩個部分的通信,通信的內容是“出現了什么錯誤”。當然,各種語言對異常的具體實現有著或多或少的區別,但是這個通信的思想是不變的。
三,異常出現之前處理錯誤的方式
在C語言的世界中,對錯誤的處理總是圍繞著兩種方法:一是使用整型的返回值標識錯誤;二是使用errno宏(可以簡單地理解為一個全局整型變量)去記錄錯誤。當然C++中仍然是可以用這兩種方法的。
這兩種方法最大的缺陷就是會出現不一致問題。例如有些函數返回1表示成功,返回0表示出錯;而有些函數返回0表示成功,返回非0表示出錯。
還有一個缺點就是函數的返回值只有一個,你通過函數的返回值表示錯誤代碼,那么函數就不能返回其他的值。當然,你也可以通過指針或者C++的引用來返回另外的值,但是這樣可能會令你的程序略微晦澀難懂。
四,異常為什么好
優點有以下幾點:
1. 函數的返回值可以忽略,但異常不可忽略。如果程序出現異常,但是沒有被捕獲,程序就會終止,這多少會促使程序員開發出來的程序更健壯一點。而如果使用C語言的error宏或者函數返回值,調用者都有可能忘記檢查,從而沒有對錯誤進行處理,結果造成程序莫名其面的終止或出現錯誤的結果。
2. 整型返回值沒有任何語義信息。而異常卻包含語義信息,有時你從類名就能夠體現出來。
3. 整型返回值缺乏相關的上下文信息。異常作為一個類,可以擁有自己的成員,這些成員就可以傳遞足夠的信息。
異常處理可以在調用跳級。這是一個代碼編寫時的問題:假設在有多個函數的調用棧中出現了某個錯誤,使用整型返回碼要求你在每一級函數中都要進行處理。而使用異常處理的棧展開機制,只需要在一處進行處理就可以了,不需要每級函數都處理。
五, C++中使用異常時應注意的問題
任何事情都是兩面性的,異常有好處就有壞處。如果在你的代碼中使用異常,那么需要注意以下事項:
1. 性能問題。這個一般不會成為瓶頸,但是如果你編寫的是高性能或者實時性要求比較強的軟件,就需要考慮了。
2. 指針和動態分配導致的內存回收問題:動態內存不會自動回收,如果遇到異常就需要考慮是否正確地回收了內存。
函數的異常拋出列表:如果沒有寫noexcept,意味著你可以拋出任何異常。
六,異?;菊Z法
很簡單,拋出一場用throw,捕獲用try...catch
- throw: 當問題出現時,程序會拋出一個異常。
- catch: 在您想要處理問題的地方,通過異常處理程序捕獲異常。
- try: try 塊中的代碼標識將被激活得特定異常。它后面通常跟著一個或多個 catch 塊。
- noexcept:用于聲明函數不拋出異常,如果函數拋了異常,則直接中斷,不能被捕獲
使用 try...catch 語句的語法如下所示:
- try
- {
- // 保護代碼
- }catch( ExceptionName e1 )
- {
- // catch 塊
- }catch( ExceptionName e2 )
- {
- // catch 塊
- }catch( ExceptionName eN )
- {
- // catch 塊
- }
如果 try 塊在不同的情境下會拋出不同的異常,這個時候可以嘗試羅列多個 catch 語句,用于捕獲不同類型的異常
捕獲異常時的注意事項:
- catch的匹配過程是找最先匹配的,不是最佳匹配。
- catch的匹配過程中,對類型的要求比較嚴格。不允許標準算術轉換和類類型的轉換。(類類型的轉化包括兩種:通過構造函數的隱式類型轉化和通過轉化操作符的類型轉化)。
七,異常之棧解旋
異常被拋出后,從進入try塊起,到異常被拋擲前,這期間在棧上構造的所有對象,都會被自動析構。
析構的順序與構造的順序相反,這一過程稱為棧的解旋(unwinding).
- struct Maker
- {
- Maker()
- {
- cout << "Maker() 構造函數" << endl;
- }
- Maker(const Maker& other)
- {
- cout << "Maker(Maker&) 拷貝構造函數" << endl;
- }
- ~Maker()
- {
- cout << "~Maker() 析構函數" << endl;
- }
- };
- void fun()
- {
- Maker m;
- cout << "--------" << endl;
- throw m;
- cout << "fun__end" << endl;
- }
- int main()
- {
- try
- {
- fun();
- }
- catch (Maker & m)
- {
- cout << "收到Maker異常" << endl;
- }
- }
八,C++ 標準的異常
C++ 提供了一系列標準的異常,定義在

每個類所在的頭文件在圖下方標識出來
標準異常類的成員: ① 在上述繼承體系中,每個類都有提供了構造函數、復制構造函數、和賦值操作符重載。 ② logic_error類及其子類、runtime_error類及其子類,它們的構造函數是接受一個string類型的形式參數,用于異常信息的描述; ③ 所有的異常類都有一個what()方法,返回const char* 類型(C風格字符串)的值,描述異常信息。
下表是對上面層次結構中出現的每個異常的說明:
九、編寫自己的異常類
- 為什么要編寫自己的異常類? ① 標準庫中的異常是有限的; ② 在自己的異常類中,可以添加自己的信息。(標準庫中的異常類值允許設置一個用來描述異常的字符串)。
- 如何編寫自己的異常類? ① 建議自己的異常類要繼承標準異常類。因為C++中可以拋出任何類型的異常,所以我們的異常類可以不繼承自標準異常,但是這樣可能會導致程序混亂,尤其是當我們多人協同開發時。 ② 當繼承標準異常類時,應該重載父類的what函數和虛析構函數。 ③ 因為棧展開的過程中,要復制異常類型,那么要根據你在類中添加的成員考慮是否提供自己的復制構造函數。
示例:
- #include <iostream>
- #include <exception>
- using namespace std;
- //第一種
- class Out_Range : public exception
- {
- public:
- explicit Out_Range(const string& _Message) : exception(_Message.c_str()) {}
- explicit Out_Range(const char* _Message) : exception(_Message) {}
- };
- //第二種
- struct Exce :public exception
- {
- const char* what() const override
- {
- return "Exce";
- }
- };
- void foo(int arr[], int len)
- {
- int i = -1;
- if (i<0 || i>=len)
- {
- throw Out_Range("數組越界啦~");
- }
- cout << arr[i] << endl;
- }
- int main()
- {
- int arr[3] = { 0 };
- try
- {
- foo(arr, 3);
- }
- catch (Out_Range& e) //自定義錯誤
- {
- cout << "Out_Range& e " << e.what() << endl;
- }
- catch (std::exception& e) //其他錯誤
- {
- cout <<"std::exception& e "<<e.what()<< endl;
- }
- return 0;
- }
十,來自C++之父Bjarne Stroustrup的建議
節選自《The C++ Programming Language》 ——C++之父Bjarne Stroustrup
1. 當局部的控制能夠處理時,不要使用異常;
2.使用“資源分配即初始化”技術去管理資源;
3. 盡量少用try-catch語句塊,而是使用“資源分配即初始化”技術。
4. 如果構造函數內發生錯誤,通過拋出異常來指明。
5. 避免在析構函數中拋出異常。
6. 保持普通程序代碼和異常處理代碼分開。
7. 小心通過new分配的內存在發生異常時,可能造成內存泄露。
8. 如果一個函數可能拋出某種異常,那么我們調用它時,就要假定它一定會拋出該異常,即要進行處理。
9. 要記住,不是所有的異常都繼承自exception類。
10. 編寫的供別人調用的程序庫,不應該結束程序,而應該通過拋出異常,讓調用者決定如何處理(因為調用者必須要處理拋出的異常)。