【C++】:異常處理
阿新 • • 發佈:2019-01-13
C++的異常
1.C語言處理錯誤的方式
- 終止程式。例如:
assert
,斷言為假
則終止程序 - 返回錯誤碼。程式設計師自己去查詢
錯誤碼
對應的錯誤資訊,使用廣泛 C 標準庫
中setjmp
和longjmp
組合
2. C++的異常處理
異常是一種處理錯誤
自己無法處理
的錯誤時就可以丟擲異常
,讓函式的直接或間接
的呼叫者處理這個錯誤。
- throw:當問題出現時,程式會
丟擲
一個異常。通過使用throw
關鍵字來完成。 - catch:在想要處理問題的地方,通過
異常處理程式
捕獲異常,catch
關鍵字用於捕獲異常,也可以有多個catch
進行捕獲。 - try:
try
塊中的程式碼
標識將被啟用的特定異常
,它後面通常跟著一個或多個catch 塊
。
try
塊中放置可能
丟擲異常的程式碼,try 塊中的程式碼被稱為保護程式碼
。使用 try/catch 語句的語法如下:
try
{
//保護的程式碼標識
}
catch ( ExceptionName e1)
{
// catch 塊
}catch (ExceptionName e2)
{
// catch 塊
}catch (ExceptionName e3)
{
// catch 塊
}
3.使用異常方法
3.1 拋異常和捕捉異常
- 異常是通過
丟擲物件
而引發的,該物件的型別
決定了應該啟用哪個catch
的處理程式碼。 - 被選中的處理程式碼是呼叫
鏈
中與該物件型別匹配
且離丟擲異常位置最近
的那一個。 - 丟擲異常物件後,會生成一個
異常物件的拷貝
,因為丟擲的異常物件可能是一個臨時物件
,所以會生成 一個拷貝物件
,這個拷貝的臨時物件會在被catch
以後銷燬,類似於函式的傳值返回
catch
可以捕獲任意型別
的異常。- 丟擲和捕獲的匹配原則有個
例外
,並不都是型別完全嚴格匹配
,可以丟擲的派生類物件
,使用基類捕獲。實際工程中,使用的很多。
在函式呼叫鏈中異常棧展開匹配原則
:
- 檢查
throw
本身是否在try
塊內部,如果存在則查詢匹配的catch
語句。如果有匹配
的,則調到catch
的地方進行處理。 如果沒有匹配的catch
則退出當前函式棧,繼續在呼叫函式的棧
中進行查詢匹配的catch
。 - 如果到達
main函式的棧
,依舊沒有匹配的,則終止程式
。這個過程稱為棧展開
。實際使用中我們最後都要加一個catch(...)
捕獲任意型別
的異常,否則當有異常沒捕獲,程式就會直接終止。 - 找到匹配的
catch
子句並處理以後,會繼續沿著catch
子句後面繼續執行
。
double Div(double x1, double x2)
{
if (x2 == 0)
{
throw "Div zero mistakes";//丟擲異常
}
else
{
return x1 / x2;
}
}
void Test()
{
try{
int x1;
int x2;
cin >> x1 >> x2;
Div(x1, x2);
}
catch (const char* msg){//捕獲字串型別的異常
cout << msg << endl;
}
catch (int msg){//捕獲整型的異常
cout << msg << endl;
}
catch (...){//捕獲任意型別的異常
cout << "unkown exception" << endl;
}
cout << "繼續執行" << endl;//沿著catch子句後面繼續執行
}
int main()
{
try {
Test();
}
catch (const char* msg){//匹配且離丟擲異常位置最近的
cout << msg << endl;
}
return 0;
}
3.2 重新丟擲異常
有可能單個的catch
不能完全處理
一個異常,在進行一些校正處理
以後,希望再交給外層
的呼叫鏈函式
來處理,catch
則可以通過重新丟擲
將異常傳遞給更上層的函式進行處理。
double Div(double x1, double x2)
{
if (x2 == 0)
{
throw "Div zero mistakes";//丟擲異常
}
else
{
return x1 / x2;
}
}
void Test()
{
int* arr = new int[10];
try{
int x1;
int x2;
cin >> x1 >> x2;
Div(x1, x2);
}
catch (...){/
cout << "delete arr[]" << endl;
delete[] arr;//釋放陣列之後重新丟擲
throw;
}
cout << "繼續執行" << endl;//沿著catch子句後面繼續執行
}
void Func()
{
}
int main()
{
try {
Test();
}
catch (const char* msg){//匹配且離丟擲異常位置最近的
cout << msg << endl;
}
return 0;
}
3.3 異常安全及規範
異常安全:
構造
函式完成物件的構造和初始化
,最好不要在建構函式中丟擲異常
,否則可能導致物件不完整
或沒有完全初始化
,可能會跳過某些程式碼。析構
函式主要完成資源的清理
,最好不要在解構函式內丟擲異常
,否則可能導致資源洩漏(記憶體洩漏、控制代碼未關閉等)
C++
中異常經常會導致資源洩漏
的問題,比如在new
和delete
中丟擲了異常,導致記憶體洩漏,在lock
和unlock
之間丟擲了異常導致死鎖
,C++
經常使用RAII
來解決。
異常規範:
- 異常規格說明的
目的
是為了讓函式使用者知道該函式可能丟擲的異常
有哪些。 可以在函式的後面接throw(型別)
,列出這個函式可能拋的所有異常型別
。 - 函式的後面接
throw()
,表示函式不拋異常
。 - 若無異常介面宣告,則此函式可以拋
任何型別
的異常。
//表示這個函式會丟擲int/string/char中的某種型別的異常
void Func1() throw(int, string, char);
//表示這個函式不會丟擲異常
void Func2() throw();
//表示可以丟擲任意型別的異常
void Func3();
4. 自定義異常
實際使用中,我們都會自定義自己的異常體系
進行規範
的異常管理,因為一個專案中如果大家隨意拋異常
,那麼外層
的呼叫者會存在很多catch型別
,所以實際中都會定義一套繼承
的規範體系。這樣大家丟擲的都是繼承的派生類物件
,捕獲一個基類
即可。
class Exception {
protected:
string _errmsg;
int _id;
};
class SqlException : public Exception {};
class CacheException : public Exception {};
class HttpServerException : public Exception {};
int main() {
try{
// server.Start();
// 丟擲物件都是派生類物件
}
catch (const Exception& e) //捕獲父類物件就可以
{}
catch (...){//捕捉任意型別即可
cout << "Unkown Exception" << endl;
}
return 0;
}
5.C++標準庫的異常體系
C++ 提供了一系列標準的異常,定義在<exception>
中,我們可以在程式中使用這些標準的異常。它們是以父子類層次結構組織起來的,如下圖所示:
下表是對上面層次結構中出現的每個異常
的說明:
void Test()
{
try{
vector<int> v(10, 5);
//系統記憶體不夠會拋異常
v.reserve(1000000000);
}
//父類子類同時存在,嚴格匹配
catch (const bad_alloc& e)//捕捉子類物件
{
cout << e.what << endl;
}
catch (const exception& e) //捕獲父類物件
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
注:父類子類同時存在,嚴格匹配,跳轉到子類的catch
。
6. C++異常的優缺點
優點:
- 異常
物件
定義好之後,相比錯誤碼的方式可以清晰準確
的展示出錯誤的各種資訊
,甚至可以包含堆疊呼叫
的資訊,這樣可以幫助更好的定位程式的bug
。 - 返回
錯誤碼
的傳統方式有個很大的問題就是在函式呼叫鏈中,深層的函式返回了錯誤,那麼我們得層層返回
錯誤,這樣最外層才能拿到錯誤。但如果是異常體系
,不管那個呼叫函數出錯,都不用檢查返回值,因為丟擲的異常
會直接跳到main函式catch捕獲
的地方,直接在main函式
處理錯誤即可。 - 很多的第三方庫都包含
異常
,例如boost、gtest、gmock
等常用的庫。 - 很多
測試框架
都使用異常,這樣能更好的使用單元測試
等進行白盒的測試
。 - 部分函式使用異常可以更好處理錯誤。。例如
T& operator()
函式,如果pos
越界了只能使用異常或者終止程式
處理,因為T代表的自定義型別
沒辦法通過返回值
表示錯誤。
缺點:
- 異常會導致程式的
執行流亂跳
,非常的混亂,這會導致我們跟蹤除錯
時以及分析程式時比較困難。 - 異常會有一些
效能的開銷
。 - C++沒有
垃圾回收機制
,資源需要自己管理。有了異常非常容易導致記憶體洩漏、死鎖
等異常安全問題。 這個需要使用RAII
來處理資源
的管理問題。 - C++標準庫的異常體系定義得不好,導致大家
各自定義各自的異常體系
,非常的混亂
。 - 異常儘量
規範使用
,如果隨意拋異常,外層捕獲的使用者
會catch
很多型別。 - C++異常利大於弊。
異常規範有兩點:
- 丟擲異常型別都繼承自一個
基類
。 - 函式
是否拋異常、拋什麼異常,都使用 func() throw()
的方式規範化。