C++中異常的深度解析
1 異常的概念及使用
1.1 異常的概念
1>.異常處理機制允許程序中獨立開發(fā)部分能夠在運行時就出現(xiàn)的問題進行通信并做出相應的處理,異常使得我們能夠?qū)栴}的檢測與解決問題的過程分開,程序的一部分負責檢測問題的出現(xiàn),然后解決問題的任務傳遞給出現(xiàn)的另一部分,檢測環(huán)節(jié)無須知道問題的處理模塊的所有細節(jié)。
2>.C語言主要是通過錯誤碼的形式處理錯誤,錯誤碼本質(zhì)上就是對錯誤信息進行分類編號,拿到錯誤碼以后還需要我們自己去查詢錯誤信息,比較麻煩。異常時會拋出一個對象,這個對象可以涵蓋更全面的各種信息。
1.2 異常的拋出和捕獲
1>.程序出現(xiàn)問題時,我們通過拋出(throw)一個對象來引發(fā)一個異常,該對象的類型以及當前的調(diào)用鏈決定了改由哪個catch的處理代碼來處理異常。
2>.被選中的處理代碼是調(diào)用鏈中與該類型匹配且離拋出異常的位置最近的那一個catch的處理代碼。根據(jù)拋出對象的類型與內(nèi)容,程序的拋出異常部分要告知異常處理部分到底發(fā)生了什么錯誤。
3>.當throw執(zhí)行時,throw后面的語句將不再被執(zhí)行。程序的執(zhí)行從throw位置會跳到與之匹配的catch模塊,catch可能是同一個函數(shù)中的一個局部catch模塊,也可能是調(diào)用鏈中的另一個函數(shù)中的catch模塊,控制權(quán)從throw位置轉(zhuǎn)移到了catch模塊的位置。這里還有兩個重要的含義:1.沿著調(diào)用鏈的函數(shù)可能會提早推出;2.一旦程序開始執(zhí)行異常處理程序,沿著調(diào)用鏈創(chuàng)建的對象都將會自動被編譯器銷毀。
4>.拋出異常對象后,會生成一個異常對象的拷貝,因為拋出的異常對象可能是一個局部對象,所以會生成一個拷貝對象,這個拷貝的對象會在catch模塊結(jié)束后就被銷毀了。(這里的處理類似于函數(shù)的傳值返回)
5>.在C++的異常處理過程中,我們常常選擇使用try-catch去處理異常,我們這里就先來講解一下這個try-catch:1.try:表示將有可能出現(xiàn)異常的代碼書寫在try代碼塊中;2.catch:try不能單獨使用,必須結(jié)合catch / finally / catch-finally(這里try結(jié)合catch),catch也不能單獨使用,必須結(jié)合try一起用。
int Divide(int a, int b) { try { if (b == 0)//如果b等于0,就拋異常。 { string s("Divide by zero condition!"); throw s;//這里會將類型為string的對象s拋出去,去找這條調(diào)用鏈中與s這個對象類型匹配且離拋出異常的哪個位置的那一個catch代碼塊(拋出的并不是s對象,而是s這個異常對象的一個拷貝對象)。 } else { return a / b; } } catch (int errid)//catch這個代碼塊接收的是int類型的一個對象。 { cout << errid << endl; } } void Func(int a, int b) { try { cout << Divide(a, b) << endl; } catch (const char* errmsg) { cout << errmsg << endl; } } int main() { int a = 0, b = 0; cin >> a >> b; try { Func(a, b); } catch (const string* errmsg)//catch接收的是一個string類型的對象。 { cout << errmsg << endl; } return 0; }//我們開始運行程序,輸入兩個變量分別為10和0,在main函數(shù)中進入try代碼塊中,去調(diào)用Func這個函數(shù),進入Func這個局部棧幀中,又進入到try代碼中,再去調(diào)用Divide函數(shù),首先,又去進入到try代碼塊中,由于b==0,會進入到if語句中去執(zhí)行代碼,通過throw來將s對象跑出來引發(fā)異常,編譯器這里會順著調(diào)用鏈去找接收string類型對象的catch模塊,首先找到第15這行代碼的catch模塊(因為離throw的位置最近),類型不符合,再順著調(diào)用鏈去找,找到第26這行代碼的catch模塊,類型又不符合,再找到第39這行代碼的catch模塊,OK了,類型符合,就是errmsg這個對象接收到了Divide函數(shù)中跑出來的哪個對象s,既然是第39這行代碼的catch模塊接收了,那么程序的執(zhí)行就從拋出的位置跳到了第39這行代碼的catch模塊這里來了。 //當我們在Divide函數(shù)中拋出s對象時,那么第7這句代碼之后的語句將不會再被執(zhí)行(僅限于Divide這個棧幀中),而且Func函數(shù)在這里其實是提早退出了的,F(xiàn)unc這個函數(shù)中,如果在調(diào)用了這個函數(shù)之前多余開辟了空間的話,那么編譯器在這里會自動地將Func函數(shù)中開辟的那塊空間給銷毀掉。
上述函數(shù)的調(diào)用鏈:
1.3 棧展開
1>.拋出異常后,程序暫停當前函數(shù)的執(zhí)行,開始尋找與之匹配的catch子句,首先檢查throw本身是否在try模塊的內(nèi)部,如果在的話則查找匹配的那個catch模塊,如果有匹配的,則跳到那個與之匹配的catch模塊的那個地方去進行處理。
2>.如果當前所在的這個函數(shù)中沒有try/catch,或者有try/catch子句但是類型不匹配,則退出當前函數(shù),進行在外層調(diào)用函數(shù)鏈中去查找,上述查找的catch模塊的過程被稱之為是棧展開。
3>.如果我們到達main函數(shù)的棧幀,并且依舊沒有找到與之匹配的catch模塊,那么程序在這里會自動去調(diào)用標準庫中的terminate這個函數(shù)去終止程序,簡單來說就是報錯。
4>.如果找到匹配的catch模塊去處理后,catch模塊中的以及后續(xù)的代碼則會進行執(zhí)行。
上圖就是一個棧展開的過程。
1.4 查找匹配的處理代碼
1>.一般情況下拋儲對象和catch接收的那個對象的類型是完全匹配的,如果有多個類型匹配的catch子句,那么就選擇離他位置更近的那個catch子句。
2>.但是也有一些例外,允許從非常量向常量的類型準換,也就是權(quán)限縮??;允許數(shù)組轉(zhuǎn)換成指向數(shù)組元素類型的指針,函數(shù)被轉(zhuǎn)換成指向函數(shù)的指針;允許從派生類向基類類型的轉(zhuǎn)換,這一點非常實用,實際中繼承體系基本都是用這個方式去設計的。
3>.如果到main函數(shù)中,異常人就沒有被匹配的話就會被終止程序,不是發(fā)生嚴重錯誤的情況下,我們是不期望程序最終的,所以一般的main函數(shù)中在最后都會使用catch(...),它可以捕獲任意類型的異常,但是我們是不知道異常的錯誤是什么。注:一個try模塊我們可以搭配多個catch模塊。
//由于時間等等各種原因,我們這里就不一一為大家展示匹配的過程代碼了,我們接下來就來模擬設計一個繼承的匹配機制。 class person { public: person(const string& name) :_name(name) { } protected: string _name; }; class student :public person { public: student(const string& name, int id) :person(name) , _id(id) { } private: int _id; }; class teacher :public person { public: teacher(const string& name, int teach) :person(name) , _teach(teach) { } private: int _teach; }; void Print() { if (rand() % 5 == 0) { throw student("學號", 20); } else if (rand() % 2 == 0) { throw teacher("工號", 32); } else { throw string(); } } int main() { try { Print(); } catch (const person& p) { }//可以捕捉所有繼承了person類型的對象。 catch (...)//可以捕捉任意類型的異常對象。 { } return 0; }//好了,我們這里直接來看Print函數(shù)中拋異常的操作,首先看第36到39這段代碼,它拋出的student類型的對象,在第55到56這段代碼中的catch子句被捕獲了,派生類的對象被基類類型的對象給捕獲了;再來看第40到43這段代碼,它拋出的是一個teacher類型的對象,在第55到56這段代碼中的catch子句被捕獲了,teacher這個派生類對象被person這個基類對象給捕獲了;最后看第44到47這段代碼,它所拋出的是一個string類型的對象,是被第57到58這段代碼中的catch子句捕獲的,第55到56這段代碼中的catch子句它主要捕獲的是person類型的對象以及繼承了person類的派生類對象,string類型與其不匹配,第55到56這段代碼中的catch子句捕獲不到,而第57到58這段代碼中的catch子句可以捕捉到任意類型的異常對象,因此就被第57到58這段代碼中的catch子句給捕捉到了。
1.5 異常重新拋出
1>.有時catch到一個異常對象后,需要對錯誤進行分類,其中的某種異常錯誤需要進行特殊的處理,其他錯誤則重新拋出異常給外層調(diào)用鏈處理。捕獲異常需要重新拋出,直接throw;就可以把捕捉到的對象再次拋出。
void Print() { int a = rand() % 2; try { throw string(); } catch (string& s) { if (a == 1) { throw;//如果a==1的話,就將捕獲到的那個string類型的對象再次拋出。 } else { cout << s << endl; } } } int main() { try { Print(); } catch (string& s)//Print函數(shù)將捕捉到的那個對象重新拋出后,被這個catch子句重新捕捉到了。 { cout << s << endl; } return 0; }
1.6 異常安全問題
1>.異常拋出后,后面的代碼就不再執(zhí)行了,前面申請了資源(內(nèi)存、鎖等),后面要進行釋放(這里指的是我們自己用new/malloc向內(nèi)存申請的一塊資源,它在釋放時需要我們自己去調(diào)用delete函數(shù)),但是中間可能會拋異常就會導致資源沒有釋放,這里由于異常就引發(fā)了資源泄露,會產(chǎn)生安全性的問題。為了解決這個問題,那么我們就要在拋出到外層調(diào)用鏈之前要提前捕獲到這個異常對象,將那些資源釋放之后再將其重新拋出。當然我們下一章要講解的智能指針章節(jié)中所講的RALL方式解決這種問題時更好的。
2>.其次在析構(gòu)函數(shù)中,如果在析構(gòu)函數(shù)的過程中拋出了異常的話,那么就也需要慎重處理(在C類語言中,只要是開創(chuàng)資源的函數(shù),如new、malloc或釋放資源的函數(shù),如free、delete,這幾個函數(shù)都有可能會拋異常),比如析構(gòu)函數(shù)要釋放10個資源,在釋放到第5個時拋出異常,則也需要捕獲處理,否則的話后面的5個資源就沒有釋放,也會造成資源泄露。
void Print() { int* array = new int[10] {0};//創(chuàng)建一個int類型的數(shù)組空間,數(shù)組的對象為10。 try { string s; throw s;//拋出一個string類型的對象。 } catch (...)//我們在拋出異常對象之前就申請了一塊有10個int類型空間大小的資源,為了防止出現(xiàn)資源泄露的問題,異常,我們需要Print函數(shù)內(nèi)部就捕獲到了這個異常對象,等將array執(zhí)行的那塊資源說服力之后,再將捕獲到的那個異常對象重新拋出即可。 { delete[] array; throw;//將捕獲的那個對象重新拋出。 } delete[] array;//如果這里并不會拋異常的話,編譯器不會走catch子句,異常這里還需再寫上一句刪除array指向的那塊資源的代碼。 }
1.7 異常規(guī)范
1>.對于用戶和編譯器而言,預先知道某個程序會不會拋出異常大有益處,知道某個函數(shù)是否會拋出異常會有助于簡化調(diào)用函數(shù)的代碼。
2>.C++98中函數(shù)參數(shù)列表的后面接throw(),表示該函數(shù)不會拋異常,函數(shù)參數(shù)列表的后面接throw(類型1,類型2,...)表示可能會拋出多種類型的異常,將可能會拋出的類型之間均用逗號分割。
3>.C++98的這種方式有點過于復雜,在實踐中其實并不好用,C++11中對其進行了簡化,函數(shù)參數(shù)列表后面若加noexcept這個關(guān)鍵字就表示該函數(shù)不會拋異常,若啥都不加的話則表示可能會拋出異常。
4>.編譯器并不會在編譯時去檢查noexcept修飾了,也就是說如果一個函數(shù)用noexcept修飾了,但是同時又包含了throw語句或者調(diào)用的函數(shù)可能會拋出異常,編譯器還是會順利通過的(有些編譯器可能會報個警告)。但是如果一個聲明了noexcept的函數(shù)拋出了異常的話,程序便會去調(diào)用terminate終止程序。
5>.noexcept(expression)還可以作為一個運算符去檢測一個表達式是否會拋出異常,可能會拋出異常的話則返回false,不會的話就會返回true。
void Print()noexcept { int a = 0; cin >> a; if (a == 10) { throw "a==10"; } } int main() { try { Print(); } catch (char* errmsg) { cout << errmsg << endl; } return 0; }//如果我們大家仔細看上述這段代碼時,稍微有一點問題,Print函數(shù)有拋出異常的風險,但是Print函數(shù)的參數(shù)后面加了noexcept這個關(guān)鍵字,理論上來說的話是不能加這個關(guān)鍵字的,通過前面的解析,我們可知,這種情況下有的編譯器是不會報錯的。我們現(xiàn)在來運行這個代碼來看一下,如果我們輸入5的話,編譯器確實不會報錯,而且還完整地運行了下來,但如果我們輸入10的話,程序在這里就別破中止運行了,原因是因為Print這個用noexcept修飾的函數(shù)在運行時拋出了一個異常對象。
2 標準庫的異常
1>.C++標準庫也定義了一套自己的異常繼承體系,基類是exception;所以我們?nèi)粘T趯懗绦驎r,需要在主函數(shù)捕獲exception即可,要獲取異常信息,調(diào)用what函數(shù),what函數(shù)是一個虛函數(shù),派生類可以重寫。
OK,今天我們就先講到這里了,那么,我們下一篇再見,謝謝大家的支持!
到此這篇關(guān)于C++中異常的深度解析的文章就介紹到這了,更多相關(guān)C++異常內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++中的std::funture和std::promise實例詳解
在線程池中獲取線程執(zhí)行函數(shù)的返回值時,通常使用 std::future 而不是 std::promise 來傳遞返回值,這篇文章主要介紹了C++中的std::funture和std::promise實例詳解,需要的朋友可以參考下2024-05-05C數(shù)據(jù)結(jié)構(gòu)中串簡單實例
這篇文章主要介紹了C數(shù)據(jù)結(jié)構(gòu)中串簡單實例的相關(guān)資料,需要的朋友可以參考下2017-06-06C++結(jié)構(gòu)體與類指針知識點總結(jié)
在本篇文章里小編給大家整理了關(guān)于C++結(jié)構(gòu)體與類指針知識點以及相關(guān)內(nèi)容,有興趣的朋友們參考學習下。2019-09-09