C++中異常的深度解析
1 異常的概念及使用
1.1 異常的概念
1>.異常處理機(jī)制允許程序中獨(dú)立開(kāi)發(fā)部分能夠在運(yùn)行時(shí)就出現(xiàn)的問(wèn)題進(jìn)行通信并做出相應(yīng)的處理,異常使得我們能夠?qū)?wèn)題的檢測(cè)與解決問(wèn)題的過(guò)程分開(kāi),程序的一部分負(fù)責(zé)檢測(cè)問(wèn)題的出現(xiàn),然后解決問(wèn)題的任務(wù)傳遞給出現(xiàn)的另一部分,檢測(cè)環(huán)節(jié)無(wú)須知道問(wèn)題的處理模塊的所有細(xì)節(jié)。
2>.C語(yǔ)言主要是通過(guò)錯(cuò)誤碼的形式處理錯(cuò)誤,錯(cuò)誤碼本質(zhì)上就是對(duì)錯(cuò)誤信息進(jìn)行分類(lèi)編號(hào),拿到錯(cuò)誤碼以后還需要我們自己去查詢(xún)錯(cuò)誤信息,比較麻煩。異常時(shí)會(huì)拋出一個(gè)對(duì)象,這個(gè)對(duì)象可以涵蓋更全面的各種信息。
1.2 異常的拋出和捕獲
1>.程序出現(xiàn)問(wèn)題時(shí),我們通過(guò)拋出(throw)一個(gè)對(duì)象來(lái)引發(fā)一個(gè)異常,該對(duì)象的類(lèi)型以及當(dāng)前的調(diào)用鏈決定了改由哪個(gè)catch的處理代碼來(lái)處理異常。
2>.被選中的處理代碼是調(diào)用鏈中與該類(lèi)型匹配且離拋出異常的位置最近的那一個(gè)catch的處理代碼。根據(jù)拋出對(duì)象的類(lèi)型與內(nèi)容,程序的拋出異常部分要告知異常處理部分到底發(fā)生了什么錯(cuò)誤。
3>.當(dāng)throw執(zhí)行時(shí),throw后面的語(yǔ)句將不再被執(zhí)行。程序的執(zhí)行從throw位置會(huì)跳到與之匹配的catch模塊,catch可能是同一個(gè)函數(shù)中的一個(gè)局部catch模塊,也可能是調(diào)用鏈中的另一個(gè)函數(shù)中的catch模塊,控制權(quán)從throw位置轉(zhuǎn)移到了catch模塊的位置。這里還有兩個(gè)重要的含義:1.沿著調(diào)用鏈的函數(shù)可能會(huì)提早推出;2.一旦程序開(kāi)始執(zhí)行異常處理程序,沿著調(diào)用鏈創(chuàng)建的對(duì)象都將會(huì)自動(dòng)被編譯器銷(xiāo)毀。
4>.拋出異常對(duì)象后,會(huì)生成一個(gè)異常對(duì)象的拷貝,因?yàn)閽伋龅漠惓?duì)象可能是一個(gè)局部對(duì)象,所以會(huì)生成一個(gè)拷貝對(duì)象,這個(gè)拷貝的對(duì)象會(huì)在catch模塊結(jié)束后就被銷(xiāo)毀了。(這里的處理類(lèi)似于函數(shù)的傳值返回)
5>.在C++的異常處理過(guò)程中,我們常常選擇使用try-catch去處理異常,我們這里就先來(lái)講解一下這個(gè)try-catch:1.try:表示將有可能出現(xiàn)異常的代碼書(shū)寫(xiě)在try代碼塊中;2.catch:try不能單獨(dú)使用,必須結(jié)合catch / finally / catch-finally(這里try結(jié)合catch),catch也不能單獨(dú)使用,必須結(jié)合try一起用。
int Divide(int a, int b)
{
try
{
if (b == 0)//如果b等于0,就拋異常。
{
string s("Divide by zero condition!");
throw s;//這里會(huì)將類(lèi)型為string的對(duì)象s拋出去,去找這條調(diào)用鏈中與s這個(gè)對(duì)象類(lèi)型匹配且離拋出異常的哪個(gè)位置的那一個(gè)catch代碼塊(拋出的并不是s對(duì)象,而是s這個(gè)異常對(duì)象的一個(gè)拷貝對(duì)象)。
}
else
{
return a / b;
}
}
catch (int errid)//catch這個(gè)代碼塊接收的是int類(lèi)型的一個(gè)對(duì)象。
{
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接收的是一個(gè)string類(lèi)型的對(duì)象。
{
cout << errmsg << endl;
}
return 0;
}//我們開(kāi)始運(yùn)行程序,輸入兩個(gè)變量分別為10和0,在main函數(shù)中進(jìn)入try代碼塊中,去調(diào)用Func這個(gè)函數(shù),進(jìn)入Func這個(gè)局部棧幀中,又進(jìn)入到try代碼中,再去調(diào)用Divide函數(shù),首先,又去進(jìn)入到try代碼塊中,由于b==0,會(huì)進(jìn)入到if語(yǔ)句中去執(zhí)行代碼,通過(guò)throw來(lái)將s對(duì)象跑出來(lái)引發(fā)異常,編譯器這里會(huì)順著調(diào)用鏈去找接收string類(lèi)型對(duì)象的catch模塊,首先找到第15這行代碼的catch模塊(因?yàn)殡xthrow的位置最近),類(lèi)型不符合,再順著調(diào)用鏈去找,找到第26這行代碼的catch模塊,類(lèi)型又不符合,再找到第39這行代碼的catch模塊,OK了,類(lèi)型符合,就是errmsg這個(gè)對(duì)象接收到了Divide函數(shù)中跑出來(lái)的哪個(gè)對(duì)象s,既然是第39這行代碼的catch模塊接收了,那么程序的執(zhí)行就從拋出的位置跳到了第39這行代碼的catch模塊這里來(lái)了。
//當(dāng)我們?cè)贒ivide函數(shù)中拋出s對(duì)象時(shí),那么第7這句代碼之后的語(yǔ)句將不會(huì)再被執(zhí)行(僅限于Divide這個(gè)棧幀中),而且Func函數(shù)在這里其實(shí)是提早退出了的,F(xiàn)unc這個(gè)函數(shù)中,如果在調(diào)用了這個(gè)函數(shù)之前多余開(kāi)辟了空間的話,那么編譯器在這里會(huì)自動(dòng)地將Func函數(shù)中開(kāi)辟的那塊空間給銷(xiāo)毀掉。上述函數(shù)的調(diào)用鏈:

1.3 棧展開(kāi)
1>.拋出異常后,程序暫停當(dāng)前函數(shù)的執(zhí)行,開(kāi)始尋找與之匹配的catch子句,首先檢查throw本身是否在try模塊的內(nèi)部,如果在的話則查找匹配的那個(gè)catch模塊,如果有匹配的,則跳到那個(gè)與之匹配的catch模塊的那個(gè)地方去進(jìn)行處理。
2>.如果當(dāng)前所在的這個(gè)函數(shù)中沒(méi)有try/catch,或者有try/catch子句但是類(lèi)型不匹配,則退出當(dāng)前函數(shù),進(jìn)行在外層調(diào)用函數(shù)鏈中去查找,上述查找的catch模塊的過(guò)程被稱(chēng)之為是棧展開(kāi)。
3>.如果我們到達(dá)main函數(shù)的棧幀,并且依舊沒(méi)有找到與之匹配的catch模塊,那么程序在這里會(huì)自動(dòng)去調(diào)用標(biāo)準(zhǔn)庫(kù)中的terminate這個(gè)函數(shù)去終止程序,簡(jiǎn)單來(lái)說(shuō)就是報(bào)錯(cuò)。
4>.如果找到匹配的catch模塊去處理后,catch模塊中的以及后續(xù)的代碼則會(huì)進(jìn)行執(zhí)行。

上圖就是一個(gè)棧展開(kāi)的過(guò)程。
1.4 查找匹配的處理代碼
1>.一般情況下拋儲(chǔ)對(duì)象和catch接收的那個(gè)對(duì)象的類(lèi)型是完全匹配的,如果有多個(gè)類(lèi)型匹配的catch子句,那么就選擇離他位置更近的那個(gè)catch子句。
2>.但是也有一些例外,允許從非常量向常量的類(lèi)型準(zhǔn)換,也就是權(quán)限縮??;允許數(shù)組轉(zhuǎn)換成指向數(shù)組元素類(lèi)型的指針,函數(shù)被轉(zhuǎn)換成指向函數(shù)的指針;允許從派生類(lèi)向基類(lèi)類(lèi)型的轉(zhuǎn)換,這一點(diǎn)非常實(shí)用,實(shí)際中繼承體系基本都是用這個(gè)方式去設(shè)計(jì)的。
3>.如果到main函數(shù)中,異常人就沒(méi)有被匹配的話就會(huì)被終止程序,不是發(fā)生嚴(yán)重錯(cuò)誤的情況下,我們是不期望程序最終的,所以一般的main函數(shù)中在最后都會(huì)使用catch(...),它可以捕獲任意類(lèi)型的異常,但是我們是不知道異常的錯(cuò)誤是什么。注:一個(gè)try模塊我們可以搭配多個(gè)catch模塊。
//由于時(shí)間等等各種原因,我們這里就不一一為大家展示匹配的過(guò)程代碼了,我們接下來(lái)就來(lái)模擬設(shè)計(jì)一個(gè)繼承的匹配機(jī)制。
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("學(xué)號(hào)", 20);
}
else if (rand() % 2 == 0)
{
throw teacher("工號(hào)", 32);
}
else
{
throw string();
}
}
int main()
{
try
{
Print();
}
catch (const person& p)
{ }//可以捕捉所有繼承了person類(lèi)型的對(duì)象。
catch (...)//可以捕捉任意類(lèi)型的異常對(duì)象。
{ }
return 0;
}//好了,我們這里直接來(lái)看Print函數(shù)中拋異常的操作,首先看第36到39這段代碼,它拋出的student類(lèi)型的對(duì)象,在第55到56這段代碼中的catch子句被捕獲了,派生類(lèi)的對(duì)象被基類(lèi)類(lèi)型的對(duì)象給捕獲了;再來(lái)看第40到43這段代碼,它拋出的是一個(gè)teacher類(lèi)型的對(duì)象,在第55到56這段代碼中的catch子句被捕獲了,teacher這個(gè)派生類(lèi)對(duì)象被person這個(gè)基類(lèi)對(duì)象給捕獲了;最后看第44到47這段代碼,它所拋出的是一個(gè)string類(lèi)型的對(duì)象,是被第57到58這段代碼中的catch子句捕獲的,第55到56這段代碼中的catch子句它主要捕獲的是person類(lèi)型的對(duì)象以及繼承了person類(lèi)的派生類(lèi)對(duì)象,string類(lèi)型與其不匹配,第55到56這段代碼中的catch子句捕獲不到,而第57到58這段代碼中的catch子句可以捕捉到任意類(lèi)型的異常對(duì)象,因此就被第57到58這段代碼中的catch子句給捕捉到了。1.5 異常重新拋出
1>.有時(shí)catch到一個(gè)異常對(duì)象后,需要對(duì)錯(cuò)誤進(jìn)行分類(lèi),其中的某種異常錯(cuò)誤需要進(jìn)行特殊的處理,其他錯(cuò)誤則重新拋出異常給外層調(diào)用鏈處理。捕獲異常需要重新拋出,直接throw;就可以把捕捉到的對(duì)象再次拋出。
void Print()
{
int a = rand() % 2;
try
{
throw string();
}
catch (string& s)
{
if (a == 1)
{
throw;//如果a==1的話,就將捕獲到的那個(gè)string類(lèi)型的對(duì)象再次拋出。
}
else
{
cout << s << endl;
}
}
}
int main()
{
try
{
Print();
}
catch (string& s)//Print函數(shù)將捕捉到的那個(gè)對(duì)象重新拋出后,被這個(gè)catch子句重新捕捉到了。
{
cout << s << endl;
}
return 0;
}1.6 異常安全問(wèn)題
1>.異常拋出后,后面的代碼就不再執(zhí)行了,前面申請(qǐng)了資源(內(nèi)存、鎖等),后面要進(jìn)行釋放(這里指的是我們自己用new/malloc向內(nèi)存申請(qǐng)的一塊資源,它在釋放時(shí)需要我們自己去調(diào)用delete函數(shù)),但是中間可能會(huì)拋異常就會(huì)導(dǎo)致資源沒(méi)有釋放,這里由于異常就引發(fā)了資源泄露,會(huì)產(chǎn)生安全性的問(wèn)題。為了解決這個(gè)問(wèn)題,那么我們就要在拋出到外層調(diào)用鏈之前要提前捕獲到這個(gè)異常對(duì)象,將那些資源釋放之后再將其重新拋出。當(dāng)然我們下一章要講解的智能指針章節(jié)中所講的RALL方式解決這種問(wèn)題時(shí)更好的。
2>.其次在析構(gòu)函數(shù)中,如果在析構(gòu)函數(shù)的過(guò)程中拋出了異常的話,那么就也需要慎重處理(在C類(lèi)語(yǔ)言中,只要是開(kāi)創(chuàng)資源的函數(shù),如new、malloc或釋放資源的函數(shù),如free、delete,這幾個(gè)函數(shù)都有可能會(huì)拋異常),比如析構(gòu)函數(shù)要釋放10個(gè)資源,在釋放到第5個(gè)時(shí)拋出異常,則也需要捕獲處理,否則的話后面的5個(gè)資源就沒(méi)有釋放,也會(huì)造成資源泄露。
void Print()
{
int* array = new int[10] {0};//創(chuàng)建一個(gè)int類(lèi)型的數(shù)組空間,數(shù)組的對(duì)象為10。
try
{
string s;
throw s;//拋出一個(gè)string類(lèi)型的對(duì)象。
}
catch (...)//我們?cè)趻伋霎惓?duì)象之前就申請(qǐng)了一塊有10個(gè)int類(lèi)型空間大小的資源,為了防止出現(xiàn)資源泄露的問(wèn)題,異常,我們需要Print函數(shù)內(nèi)部就捕獲到了這個(gè)異常對(duì)象,等將array執(zhí)行的那塊資源說(shuō)服力之后,再將捕獲到的那個(gè)異常對(duì)象重新拋出即可。
{
delete[] array;
throw;//將捕獲的那個(gè)對(duì)象重新拋出。
}
delete[] array;//如果這里并不會(huì)拋異常的話,編譯器不會(huì)走catch子句,異常這里還需再寫(xiě)上一句刪除array指向的那塊資源的代碼。
}1.7 異常規(guī)范
1>.對(duì)于用戶和編譯器而言,預(yù)先知道某個(gè)程序會(huì)不會(huì)拋出異常大有益處,知道某個(gè)函數(shù)是否會(huì)拋出異常會(huì)有助于簡(jiǎn)化調(diào)用函數(shù)的代碼。
2>.C++98中函數(shù)參數(shù)列表的后面接throw(),表示該函數(shù)不會(huì)拋異常,函數(shù)參數(shù)列表的后面接throw(類(lèi)型1,類(lèi)型2,...)表示可能會(huì)拋出多種類(lèi)型的異常,將可能會(huì)拋出的類(lèi)型之間均用逗號(hào)分割。
3>.C++98的這種方式有點(diǎn)過(guò)于復(fù)雜,在實(shí)踐中其實(shí)并不好用,C++11中對(duì)其進(jìn)行了簡(jiǎn)化,函數(shù)參數(shù)列表后面若加noexcept這個(gè)關(guān)鍵字就表示該函數(shù)不會(huì)拋異常,若啥都不加的話則表示可能會(huì)拋出異常。
4>.編譯器并不會(huì)在編譯時(shí)去檢查noexcept修飾了,也就是說(shuō)如果一個(gè)函數(shù)用noexcept修飾了,但是同時(shí)又包含了throw語(yǔ)句或者調(diào)用的函數(shù)可能會(huì)拋出異常,編譯器還是會(huì)順利通過(guò)的(有些編譯器可能會(huì)報(bào)個(gè)警告)。但是如果一個(gè)聲明了noexcept的函數(shù)拋出了異常的話,程序便會(huì)去調(diào)用terminate終止程序。
5>.noexcept(expression)還可以作為一個(gè)運(yùn)算符去檢測(cè)一個(gè)表達(dá)式是否會(huì)拋出異常,可能會(huì)拋出異常的話則返回false,不會(huì)的話就會(huì)返回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;
}//如果我們大家仔細(xì)看上述這段代碼時(shí),稍微有一點(diǎn)問(wèn)題,Print函數(shù)有拋出異常的風(fēng)險(xiǎn),但是Print函數(shù)的參數(shù)后面加了noexcept這個(gè)關(guān)鍵字,理論上來(lái)說(shuō)的話是不能加這個(gè)關(guān)鍵字的,通過(guò)前面的解析,我們可知,這種情況下有的編譯器是不會(huì)報(bào)錯(cuò)的。我們現(xiàn)在來(lái)運(yùn)行這個(gè)代碼來(lái)看一下,如果我們輸入5的話,編譯器確實(shí)不會(huì)報(bào)錯(cuò),而且還完整地運(yùn)行了下來(lái),但如果我們輸入10的話,程序在這里就別破中止運(yùn)行了,原因是因?yàn)镻rint這個(gè)用noexcept修飾的函數(shù)在運(yùn)行時(shí)拋出了一個(gè)異常對(duì)象。2 標(biāo)準(zhǔn)庫(kù)的異常
1>.C++標(biāo)準(zhǔn)庫(kù)也定義了一套自己的異常繼承體系,基類(lèi)是exception;所以我們?nèi)粘T趯?xiě)程序時(shí),需要在主函數(shù)捕獲exception即可,要獲取異常信息,調(diào)用what函數(shù),what函數(shù)是一個(gè)虛函數(shù),派生類(lèi)可以重寫(xiě)。
OK,今天我們就先講到這里了,那么,我們下一篇再見(jiàn),謝謝大家的支持!
到此這篇關(guān)于C++中異常的深度解析的文章就介紹到這了,更多相關(guān)C++異常內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何在C語(yǔ)言中判斷socket是否已經(jīng)斷開(kāi)
如果不主動(dòng)關(guān)閉socket的話,系統(tǒng)不會(huì)自動(dòng)關(guān)閉的,除非當(dāng)前進(jìn)程掛掉了,操作系統(tǒng)把占用的socket回收了才會(huì)關(guān)閉。小編今天跟大家簡(jiǎn)單介紹下如何在C語(yǔ)言中判斷socket是否已經(jīng)斷開(kāi)2019-05-05
C語(yǔ)言復(fù)雜鏈表的復(fù)制實(shí)例詳解
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言復(fù)雜鏈表的復(fù)制,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-02-02
C++輸出上三角/下三角/菱形/楊輝三角形(實(shí)現(xiàn)代碼)
本篇文章是對(duì)C++中輸出上三角/下三角/菱形/楊輝三角形的示例代碼進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-07-07
Visual Studio 2019配置OpenCV4.1.1詳細(xì)圖解教程
這篇文章主要介紹了Visual Studio 2019配置OpenCV4.1.1詳細(xì)圖解教程 ,需要的朋友可以參考下2020-02-02
C語(yǔ)言動(dòng)態(tài)分配二維字符串?dāng)?shù)組的方法
小編最近忙里偷閑,給大家整理一份教程關(guān)于C語(yǔ)言動(dòng)態(tài)分配二維字符串?dāng)?shù)組的方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-10-10
C++ Boost命令行解析庫(kù)的應(yīng)用詳解
命令行解析庫(kù)是一種用于簡(jiǎn)化處理命令行參數(shù)的工具,它可以幫助開(kāi)發(fā)者更方便地解析命令行參數(shù)并提供適當(dāng)?shù)膸椭畔?本文主要介紹了不同的命令行解析庫(kù)和它們?cè)贑++項(xiàng)目中的應(yīng)用,希望對(duì)大家有所幫助2023-11-11
C語(yǔ)言實(shí)現(xiàn)日志備份守護(hù)進(jìn)程的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何使用C語(yǔ)言開(kāi)發(fā)一個(gè)簡(jiǎn)單的日志備份守護(hù)進(jìn)程,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-04-04

