異常是指存在于程序運行時的異常行為,這些行為超出了函數(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ī)范。
審核編輯:湯梓紅
評論