C++異常沉思錄之基礎篇
異常基本概念
異常即程序在運行期間產生的非預期性錯誤,如內存配額不足、文件不存在、網絡不可用、SQL語句非法等。
異常分爲可恢復性異常和不可恢復性異常。異常是否可恢復,不同的應用場景將有不同的理解。例如,對於內存配合不足,在短暫的峯涌情況下是可以快速恢復的,屬於可恢復性異常,一但內存被釋放和回收,程序將回到常態;而在內存長時間佔用的情況下,則可以認爲屬於不可恢復性異常,此時應當終止程序的運行。
異常又可分爲已知異常和未知異常。已知異常是指程序編寫者事先考慮到的異常,對異常有清晰的定義和處理邏輯;未知異常是指程序編寫者事先未察覺到的異常,可以理解爲程序bug。
異常捕獲是一把雙刃劍,對異常進行顯示捕獲,尤其對於bug型的未知異常,需要找出產生異常的原因和代碼進行修復,即需要考慮異常的分析定位機制。因此,對於未知異常或者無法處理的異常,不應當使用catch(…)籠統的進行捕獲,當不需要關心異常或者無法處理異常的時候,就應該避免捕獲異常或者將未處理的異常再次拋出,以便交由bugreport統一對未處理異常進行捕獲、分析。
C++提供了強大的異常處理機制將異常產生和異常處理的代碼分離,即將程序的正常業務邏輯和錯誤處理邏輯進行分離,並在不改變函數原型的情況下,實現異常對象的“向上”傳遞。
函數的異常聲明
在函數的聲明後面使用throw關鍵字可對函數的異常產生進行限制。
不允許函數拋出任何異常
void f() throw(); 表示f函數不允許拋出任何異常;以VS2005編譯器爲例,若在f函數中拋出異常,編譯時會有如下提示:
warning C4297: 'f' : function assumed not to throw an exception but does
1> __declspec(nothrow) or throw() was specified on the function
但異常還是能正常拋出和被捕獲。
允許函數拋出任意異常
void f() throw(…);表示允許f函數拋出任意異常。
允許函數拋出指定類型的異常
void f() throw(MyException1,MyException2);表示允許函數f拋出MyException1和MyException2兩種類型的異常。以vs2005爲例,在f中拋出其他類型的異常,編譯未見提示,且異常能正常拋出和被捕獲。
異常的拋出
使用關鍵字throw拋出異常,有如下兩種語法。
throw express;拋出express所代表的表達式的異常,異常類型爲表達式的值的類型,異常的值爲表達式的值。例如,
try{ throw 10; }catch(int e){ } |
throw 10;語句拋出int型異常,異常的值爲10。catch(int e)語句能捕獲int型異常,e的值就是異常表達式的值10。
throw;將異常繼續“向上傳遞”。例如,
try{ throw 10; }catch(int e){ throw; } |
throw;語句將throw 10;語句產生的int型異常繼續向上傳遞。
異常類型
任何類型都可以作爲異常類型,包括基本數據類型,如int、double、char等;還包括類類型,如std:string;還包括自定義異常類型,即從std::exception派生的子類。
異常對象的傳遞
異常對象的傳遞和函數參數的傳遞一樣,有值傳遞、指針傳遞和引用傳遞3種傳遞方式。
值傳遞會產生臨時對象拷貝,且不支持多態。例如,
class MyException:public std::exception{ public: MyException(const char* msg){ msg_ = msg; } const char* what(){ return msg_.c_str(); } protected: std::string msg_; }; void func3() throw(MyException){ throw MyException("func3 raised an exception."); return; } int _tmain(int argc, _TCHAR* argv[]) { try{ func3(); }catch(std::exception e){ printf(e.what()); } return 0; } ///<運行結果:UnKnown exceptions。 |
因爲值拷貝不支持動態聯編,因此e.what()訪問的是基類exception的what方法,輸出的就是Unknown exceptions。
指針傳遞可以實現多態,若拋出臨時對象的地址,容易出現懸掛指針錯誤;若在堆上分配異常對象,使用完後需要對其進行刪除,一來刪除對象的時機不確定,二來代碼可讀性、可維護性遭到破壞,違反了從哪裏來回哪裏去的內存分配釋放原則。指針傳遞的示例代碼如下所示,
class MyException:public std::exception{ public: MyException(const char* msg){ msg_ = msg; } const char* what(){ return msg_.c_str(); } protected: std::string msg_; }; void func3() throw(MyException){ throw new MyException("func3 raised an exception."); return; } int _tmain(int argc, _TCHAR* argv[]) { try{ func3(); }catch(std::exception* e){ printf(e->what()); delete e;///<釋放異常對象 } return 0; } ///<運行結果:func3 raised an exception.。 |
值引用傳遞既能避免臨時對象的拷貝,又能支持多態,且沒有對象釋放問題。推薦採用值引用的方式傳遞異常對象。示例代碼如下,
class MyException:public std::exception{ public: MyException(const char* msg){ msg_ = msg; } const char* what(){ return msg_.c_str(); } protected: std::string msg_; }; void func3() throw(MyException){ throw MyException("func3 raised an exception."); return; } int _tmain(int argc, _TCHAR* argv[]) { try{ func3(); }catch(std::exception& e){ printf(e.what()); } return 0; } ///<運行結果:func3 raised an exception.。 |
catch的匹配規則
異常匹配採用自底向上的匹配順序進行匹配,即先在異常產生的函數中進行匹配,若該造成該異常的代碼沒有被try…catch…捕獲或者catch類型不匹配,則向上傳遞直到匹配到第一條catch結束匹配,或則未發現任何匹配的catch則產生未處理異常交由運行時環境處理。
對於基本數據類型的異常,採用嚴格類型匹配機制,不支持類型轉換。例如,
try{ throw float(1.6); }catch(double e){ } |
拋出float類型的異常,double類型的catch是匹配不成功的。
對於自定義類型,基類類型能夠匹配子類異常對象,子類類型不能匹配基本異常對象。例如,
class MyException:public std::exception{ public: MyException(const char* msg){ msg_ = msg; } const char* what(){ return msg_.c_str(); } protected: std::string msg_; }; void func1() throw(){ throw MyException ("MyException"); } int _tmain(int argc, _TCHAR* argv[]) { try{ func1(); } catch(std::exception& e){ ///<使用基類類型匹配子類對象,匹配成功 printf(e.what()); } catch(MyException& e){ printf(e.what()); } return 0; } //運行結果:MyException |
class MyException:public std::exception{ public: MyException(const char* msg){ msg_ = msg; } const char* what(){ return msg_.c_str(); } protected: std::string msg_; }; void func1() throw(){ throw std::exception("std::exception"); } int _tmain(int argc, _TCHAR* argv[]) { try{ func1(); }catch(MyException& e){///<使用子類類型匹配基類對象,匹配失敗 printf(e.what()); }catch(std::exception& e){///<匹配成功 printf(e.what()); } return 0; } //運行結果:std::exception |
異常調用棧unwind
擬另起文檔專門進行描述。
供應寶異常體系結構
供應寶異常類受限於sens名字空間,其部分類圖如下所示。
sens::exception
供應寶異常基類,派生自c++標準異常類std::exception,統一異常處理行爲,如輸出堆棧,輸出日誌等。
showstack()
輸出異常產生時的堆棧,便於分析和定位異常。關於如何進行異常堆棧跟蹤,將另起文檔專門進行描述。
log()
將異常輸出到日誌文件中。
exception()
構造函數,調用showstack和log函數輸出異常的調用堆棧和異常日誌。
sens::dbexception
數據庫異常類。
sens::fileexception
文件異常類