異常是指存在于程序運(yùn)行時(shí)的異常行為,這些行為超出了函數(shù)正常功能的范圍,當(dāng)程序的某部分檢測(cè)到一個(gè)無法處理的問題時(shí),就需要用到異常處理。
?
1. C語(yǔ)言中傳統(tǒng)的處理錯(cuò)誤方式
終止程序:如assert,當(dāng)發(fā)生錯(cuò)誤時(shí),直接終止程序,這樣的作法不友好。
返回錯(cuò)誤碼:如果函數(shù)體里發(fā)生錯(cuò)誤時(shí),將錯(cuò)誤碼返回給coder,需要去查找對(duì)應(yīng)的錯(cuò)誤,系統(tǒng)的庫(kù)函數(shù)接口就是通過把錯(cuò)誤碼放到errno中,表示錯(cuò)誤。
Windows
下,使用perror
打印全部錯(cuò)誤:
- ?
- ?
- ?
- ?
- ?
- ?
for (int i = 0; i < 43; ++i)
{
cout << i << " : ";
perror(strerror(i));
cout << endl;
??}
大部分情況下,C語(yǔ)言出現(xiàn)錯(cuò)誤,都是使用的是返回錯(cuò)誤碼的方式處理,部分情況下使用終止程序來處理十分嚴(yán)重的錯(cuò)誤。
?
2. C++中處理異常的方式
如果程序中含有可能引發(fā)異常的代碼,那么通常也需要有專門的代碼處理問題,如:程序的問題是輸入無效,則異常處理部分可能會(huì)要求用戶重新輸入正確的數(shù)據(jù)。
異常處理機(jī)制為程序中異常檢測(cè)和異常處理這兩部分的協(xié)作提供支持,C++中,異常處理包括:
-
throw:異常檢測(cè)部分使用
throw
來表示它遇到了無法處理的問題,此時(shí)就會(huì)拋異常; -
catch:用于捕獲異常,可以有多個(gè)catch同時(shí)進(jìn)行捕獲;
-
try:try塊中的代碼拋出的異常通常會(huì)被一個(gè)或多個(gè)catch處理,因?yàn)?span style="margin:0px;padding:0px;max-width:100%;">catch處理異常,所以他們也被稱為異常處理代碼。
?
2.1 throw
程序的異常檢測(cè)部分使用throw拋出一個(gè)異常,throw后緊跟一個(gè)表達(dá)式,該表達(dá)式的類型就是拋出的異常類型。
一般來說,直接將異常拋出,交給后面的程序處理異常,不應(yīng)該將異常信息給直接輸出。
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
int main()
{
FILE* fp = fopen("a.txt", "a");
//拋出異常的類型為string類型
if (fp == nullptr)
throw string("請(qǐng)檢查文件是否存在");
return 0;
}
2.2 try
try關(guān)鍵字后,緊跟著一個(gè)塊,這個(gè)塊中是花括號(hào)擴(kuò)起來的語(yǔ)句序列,跟在try塊之后的是一個(gè)或多個(gè)catch子句;
catch子句包括三部分:關(guān)鍵字catch、括號(hào)內(nèi)的對(duì)象聲明(異常聲明,異常類型,拋出異常的類型要和catch處理的異常類型相同),一個(gè)處理異常的代碼塊;
當(dāng)選中某個(gè)catch子句處理異常后,執(zhí)行與之對(duì)應(yīng)的塊,catch一旦完成,程序跳轉(zhuǎn)到try語(yǔ)句最后一個(gè)catch之后的語(yǔ)句繼續(xù)執(zhí)行。
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
int main()
{
try
{
FILE* fp = fopen("a.txt", "r");// 以只讀方式打開一個(gè)文件
if (fp == nullptr)
throw string("請(qǐng)檢查文件是否存在");
}
catch (const char* msg)// char*類型異常
{
cout << msg << endl;
}
catch (const string& msg)//string類型異常
{
cout << msg << endl;
}
catch (...)//... 代表可以捕獲任意類型的異常
{
cout << "出現(xiàn)了無法解決的異常" << endl;
}
return 0;
}
?
3. 異常拋出和捕獲的規(guī)則
-
異常是通過拋出對(duì)象而引起的,該對(duì)象的類型決定了應(yīng)該匹配哪個(gè)
catch
的處理代碼; -
異常處理部分
catch
,是調(diào)用鏈中與該類型匹配且拋出異常位置最近的那一個(gè); -
拋出異常對(duì)象后,會(huì)生成一個(gè)異常對(duì)象的拷貝,因?yàn)閽伋龅膶?duì)象可能是一個(gè)臨時(shí)對(duì)象,所以會(huì)生成一個(gè)拷貝對(duì)象,該拷貝的臨時(shí)對(duì)象會(huì)在
catch
結(jié)束后銷毀; -
catch(...)
可捕獲任意類型異常,代表了不知道出現(xiàn)異常的錯(cuò)誤是什么,也就無法進(jìn)行解決; -
在異常的拋出和捕獲中,并不是類型的完全匹配,可以拋出派生類對(duì)象,使用基類捕獲(很重要)。
在函數(shù)調(diào)用鏈中異常棧展開的匹配規(guī)則:
-
先檢查
throw
是否在try
塊內(nèi)部,如果是則再查找匹配的catch
語(yǔ)句,如果有匹配則調(diào)用catch
的異常處理代碼; -
若沒有匹配的
catch
異常處理代碼,則退出當(dāng)前函數(shù)棧,繼續(xù)在調(diào)用函數(shù)棧中查找匹配catch; -
如果達(dá)到main函數(shù)棧中,仍沒有匹配,則終止程序,沿著調(diào)用棧查找匹配的
catch
子句的過程稱為棧展開;所以一般情況下,都要在最后加一個(gè)catch(...)
捕獲任意類型的異常,否則當(dāng)有異常沒有被捕獲時(shí),就會(huì)導(dǎo)致程序終止; -
找到匹配的catch子句后,會(huì)沿著catch之后的代碼繼續(xù)執(zhí)行。
注意:
-
throw可以拋出任意類型的異常,拋出的異常必須進(jìn)行捕獲,否則程序就會(huì)終止;
-
throw拋出異常后,若是在多個(gè)函數(shù)棧中調(diào)用時(shí),會(huì)直接跳轉(zhuǎn)到有匹配的catch子句中,若沒有匹配的子句時(shí),程序終止;
-
catch(...)
可捕獲任意類型異常。
?異常重新拋出
有可能單個(gè)的異常不能完全處理一個(gè)異常,則在進(jìn)行一些處理后,希望再給外層的調(diào)用鏈函數(shù)來處理,catch則可以通過重新拋出異常將異常傳遞給更上層的函數(shù)處理;
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
double Div(int a, int b)
{
if (b == 0)
throw string("發(fā)生了除0錯(cuò)誤");
return a / b;
}
void func()
{
int *p = new int(10);
int b = 0;
cout << Div(*p, b);
delete p;
}
int main()
{
try
{
func();
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
上述代碼中,會(huì)出現(xiàn)內(nèi)存泄漏,throw拋出的異常,直接跳轉(zhuǎn)到main函數(shù)棧中,則會(huì)導(dǎo)致func中,申請(qǐng)的空間沒有釋放,造成內(nèi)存泄漏,則需要對(duì)該異常進(jìn)行重新捕獲,并且釋放該空間,避免內(nèi)存泄漏。
這樣修改就不會(huì)存在內(nèi)存泄漏:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
void func()
{
int *p = new int(10);
try
{
int b = 0;
cout << Div(*p, b);
}
catch (...)
{
delete p;
throw;
}
delete p;
}
?
4. 異常安全
異常安全:異常導(dǎo)致的安全問題。
異常中斷了程序的正常流程,異常發(fā)生時(shí),調(diào)用者請(qǐng)求的一部分計(jì)算可能已經(jīng)完成了,另一部可能還沒完成。通常情況下,略過部分程序意味著某些對(duì)象處理到一般就戛然而止了,從而導(dǎo)致對(duì)象處于無效或未完成的狀態(tài),或者資源沒有被正常釋放。
那些在異常發(fā)生期間正確執(zhí)行了“清理”工作的程序被稱為異常安全的代碼。
注意:
-
構(gòu)造函數(shù)完成對(duì)象的構(gòu)造和初始化,最好不要在構(gòu)造函數(shù)中拋異常,否則可能導(dǎo)致對(duì)象不完整或者沒有完全初始化;
-
析構(gòu)函數(shù)主要完成資源的清理,最好不要在析構(gòu)函數(shù)中拋出異常,否則可能導(dǎo)致資源泄漏;
-
C++中經(jīng)常會(huì)導(dǎo)致資源泄漏的問題,如new 和 delete中拋出異常,導(dǎo)致內(nèi)存泄漏,lock和unlock之間拋出遺產(chǎn),導(dǎo)致死鎖,C++經(jīng)常使用RAII來解決上述問題;
異常規(guī)范
-
異常規(guī)則說明說明的目的是為了讓函數(shù)使用者知道該函數(shù)可能拋出什么異常,在函數(shù)后面接throw,列出這個(gè)函數(shù)可能拋出的所有異常類型;
- ?
- ?
void func() throw(string, char, char*);//可拋出三種類型的異常
void*?operator?new(size_t)?throw(bad_alloc);//只會(huì)拋bad_alloc異常
-
函數(shù)后面接throw(),表示不會(huì)拋出異常;
- ?
- ?
void func() throw();//不拋異常
void*?operator?new(size_t)?throw();//不拋異常
-
如果沒有異常接口聲明,則可以拋任意類型的異常。
?
5. 自定義異常體系
自定義異常的體系,一般情況下,拋出派生類的異常,由基類捕獲,這樣在不同的派生類中,可以拋出許多不同的異常,而且具有相同的調(diào)用方式(由基類調(diào)用),避免調(diào)用混亂,方便管理。
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
class Exception
{
public:
Exception(const char* msg)
:_errmsg(msg)
{}
virtual string what() = 0;//純虛函數(shù),接口類
string _errmsg;
};
class NetException : public Exception
{
public:
NetException(const char* msg)
:Exception(msg)
{}
virtual string what()
{
return "網(wǎng)絡(luò)錯(cuò)誤" + _errmsg;
}
};
class SqlException : public Exception
{
public:
SqlException(const char* msg)
:Exception(msg)
{}
virtual string what()
{
return "數(shù)據(jù)庫(kù)錯(cuò)誤" + _errmsg;
}
};
那么在捕獲的時(shí)候,只需要捕獲基類的異常即可:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
void Func()
{
if (rand() % 33 == 0)
throw SqlException("數(shù)據(jù)庫(kù)啟動(dòng)出錯(cuò)");
else if(rand() % 17 == 0)
throw NetException("網(wǎng)絡(luò)連接出錯(cuò)");
}
int main()
{
for (int i = 0; i < 188; ++i)
{
try
{
Func();
}
catch (Exception& e)//捕獲基類 即可
{
cout << e.what() << endl;
}
}
return 0;
}
?
6. 異常優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
1.清晰的包含錯(cuò)誤信息;
2.如果有越界問題時(shí),可以很方便的處理;
3.多層調(diào)用時(shí),里層發(fā)生錯(cuò)誤,不會(huì)層層調(diào)用,最外層可直接捕獲;
4.一些第三方庫(kù)也是使用異常,使用異常時(shí)可以很方便使用這些庫(kù):如boost
缺點(diǎn):
1.異常會(huì)導(dǎo)致執(zhí)行流跳轉(zhuǎn),分析程序時(shí)會(huì)有一些問題;
2.C++中沒有GC,異??赡軙?huì)導(dǎo)致資源泄漏的風(fēng)險(xiǎn);
3.C++庫(kù)中定義的異常體系,可用性不高,一般自己定義;
4.C++可以拋任意類型的異常,則需要對(duì)異常最很好的規(guī)范管理,否則就會(huì)非常混亂,所以一般定義出繼承體系下的異常規(guī)范。
審核編輯:湯梓紅
評(píng)論