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