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

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

上圖就是一個棧展開的過程。
1.4 查找匹配的處理代碼
1>.一般情況下拋儲對象和catch接收的那個對象的類型是完全匹配的,如果有多個類型匹配的catch子句,那么就選擇離他位置更近的那個catch子句。
2>.但是也有一些例外,允許從非常量向常量的類型準(zhǔn)換,也就是權(quán)限縮??;允許數(shù)組轉(zhuǎn)換成指向數(shù)組元素類型的指針,函數(shù)被轉(zhuǎn)換成指向函數(shù)的指針;允許從派生類向基類類型的轉(zhuǎn)換,這一點(diǎn)非常實(shí)用,實(shí)際中繼承體系基本都是用這個方式去設(shè)計(jì)的。
3>.如果到main函數(shù)中,異常人就沒有被匹配的話就會被終止程序,不是發(fā)生嚴(yán)重錯誤的情況下,我們是不期望程序最終的,所以一般的main函數(shù)中在最后都會使用catch(...),它可以捕獲任意類型的異常,但是我們是不知道異常的錯誤是什么。注:一個try模塊我們可以搭配多個catch模塊。
//由于時間等等各種原因,我們這里就不一一為大家展示匹配的過程代碼了,我們接下來就來模擬設(shè)計(jì)一個繼承的匹配機(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é)號", 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到一個異常對象后,需要對錯誤進(jìn)行分類,其中的某種異常錯誤需要進(jìn)行特殊的處理,其他錯誤則重新拋出異常給外層調(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)存、鎖等),后面要進(jìn)行釋放(這里指的是我們自己用new/malloc向內(nèi)存申請的一塊資源,它在釋放時需要我們自己去調(diào)用delete函數(shù)),但是中間可能會拋異常就會導(dǎo)致資源沒有釋放,這里由于異常就引發(fā)了資源泄露,會產(chǎn)生安全性的問題。為了解決這個問題,那么我們就要在拋出到外層調(diào)用鏈之前要提前捕獲到這個異常對象,將那些資源釋放之后再將其重新拋出。當(dāng)然我們下一章要講解的智能指針章節(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>.對于用戶和編譯器而言,預(yù)先知道某個程序會不會拋出異常大有益處,知道某個函數(shù)是否會拋出異常會有助于簡化調(diào)用函數(shù)的代碼。
2>.C++98中函數(shù)參數(shù)列表的后面接throw(),表示該函數(shù)不會拋異常,函數(shù)參數(shù)列表的后面接throw(類型1,類型2,...)表示可能會拋出多種類型的異常,將可能會拋出的類型之間均用逗號分割。
3>.C++98的這種方式有點(diǎn)過于復(fù)雜,在實(shí)踐中其實(shí)并不好用,C++11中對其進(jìn)行了簡化,函數(shù)參數(shù)列表后面若加noexcept這個關(guān)鍵字就表示該函數(shù)不會拋異常,若啥都不加的話則表示可能會拋出異常。
4>.編譯器并不會在編譯時去檢查noexcept修飾了,也就是說如果一個函數(shù)用noexcept修飾了,但是同時又包含了throw語句或者調(diào)用的函數(shù)可能會拋出異常,編譯器還是會順利通過的(有些編譯器可能會報個警告)。但是如果一個聲明了noexcept的函數(shù)拋出了異常的話,程序便會去調(diào)用terminate終止程序。
5>.noexcept(expression)還可以作為一個運(yùn)算符去檢測一個表達(dá)式是否會拋出異常,可能會拋出異常的話則返回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;
}//如果我們大家仔細(xì)看上述這段代碼時,稍微有一點(diǎn)問題,Print函數(shù)有拋出異常的風(fēng)險,但是Print函數(shù)的參數(shù)后面加了noexcept這個關(guān)鍵字,理論上來說的話是不能加這個關(guān)鍵字的,通過前面的解析,我們可知,這種情況下有的編譯器是不會報錯的。我們現(xiàn)在來運(yùn)行這個代碼來看一下,如果我們輸入5的話,編譯器確實(shí)不會報錯,而且還完整地運(yùn)行了下來,但如果我們輸入10的話,程序在這里就別破中止運(yùn)行了,原因是因?yàn)镻rint這個用noexcept修飾的函數(shù)在運(yùn)行時拋出了一個異常對象。2 標(biāo)準(zhǔn)庫的異常
1>.C++標(biāo)準(zhǔn)庫也定義了一套自己的異常繼承體系,基類是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實(shí)例詳解
在線程池中獲取線程執(zhí)行函數(shù)的返回值時,通常使用 std::future 而不是 std::promise 來傳遞返回值,這篇文章主要介紹了C++中的std::funture和std::promise實(shí)例詳解,需要的朋友可以參考下2024-05-05
C數(shù)據(jù)結(jié)構(gòu)中串簡單實(shí)例
這篇文章主要介紹了C數(shù)據(jù)結(jié)構(gòu)中串簡單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06
OpenCV圖像處理之實(shí)現(xiàn)圖像膨脹腐蝕操作
圖像形態(tài)學(xué)操作是指基于形狀的一系列圖像處理操作的合集,主要是基于集合論基礎(chǔ)上的形態(tài)學(xué)數(shù)學(xué)對圖像進(jìn)行處理。本文將為大家介紹一下如何利用OpenCV實(shí)現(xiàn)其中的腐蝕和膨脹操作,需要的可以參考一下2022-09-09
C++結(jié)構(gòu)體與類指針知識點(diǎn)總結(jié)
在本篇文章里小編給大家整理了關(guān)于C++結(jié)構(gòu)體與類指針知識點(diǎn)以及相關(guān)內(nèi)容,有興趣的朋友們參考學(xué)習(xí)下。2019-09-09
C語言分別實(shí)現(xiàn)棧和隊(duì)列詳解流程
棧和隊(duì)列,嚴(yán)格意義上來說,也屬于線性表,因?yàn)樗鼈円捕加糜诖鎯壿嬯P(guān)系為 "一對一" 的數(shù)據(jù),但由于它們比較特殊,因此將其單獨(dú)作為一章,做重點(diǎn)講解2022-04-04

