一,什麼是異常處理
一句話:異常處理就是處理程式中的錯誤,比如嘗試除以零的操作。
二,為什麼需要異常,以及異常處理的基本思想
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,意味著你可以丟擲任何異常。六,異常基本語法
很簡單,丟擲一場用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++ 提供了一系列標準的異常,定義在 <exception> 中,我們可以在程式中使用這些標準的異常。它們是以父子類層次結構組織起來的,如下所示:
每個類所在的標頭檔案在圖下方標識出來
標準異常類的成員: ① 在上述繼承體系中,每個類都有提供了建構函式、複製建構函式、和賦值運算子過載。 ② logic_error類及其子類、runtime_error類及其子類,它們的建構函式是接受一個string型別的形式引數,用於異常資訊的描述; ③ 所有的異常類都有一個what()方法,返回const char* 型別(C風格字串)的值,描述異常資訊。
下表是對上面層次結構中出現的每個異常的說明:
異常 |
描述 |
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::vector 和 std::bitset<>::operator。 |
std::runtime_error |
理論上不可以透過讀取程式碼來檢測到的異常。 |
std::overflow_error |
當發生數學上溢時,會丟擲該異常。 |
std::range_error |
當嘗試儲存超出範圍的值時,會丟擲該異常。 |
std::underflow_error | 當發生數學下溢時,會丟擲該異常。 |
九、編寫自己的異常類為什麼要編寫自己的異常類? ① 標準庫中的異常是有限的; ② 在自己的異常類中,可以新增自己的資訊。(標準庫中的異常類值允許設定一個用來描述異常的字串)。如何編寫自己的異常類? ① 建議自己的異常類要繼承標準異常類。因為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. 編寫的供別人呼叫的程式庫,不應該結束程式,而應該透過丟擲異常,讓呼叫者決定如何處理(因為呼叫者必須要處理丟擲的異常)。
最後
如果足下基礎比較差,正好在學習C/C++,不妨關注下人人都可以學習的影片教程,通俗易懂,深入淺出,一個影片只講一個知識點。影片不深奧,不需要鑽研,在公交、在地鐵、在廁所都可以觀看,隨時隨地漲姿勢