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











評論