C++ 面向对象(三)异常 :异常概念、异常的匹配规则、异常安全、异常体系
目錄
- C語言傳統的錯誤處理方法
- 異常的概念
- 異常的匹配規則
- 異常安全
- 異常規范
- 異常體系
- C++標準庫的異常體系
- 自定義異常體系
C語言傳統的錯誤處理方法
在C語言中,因為沒有異常這個機制,所以出現錯誤時一般都是以下方式解決
缺點:直接終止程序,而問題并沒有得到處理,用戶使用效果差
缺點:需要自己去查錯誤碼來定位錯誤,而返回錯誤碼當出現大量函數嵌套,一層一層返回錯誤碼時處理就會非常麻煩。
缺點:規避了問題,但是問題還是沒有得到處理,并且效率不高。
在C語言和C++早期沒有引入異常時,大部分錯誤都是使用錯誤碼和終止程序來解決的。然而這些方法都存在著明顯的缺陷和限制,對于一門現代的語言來說,是無法接受的。
所以C++即引入了一套異常處理機制。
異常的概念
異常是一種處理錯誤的方式,當一個函數發現自己無法處理的錯誤時就可以拋出異常,讓函數的直接或間接
的調用者處理這個錯誤
-
throw: 當問題出現時,程序會拋出一個異常。這是通過使用 throw 關鍵字來完成的。
-
catch:在您想要處理問題的地方,通過異常處理程序捕獲異常。catch 關鍵字用于捕獲異常。
-
try: try塊中的代碼標識將被激活的特定異常。它后面通常跟著一個或多個 catch 塊。
-
如果有一個塊拋出一個異常,捕獲異常的方法會使用 try 和 catch 關鍵字。try 塊中放置可能拋出異常的代碼,try
塊中的代碼被稱為保護代碼。
throw即拋出異常,語句中的操作數即為異常的類型(可為表達式)。
void func(int x, int y) {if (y == 0){throw string("除零異常");}cout << x / y << endl; }例如上面的即說明當y=0時,拋出String類型的異常。
throw也可以用作異常規范說明符,括號中的為可能拋出的異常類型
void func() throw(string);而如果不拋出異常,則直接給一個空括號即可(C++11引入關鍵字noexcept,效果一樣)
void* operator new (std::size_t size, void* ptr) throw(); void* operator new (std::size_t size, void* ptr) noexcept; //c++11引入try catch語句即為捕獲異常,try展開捕獲,catch()則根據括號中的類型來捕獲對應的異常
例如:
try {//需要捕獲的執行函數 } catch (//捕獲的異常類型) {//捕獲后的處理方法 } catch(...) //...代表捕獲任意類型異常異常的匹配規則
一個拷貝對象,這個拷貝的臨時對象會在被catch以后銷毀。(這里的處理類似于函數的傳值返回)
獲
在函數調用鏈中異常棧展開匹配原則
的地方進行處理。
過程稱為棧展開。所以實際中我們最后都要加一個catch(…)捕獲任意類型的異常,否則當有異常沒捕
獲,程序就會直接終止)
異常安全
從前面可以看到,一旦發生了拋出了異常,就會直接跳轉到捕獲的位置,而沒有執行原來的后續語句,這就導致了可能會因為異常發生內存泄漏、死鎖等問題。而這種由異常導致的問題,也被稱為異常安全問題。
通常發生在我們創建了某個空間或者描述符,還沒關閉就因為異常而跳轉出去,例如下面。
void func(int x, int y) {//arr沒有意義,只是用來舉個例子int* arr = new int[10];if (y == 0){throw string("除零異常");}cout << x / y << endl;delete[] arr; }int main() {try{func(4, 0);}catch (const string& e){cout << e << endl;}return 0; }當發生異常時,還沒來得及delete掉剛申請的空間,就直接跳轉到了main函數的捕獲位置,這就導致了arr沒有被釋放,這是一種很常見的問題。
可以通過RAII和異常的重新拋出來解決這個問題。
異常的重新拋出:有可能單個的catch不能完全處理一個異常,在進行一些校正處理以后,希望再交給更外層的調用鏈函數來處理,catch則可以通過重新拋出將異常傳遞給更上層的函數進行處理
void func(int x, int y) {//arr沒有意義,只是用來舉個例子int* arr = new int[10];try{if (y == 0){throw string("除零異常");}cout << x / y << endl;}catch (...){delete[] arr;throw;}delete[] arr; }int main() {try{func(4, 0);}catch (const string& e){cout << e << endl;}return 0; }這里直接將異常捕獲,然后釋放空間,再將異常重新拋出給外面處理,這樣就沒有發生內存泄漏。
但是可以看得出來,這種方法很麻煩,所以C++通常會使用RAII來解決這個問題。
- 構造函數完成對象的構造和初始化,最好不要在構造函數中拋出異常,否則可能導致對象不完整或沒有 完全初始化
- 析構函數主要完成資源的清理,最好不要在析構函數內拋出異常,否則可能導致資源泄漏(內存泄漏、句 柄未關閉等)
- C++中異常經常會導致資源泄漏的問題,比如在new和delete中拋出了異常,導致內存泄漏,在lock和 unlock之間拋出了異常導致死鎖
異常規范
從上面可以看到,隨意拋出異常可能會引發風險,即使有所控制的拋出,其他的使用者可能會因為不了解這個函數會拋出什么異常,從而不方便處理。所以通常需要對異常進行規范,說明該函數會拋出的異常的類型,讓其他人方便處理。
異常體系
C++標準庫的異常體系
C++ 提供了一系列標準的異常,定義在 <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::vector 和 std::bitset<>::operator。
std::runtime_error 理論上不可以通過讀取代碼來檢測到的異常。
std::overflow_error 當發生數學上溢時,會拋出該異常。
std::range_error 當嘗試存儲超出范圍的值時,會拋出該異常。
std::underflow_error 當發生數學下溢時,會拋出該異常。
自定義異常體系
從上面可以看到,C++標準庫的異常處理體系其實不是很完善,很多常見的錯誤都沒有進行處理,并且在我們具體使用中,還可能會根據使用的情景、環境不同,出現更多樣的異常,所以通常針對這種情況,我們會自定義一套自己的異常體系進行規范的異常管理。通過繼承基類對象,來實現更多的異常。拋出時拋出一個派生類,用基類來捕獲即可。
例如:
class Exception { public: //純虛函數,異常的描述信息virtual string what() = 0;protected:string _errmsg;//錯誤信息size_t _id;//錯誤碼stack<string> _st;//函數調用棧// ... };class SqlException : public Exception {};class SocketException : public Exception {};class TCPServerException : public Exception {};int main() {try {//拋出派生類對象}catch (const Exception& e) // 捕獲父類對象{}catch (...){cout << "Unkown Exception" << endl;}return 0; }總結
以上是生活随笔為你收集整理的C++ 面向对象(三)异常 :异常概念、异常的匹配规则、异常安全、异常体系的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux网络编程 | Socket编程
- 下一篇: Linux网络编程 | IO模型 :阻塞