深度剖析C++中的異常機(jī)制
傳統(tǒng)排錯(cuò)
我們?cè)缭?C 程序里面?zhèn)鹘y(tǒng)的錯(cuò)誤處理手段有:
終止程序,如 assert;缺陷是用戶難以接受,說白了就是一種及其粗暴的手法,比如發(fā)生內(nèi)存錯(cuò)誤,除0錯(cuò)誤時(shí)就會(huì)終止程序。
返回錯(cuò)誤碼。缺陷是需要我們自己去查找錯(cuò)誤,如系統(tǒng)的很多庫(kù)的接口函數(shù)都是通過把錯(cuò)誤碼放到 errno 中,表示錯(cuò)誤。
C標(biāo)準(zhǔn)庫(kù)中 setjmp 和 longjmp 組合(不常用)
實(shí)際中 C 語(yǔ)言基本都是使用返回錯(cuò)誤碼的方式處理錯(cuò)誤,部分情況下使用終止程序處理非常嚴(yán)重緊急的錯(cuò)誤,因此異常機(jī)制就時(shí)運(yùn)而橫空出世
概念
異常是面向?qū)ο笳Z(yǔ)言常用的一種處理錯(cuò)誤的方式,當(dāng)一個(gè)函數(shù)發(fā)現(xiàn)自己無(wú)法處理的錯(cuò)誤時(shí)就可以拋出異常,讓函數(shù)直接或間接調(diào)用者自己來處理這個(gè)錯(cuò)誤
throw:當(dāng)程序出現(xiàn)問題時(shí),可以通過 throw 關(guān)鍵字拋出一個(gè)異常
try:try 塊中放置的是可能拋出異常的代碼,該代碼塊在執(zhí)行時(shí)將進(jìn)行異常錯(cuò)誤檢測(cè),try 塊后面通常跟著一個(gè)或多個(gè) catch 塊。
catch:如果try塊中發(fā)生錯(cuò)誤,則可以在 catch 塊中定義對(duì)應(yīng)要執(zhí)行的代碼塊。
try-catch 語(yǔ)句的語(yǔ)法實(shí)例:
try { //被保護(hù)的代碼 } catch (ExceptionName e1) { //catch塊 } catch (ExceptionName e2) { //catch塊 } catch (ExceptionName eN) { //catch塊 }
用法
異常是通過拋出對(duì)象而引發(fā)的,該對(duì)象的類型決定了應(yīng)該激活哪個(gè) catch 的處理代碼,如果拋出的異常對(duì)象沒有捕獲,或是沒有匹配類型的捕獲,那么程序會(huì)終止報(bào)錯(cuò)
異常捕獲和拋出
被選中的處理代碼(catch塊)是調(diào)用鏈中與該對(duì)象類型匹配且離拋出異常位置最近的那一個(gè)
拋出異常對(duì)象后,會(huì)生成一個(gè)異常對(duì)象的拷貝,因?yàn)閽伋龅漠惓?duì)象可能是一個(gè)臨時(shí)對(duì)象,所以會(huì)生成一個(gè)拷貝對(duì)象 \color{red} {因?yàn)閽伋龅漠惓?duì)象可能是一個(gè)臨時(shí)對(duì)象,所以會(huì)生成一個(gè)拷貝對(duì)象}因?yàn)閽伋龅漠惓?duì)象可能是一個(gè)臨時(shí)對(duì)象,所以會(huì)生成一個(gè)拷貝對(duì)象,這個(gè)拷貝的臨時(shí)對(duì)象會(huì)在被 catch 以后銷毀(類似于函數(shù)的傳值返回)
catch(…) 可以捕獲任意類型的異常,但捕獲后無(wú)法知道異常錯(cuò)誤是什么,實(shí)際異常拋出和捕獲的匹配原則有個(gè)例外,捕獲和拋出的異常類型并不一定要完全匹配,可以拋出派生類對(duì)象,使用基類進(jìn)行捕獲,這個(gè)在實(shí)際中非常有用
在函數(shù)調(diào)用鏈中異常棧展開的匹配原則:
當(dāng)異常被拋出后,首先檢查 throw 本身是否在 try 塊內(nèi)部,如果在則查找匹配的 catch 語(yǔ)句,如果有匹配的就跳到 catch 的地方進(jìn)行處理
如果當(dāng)前沒有匹配的 catch 則退出當(dāng)前函數(shù)棧,繼續(xù)在上一個(gè)調(diào)用中進(jìn)行查找 catch。找到匹配的 catch 子句并處理以后,會(huì)沿著 catch 子句后面繼續(xù)執(zhí)行,而不會(huì)跳回到原來拋異常的地方,如果到達(dá) main 函數(shù)的棧,依舊沒有找到匹配的 catch 則終止程序
比如下面的代碼中調(diào)用了 func3,func3 中調(diào)用 func2,func2 中調(diào)用 func1,func1 中拋出了一個(gè) string 的異常對(duì)象:
void func1() { throw string("這是一個(gè)異常"); } void func2() { func1(); } void func3() { func2(); } int main() { try { func3(); } catch (const string& s) { cout << "錯(cuò)誤描述:" << s << endl; } catch (...) { cout << "未知異常" << endl; } return 0; }
首先會(huì)檢查 throw 本身是否在 try 塊內(nèi)部,這里就會(huì)因此退出 func1 所在的函數(shù)棧,繼續(xù)在上一個(gè)調(diào)用棧中進(jìn)行查找,即 func2 所在的函數(shù)棧,由于 func2 中也沒有匹配的 catch,因此會(huì)繼續(xù)復(fù)讀套娃,最終在 main 函數(shù)棧中找到匹配的 catch
這時(shí)就會(huì)跳到 main 函數(shù)中對(duì)應(yīng)的 catch 塊中執(zhí)行對(duì)應(yīng)的代碼塊,執(zhí)行完后繼續(xù)執(zhí)行該代碼塊后續(xù)的代碼:
當(dāng)然為了防止還有漏網(wǎng)之魚,一般此時(shí)我們還會(huì)搞一個(gè) catch(…) 進(jìn)行全捕獲。
異常的重新拋出
要知道一個(gè) catch 是無(wú)法完全搞定異常的,如果我們對(duì)異常進(jìn)行修正后,希望交付給上層調(diào)用鏈進(jìn)行異常的異常信息日志記錄,此時(shí)就需要我們重新對(duì)上層函數(shù)拋異常:
void func1() { throw string("這是一個(gè)異常"); } void func2() { int* array = new int[10]; func1(); //省略函數(shù)對(duì)應(yīng)實(shí)現(xiàn) //…… delete[] array; } int main() { try { func2(); } catch (const string& s) { cout << s << endl; } catch (...) { cout << "未知異常" << endl; } return 0; }
這里 func2 最后應(yīng)該 delete 進(jìn)行空間釋放,但由于 func2 中途調(diào)用 func1 ,func1 內(nèi)部拋出了一個(gè)異常,這時(shí)會(huì)直接跳轉(zhuǎn)到 main 函數(shù)中的 catch 塊執(zhí)行對(duì)應(yīng)的異常處理程序,并且在處理完后繼續(xù)沿著 catch 塊往后執(zhí)行,這時(shí)就導(dǎo)致 func2 中內(nèi)存塊沒有得到釋放,造成了內(nèi)存泄露!
此時(shí)我們應(yīng)該在 func2 中先對(duì) func1 拋出的異常進(jìn)行捕獲,捕獲后先將內(nèi)存釋放再重新拋出異常,就可以避免內(nèi)存泄露:
void func2() { int* array = new int[10]; try { func1(); //省略函數(shù)對(duì)應(yīng)實(shí)現(xiàn) //…… } catch (...) { delete[] array; throw; //將捕獲到的異常再次重新拋出 } delete[] array; }
try-catch 中 new 和 delete 之間可能還會(huì)拋出其他類型的異常,因此在 fun2 中最好再進(jìn)行 catch(…) ,將申請(qǐng)到的內(nèi)存 delete 后再通過throw 重新拋出;重新拋出異常對(duì)象時(shí),此時(shí) throw 可以不用指明要拋出的異常對(duì)象,其實(shí) catch(…) 也不知道自己到底捕了個(gè)什么異常對(duì)象
安全第一條
還是那句話,道路千萬(wàn)條,拋異常要謹(jǐn)慎:
構(gòu)造函數(shù)完成對(duì)象的構(gòu)造和初始化,最好不要在構(gòu)造函數(shù)中拋出異常,否則可能導(dǎo)致對(duì)象不完整或沒有完全初始化
析構(gòu)函數(shù)完成對(duì)象資源的清理,最好不要在析構(gòu)函數(shù)中拋出異常,否則可能導(dǎo)致內(nèi)存泄露,句柄未關(guān)閉等
C++ 中在 new 和 delete 中拋出異常經(jīng)常是內(nèi)存泄漏的罪魁禍?zhǔn)?,?lock 和 unlock 之間拋出異常導(dǎo)致死鎖,C++ 經(jīng)常使用 RAII 的方式來解決類似問題
規(guī)范使用
站在異常的嚴(yán)謹(jǐn)立場(chǎng)上, C++ 也在盡量提高咱的使用規(guī)范:
在函數(shù)的后面接throw(type1, type2, …),列出這個(gè)函數(shù)可能拋擲的所有異常類型 在函數(shù)的后面接throw()或noexcept(C++11),表示該函數(shù)不拋異常 若無(wú)異常接口聲明,則此函數(shù)可以拋擲任何類型的異常(異常接口聲明不是強(qiáng)制的)
//這里可能會(huì)拋出A/B/C/D類型的異常 void func() throw(A, B, C, D); //這里只會(huì)拋出 bad_alloc 的異常 void* operator new(std::size_t size) throw(std::bad_alloc); //這里不會(huì)拋出異常 void* operator new(std::size_t size, void* ptr) throw();
異常體系
因?yàn)楫惓賹?shí)需要嚴(yán)謹(jǐn)與規(guī)范的操作,所以在很多公司里面都會(huì)制定自己的一套異常的規(guī)范管理:
公司中的項(xiàng)目一般會(huì)進(jìn)行模塊劃分,讓不同的人或小組完成不同的模塊,如果不對(duì)拋異常這件事進(jìn)行規(guī)范,那么在最外層捕獲異常的冤種就會(huì)問候親媽了,因?yàn)樗麜?huì)來給各位擦屁股,捕獲大家拋出的所以異常對(duì)象 \color{red} {那么在最外層捕獲異常的冤種就會(huì)問候親媽了,因?yàn)樗麜?huì)來給各位擦屁股,捕獲大家拋出的所以異常對(duì)象}那么在最外層捕獲異常的冤種就會(huì)問候親媽了,因?yàn)樗麜?huì)來給各位擦屁股,捕獲大家拋出的所以異常對(duì)象
我們之前說過異常語(yǔ)法可以用基類捕獲拋出的派生類對(duì)象,因此實(shí)際中都會(huì)先定義一個(gè)最基礎(chǔ)的異常類,所有人拋出的異常對(duì)象都必須是繼承于該異常類的派生類對(duì)象,,因此最外層就只需捕獲基類就行了
?最基礎(chǔ)的異常類至少需要包含錯(cuò)誤編號(hào)和錯(cuò)誤描述兩個(gè)成員變量,甚至還可以包含當(dāng)前函數(shù)棧幀的調(diào)用鏈等信息,該異常類中一般還會(huì)提供兩個(gè)成員函數(shù),分別用來獲取錯(cuò)誤編號(hào)和錯(cuò)誤描述
class Exception { public: Exception(int errid, const char* errmsg) :_errid(errid) , _errmsg(errmsg) {} int GetErrid() const { return _errid; } virtual string what() const { return _errmsg; } protected: int _errid; //錯(cuò)誤編號(hào) string _errmsg; //錯(cuò)誤描述 //... };
其他人如果要對(duì)這個(gè)異常類進(jìn)行擴(kuò)展,必須先繼承基礎(chǔ)異常類,然后按需添加某些成員變量,或是對(duì)虛函數(shù)what 進(jìn)行重寫,使其能告知更多的異常信息:
class CacheException : public Exception { public: CacheException(int errid, const char* errmsg) :Exception(errid, errmsg) {} virtual string what() const { string msg = "CacheException: "; msg += _errmsg; return msg; } protected: //... }; class SqlException : public Exception { public: SqlException(int errid, const char* errmsg, const char* sql) :Exception(errid, errmsg) , _sql(sql) {} virtual string what() const { string msg = "CacheException: "; msg += _errmsg; msg += "sql語(yǔ)句: "; msg += _sql; return msg; } protected: string _sql; //異常的SQL語(yǔ)句 //... };
異常類的成員變量不能設(shè)置為私有,因?yàn)樗接谐蓡T在子類中是不可見的?;?Exception 中 what 成員函數(shù)最好定義為虛函數(shù),方便子類對(duì)其進(jìn)行重寫,從而達(dá)到多態(tài)的效果
標(biāo)準(zhǔn)庫(kù)體系
C++ 標(biāo)準(zhǔn)庫(kù)當(dāng)中的異常也是一個(gè)基礎(chǔ)體系,其中 exception 就是基類,它與其他異常類的繼承關(guān)系如下:
?其中具體信息如下:
?我們可以去繼承這里的 exception 類來實(shí)現(xiàn)自己的異常類,但實(shí)際上很多公司都會(huì)自己定義一套異常繼承體系!
優(yōu)缺點(diǎn)
目前情況來看,異常是利大于弊的,還是鼓勵(lì)使用異常的,而且前排的語(yǔ)言基本都會(huì)使用異常處理錯(cuò)誤,這也可以看出這是大勢(shì)所趨
異常的優(yōu)點(diǎn):
相比錯(cuò)誤碼,異??梢郧逦鷾?zhǔn)確的展示出錯(cuò)誤的各種信息,甚至可以包含堆棧調(diào)用等信息,這樣可以幫助更好的定位程序的bug
返回錯(cuò)誤碼的傳統(tǒng)方式有個(gè)很大的問題就是,在函數(shù)調(diào)用鏈中,深層的函數(shù)返回了錯(cuò)誤,那么我們得層層返回錯(cuò)誤碼,最終最外層才能拿到錯(cuò)誤
很多的第三方庫(kù)都會(huì)使用異常,比如 boost、gtest、gmock 等常用的庫(kù),如果我們不用異常就不能很好的發(fā)揮這些庫(kù)的作用,很多測(cè)試框架也都使用異常,因此使用異常能更好的使用單元測(cè)試等進(jìn)行白盒測(cè)試
部分函數(shù)使用異常更好處理,比如 T& operator 這樣的函數(shù),如果 pos 越界了只能使用異?;蛘呓K止程序處理,沒辦法通過返回值表示錯(cuò)誤
異常的缺點(diǎn):
異常會(huì)導(dǎo)致程序的執(zhí)行流混亂,這會(huì)導(dǎo)致我們跟蹤調(diào)試或分析程序時(shí)比較困難。異常還會(huì)有一些性能的開銷,當(dāng)然在現(xiàn)代硬件速度很快的情況下,這個(gè)影響基本忽略不計(jì)!
C++ 沒有垃圾回收機(jī)制,資源需要自己管理,有了異常非常容易導(dǎo)致內(nèi)存泄露、死鎖等異常安全問題,這個(gè)需要使用 RAII 來處理資源的管理問題,學(xué)習(xí)成本比較高
C++ 標(biāo)準(zhǔn)庫(kù)的異常體系定義得不夠好,導(dǎo)致大家各自定義自己的異常體系,非常的混亂,異常盡量規(guī)范使用,否則后果不堪設(shè)想,隨意拋異常,也會(huì)讓外層捕獲的用戶苦不堪言。
異常接口聲明不是強(qiáng)制的,對(duì)于沒有聲明異常類型的函數(shù),無(wú)法預(yù)知該函數(shù)是否會(huì)拋出異常
以上就是深度剖析C++中的異常機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于C++ 異常機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++?反匯編之關(guān)于Switch語(yǔ)句的優(yōu)化措施
這篇文章主要介紹了C++?反匯編之關(guān)于Switch語(yǔ)句的優(yōu)化措施,利用三種優(yōu)化來降低樹高度,誰(shuí)的效率高就優(yōu)先使用誰(shuí),三種優(yōu)化都無(wú)法匹配才會(huì)使用判定樹,具體內(nèi)容詳情跟隨小編一起看看吧2022-01-01C++面試八股文之std::string實(shí)現(xiàn)方法
這篇文章主要介紹了C++面試八股文:std::string是如何實(shí)現(xiàn)的,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)基于控制臺(tái)的電子時(shí)鐘,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05