C++中的異常實例詳解
1. 異常
異常: 異常是面向?qū)ο笈c法處理錯誤的一種方式
1.1 C語言中處理錯誤的方式
- 返回錯誤碼 (很多API接口都會把錯誤碼放到errno當中)
- 終止程序 (assert終止、除零錯誤、段錯誤) [產(chǎn)生信號去終止進程]
- C標準庫中setjump和longjump組合
1.2 C語言處理錯誤方式的缺陷
- 使用錯誤碼時,還需要查看錯誤碼表,去找每個錯誤碼的含義
- 當一個函數(shù)是通過返回值去輸出數(shù)據(jù),那么發(fā)生錯誤的時候很難處理 (不好區(qū)設置發(fā)生錯誤時返回什么)
- 如果調(diào)用的函數(shù)棧很深時,使用錯誤碼去一層層返回時,處理起來很麻煩
//第2點 T& operator[](int index) { //問題: 當訪問的下標index超出容器的范圍,此時該返回什么值? //解決方案: 可以再傳一個參數(shù),用于獲取真正的結(jié)果(當輸出型參數(shù)), 返回值就用于判斷是否正確的返回了. //實際上還是很麻煩 }
2. C++異常
2.1 異常相關(guān)關(guān)鍵字
- try: try塊中放置可能會拋出異常的代碼,這段代碼被稱為保護代碼
- catch: catch用于捕獲異常,catch塊中寫處理異常的代碼。它跟在try的后面,1個try后面可以跟多個catch
- throw: throw用于拋出異常
try { //保護代碼(可能會拋出異常的代碼) }catch(ExceptionType e1) { //處理異常 }catch(ExceptionType e2) { //處理異常 }catch(ExceptionType eN) { //處理異常 }
2.2 異常的使用
2.2.1 異常的拋出和匹配原則
- 異常是通過拋出對象而引發(fā)的,該對象的類型決定了應該匹配到哪個catch塊
- 異常的拋出會去匹配在整個調(diào)用鏈中與該對象類型相匹配且距離最近的那個
- 當異常拋出后,執(zhí)行流會直接跳轉(zhuǎn)到整個調(diào)用鏈當中能catch到異常的位置處,不能catch的地方就不會執(zhí)行了,所以這可能導致內(nèi)存泄漏、文件流沒關(guān)閉、鎖未釋放**等情況。(free、fclose、unlock未執(zhí)行)
- catch(…)可以用來捕獲任意類型對象的異常,一般用于表示"未知異常"或被用作異常的重新拋出時的接收catch塊
- 在拋出與捕獲的類型匹配上并不全都是類型相同才能匹配。我們可以使用基類去捕獲 —> 拋出的派生類的對象。 //會在下面具體講解
· 原則2:函數(shù)調(diào)用鏈中的異常 - 棧展開的匹配原則
- 查看throw是否在try的內(nèi)部,如果存在能匹配到的catch語句,就跳轉(zhuǎn)到catch中進行異常的處理。
- 如果沒有匹配的catch就退出當前棧,去查找調(diào)用鏈當中的能夠匹配到的catch
- 如果在main函數(shù)中都沒有找到匹配的catch,則終止程序。
棧展開:上述的這個沿著調(diào)用鏈去逐個棧中查找匹配的catch語句的過程就是棧展開。
//代碼演示:f3()中拋出異常,該異常會被f1()捕捉,而在main函數(shù)中的catch是無法捕捉到的 void f3() { throw 123; } void f2() { f3(); } void f1() { try { f2(); cout << "f1() not catched Exception!" << endl; }catch(int& err) { cout << "f1()-err: " << err << endl; } } int main() { try { f1(); cout << "main not catched Exception!" << endl; }catch(int& err) { cout << "main-err: " << err << endl; } return 0; }
注意:
拋出異常對象后,會生成一個異常對象的拷貝(可能是一個臨時對象),這個拷貝的臨時對象在被catch后會被銷毀。異常的執(zhí)行流執(zhí)行順序:從throw拋出異常處跳轉(zhuǎn)到調(diào)用鏈中能夠匹配的catch語句中;然后執(zhí)行catch塊中的代碼;catch執(zhí)行完畢后在當前函數(shù)棧中順序執(zhí)行。(整個調(diào)用鏈中沒有匹配的就終止程序)
2.3 異常的重新拋出
可能會存在單個catch不能完全處理一個異常的情況,在經(jīng)過一些校正處理后,我們希望將該異常交給外層調(diào)用鏈中的函數(shù)來處理,此時我們可以通過在catch中重新拋出異常的方式把異常傳遞給調(diào)用鏈的上層函數(shù)處理。
//在SecondThrowException()函數(shù)中我們要delete[]動態(tài)開辟(new出來)的空間, //如果不使用異常的重新拋出的話,就會造成內(nèi)存泄漏問題 (也可以使用RAII) void FirstThrowException() { throw "First throw a exception!"; } void SecondThrowException() { int* arr = new int[10]; try { FirstThrowException(); }catch(...) { cout << "Delete[] arr Success!" << endl; delete[] arr; throw; } } void SoluteException() { try { SecondThrowException(); }catch(const char* err) { cout << err << endl; } } int main() { SoluteException(); return 0; }
2.4 自定義異常體系
自定義異常體系實際上就是自己定義的一套異常管理體系,很多公司當中都會自定義自己的異常體系以便于規(guī)范的進行異常管理。它主要用到了我們在上面所說的一條規(guī)則: 我們只需要拋出派生類對象,然后捕獲基類對象就可以了。這樣的拋出與捕獲方式非常便于異常的處理。
class MyException { public: MyException(string errmsg, int id) :_errmsg(errmsg), _id(id) {} virtual string what() const = 0; //必須放到public下才能讓類外定義的成員訪問到 protected: int _id; //錯誤碼 string _errmsg; //存放錯誤信息 //list<StackInfo> _traceStack; //存放調(diào)用鏈的信息 //... }; class CacheException : public MyException { public: CacheException(string errmsg, int id) :MyException(errmsg, id) {} virtual string what() const { return "CacheException!: " + _errmsg; } }; class NetworkException : public MyException { public: NetworkException(string errmsg, int id) :MyException(errmsg, id) {} virtual string what() const { return "NetworkException!: " + _errmsg; } }; class SqlException : public MyException { public: SqlException(string errmsg, int id) :MyException(errmsg, id) {} virtual string what() const { return "SqlException!: " + _errmsg; } }; int main() { try { //拋出任意的派生類對象 throw SqlException("sql open failed", 10); } catch (const MyException& e) //只需要捕獲基類對象 { cout << e.what() << endl; //這里實際上完成了一個多態(tài) } catch (...) //走到這里說明出現(xiàn)未知異常 { cout << "Unknown Exception!" << endl; } return 0; }
2.5 異常安全
- 最好不要在構(gòu)造函數(shù)中拋異常,構(gòu)造函數(shù)是完成對象的構(gòu)造和初始化的,在里面拋異??赡軙е聦ο髽?gòu)造不完整或沒有完全初始化。 (可能會造成內(nèi)存泄漏)
- 最好不要在析構(gòu)函數(shù)中拋異常,析構(gòu)函數(shù)是完成資源的清理工作的,在里面拋異??赡軐е沦Y源沒清完就結(jié)束了函數(shù)調(diào)用,從而導致內(nèi)存泄漏、句柄未關(guān)閉等問題。
- 注意new、fopen、lock的使用
2.6 異常規(guī)范
異常規(guī)范的指定是為了讓使用者知道函數(shù)可能拋出哪些異常,用法:
void func1() throw(); //表示該函數(shù)不會拋異常 void func2() noexcept; //等價于throw() 表示該函數(shù)不會拋異常 void func3() throw(std::bad_alloc); //表示該函數(shù)只會拋出bad_alloc的異常 void func4() throw(int, double, string); //表示該函數(shù)會拋出int/double/string類型中的某種異常
2.7 C++標準庫的異常體系
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 | 當使用了一個無效的數(shù)學域時,會拋出該異常。 |
std::invalid_argument | 當使用了無效的參數(shù)時,會拋出該異常。 |
std::length_error | 當創(chuàng)建了太長的 std::string 時,會拋出該異常。 |
std::out_of_range | 該異??梢酝ㄟ^方法拋出,例如 std::vector 和 std::bitset<>::operator。 |
std::runtime_error | 理論上不可以通過讀取代碼來檢測到的異常。 |
std::overflow_error | 當發(fā)生數(shù)學上溢時,會拋出該異常。 |
std::range_error | 當嘗試存儲超出范圍的值時,會拋出該異常。 |
std::underflow_error | 當發(fā)生數(shù)學下溢時,會拋出該異常。 |
int main() { try { vector<int> v(5); v.at(5) = 0; //v.at(下標) = v[下標] + 拋異常 } catch(const exception& e) { cout << e.what() << endl; } catch(...) { cout << "Unknown Exception!" << endl; } }
2.8 異常的優(yōu)缺點
2.8.1 優(yōu)點
- 清晰的顯示出錯誤信息
- 可以很好地解決返回值需要返回有效數(shù)據(jù)的函數(shù) //如T& operator[ ] (int index)
- 在多層函數(shù)調(diào)用時發(fā)生錯誤,可以用直接在外層進行捕獲異常
- 異常在很多第三方庫中也有使用 //boost、gtest、gmock
2.8.2 缺點
- 異常會導致執(zhí)行流跳轉(zhuǎn),這會使得調(diào)試分析程序時更加困難
- C++沒有GC (垃圾回收機制),使用異常時可能導致資源泄露等異常安全問題。
- C++標準庫的異常體系不實用
- C++的允許拋出任意類型的異常,在項目中不進行規(guī)范管理的話,會十分混亂。 //一般需要定義一套繼承體系的異常規(guī)范
總結(jié)
到此這篇關(guān)于C++異常的文章就介紹到這了,更多相關(guān)C++異常內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
windows下vscode環(huán)境c++利用matplotlibcpp繪圖
本文主要介紹了windows下vscode環(huán)境c++利用matplotlibcpp繪圖,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-02-02c++語言中虛函數(shù)實現(xiàn)多態(tài)的原理詳解
這篇文章主要給大家介紹了關(guān)于c++語言中虛函數(shù)實現(xiàn)多態(tài)的原理的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用c++語言具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-05-05