1. C++異常概念
異常是一種處理錯誤的方式,當一個函數發現自己無法處理的錯誤時就可以拋出異常,讓函數的直接或間接的調用者處理這個錯誤。
- throw: 當問題出現時,程序會拋出一個異常。這是通過使用 throw 關鍵字來完成的。
- catch: 在您想要處理問題的地方,通過異常處理程序捕獲異常.catch 關鍵字用于捕獲異常,可以有多個catch進行捕獲。
- try: try 塊中的代碼標識將被激活的特定異常,它后面通常跟著一個或多個 catch 塊。
如果有一個塊拋出一個異常,捕獲異常的方法會使用 try 和 catch 關鍵字。try 塊中放置可能拋出異常的代碼,try 塊中的代碼被稱為保護代碼。使用 try/catch 語句的語法如下所示:
try
{//可能會出現異常的代碼塊//即保護的標識代碼
}
catch(ExceptionName e1)
{//catch快
}
catch(ExceptionName e2)
{//catch快
}
catch(ExceptionName eN)
{//catch快
}
2.異常的拋出和匹配原則
- 異常是通過拋出對象而引發的,該對象的類型決定了應該激活哪個catch的處理代碼。
- 被選中的處理代碼是調用鏈中與該對象類型匹配且離拋出異常位置最近的那一個。
- 拋出異常對象后,會生成一個異常對象的拷貝,因為拋出的異常對象可能是一個臨時對象,所以會生成
一個拷貝對象,這個拷貝的臨時對象會在被catch以后銷毀。(這里的處理類似于函數的傳值返回) - catch(…)可以捕獲任意類型的異常,問題是不知道異常錯誤是什么。
- 實際中拋出和捕獲的匹配原則有個例外,并不都是類型完全匹配,可以拋出的派生類對象,使用基類捕獲,這個在實際中非常實用,我們后面會詳細講解這個。
- 對于以上特性,為加深對其理解,代碼解析如下:
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0)//拋出char*類型字符串類型,則必須匹配對應//的類型來catch//拋出的異常會生成一個臨時對象的拷貝//因為本層函數棧幀馬上就要結束了!!!throw "Division by zero condition!";elsereturn ((double)a / (double)b);
}
void Func1()
{try{int len, time;cin >> len >> time;cout << Division(len, time) << endl;}//發生異常,如果能與本層的catch匹配,則在本層函數棧幀拋異常//若果不能與本層函數棧幀的catch匹配,則結束本層函數棧幀,//去上一層函數棧幀繼續匹配,直到main函數,//如果還不能匹配異常,則直接終止程序catch (int errid){cout << errid << endl;}
}
int main()
{try {Func1();}catch (const char* errmsg){cout << errmsg << endl;}catch (int errid) {cout << errid << endl;}catch (...){cout << "unkown exception" << endl;}return 0;
}
3.在函數調用鏈中異常棧展開匹配原則
- 首先檢查throw本身是否在try塊內部,如果是再查找匹配的catch語句。如果有匹配的,則調到catch的地方進行處理。
- 沒有匹配的catch則退出當前函數棧,繼續在調用函數的棧中進行查找匹配的catch。
- 如果到達main函數的棧,依舊沒有匹配的,則終止程序。上述這個沿著調用鏈查找匹配的catch子句的過程稱為棧展開。所以實際中我們最后都要加一個catch(…)捕獲任意類型的異常,否則當有異常沒捕獲,程序就會直接終止。
- 找到匹配的catch子句并處理以后,會繼續沿著catch子句后面繼續執行。
以上匹配原則的圖片理解方式如下所示:
4.異常的重新拋出
有可能單個的catch不能完全處理一個異常,在進行一些校正處理以后,希望再交給更外層的調用鏈函數來處理,catch則可以通過重新拋出將異常傳遞給更上層的函數進行處理.如下代碼示例所示:
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}
void Func()
{// 這里可以看到如果發生除0錯誤拋出異常,另外下面的array沒有得到釋放。// 所以這里捕獲異常后并不處理異常,異常還是交給外面處理,這里捕獲了再// 重新拋出去。int* array = new int[10];try {int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch (...){cout << "delete []" << array << endl;delete[] array;throw;}// ...cout << "delete []" << array << endl;delete[] array;
}
int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
5.異常安全
- 構造函數完成對象的構造和初始化,最好不要在構造函數中拋出異常,否則可能導致對象不完整或沒有完全初始化。
- 析構函數主要完成資源的清理,最好不要在析構函數內拋出異常,否則可能導致資源泄漏(內存泄漏、句柄未關閉等)
- C++中異常經常會導致資源泄漏的問題,比如在new和delete中拋出了異常,導致內存泄漏,在lock和unlock之間拋出了異常導致死鎖,C++經常使用RAII來解決以上問題。(關于RAII智能指針,可以觀看我的RAII智能篇)
6.異常規則
- 異常規格說明的目的是為了讓函數使用者知道該函數可能拋出的異常有哪些。 可以在函數的后面接throw(類型),列出這個函數可能拋擲的所有異常類型。
- C++98中函數的后面接throw(),表示函數不拋異常。C++11中函數的后面接except(),表示函數不拋異常。
- 若無異常接口聲明,則此函數可以拋擲任何類型的異常。
// 這里表示這個函數會拋出A/B/C/D中的某種類型的異常
void fun() throw(A,B,C,D);
// 這里表示這個函數只會拋出bad_alloc的異常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 這里表示這個函數不會拋出異常
void* operator new (std::size_t size, void* ptr) throw()/noexcept();
7.C++標準庫中的異常體系
C++ 提供了一系列標準的異常,定義在 中,我們可以在程序中使用這些標準的異常。它們是以父子類層次結構組織起來的,如下所示:
說明: 實際中我們可以可以去繼承exception類實現自己的異常類。但是實際中很多公司像上面一樣自己定義一套異常繼承體系。因為C++標準庫設計的不夠好用。如下代碼示例:
int main()
{try{vector<int> v(10, 5);// 這里如果系統內存不夠也會拋異常v.reserve(1000000000);// 這里越界會拋異常v.at(10) = 100; }catch (const exception& e) // 這里捕獲父類對象就可以{cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}return 0;
}
8.異常的優缺點(面試有面到!!!)
C++異常的優點:
- 異常對象定義好了,相比錯誤碼的方式可以清晰準確的展示出錯誤的各種信息,甚至可以包含堆棧調用的信息,這樣可以幫助更好的定位程序的bug。
- 返回錯誤碼的傳統方式有個很大的問題就是,在函數調用鏈中,深層的函數返回了錯誤,那么我們得層層返回錯誤,最外層才能拿到錯誤,具體看下面的詳細解釋。
// 1.下面這段偽代碼我們可以看到ConnnectSql中出錯了,
// 先返回給ServerStart,ServerStart再
//返回給main函數,main函數再針對問題處理具體的錯誤。
// 2.如果是異常體系,不管是ConnnectSql
// 還是ServerStart及調用函數出錯,都不用檢查,因為拋
//出的異常異常會直接跳到main函數中catch捕獲的地方,main函數直接處理錯誤。
int ConnnectSql()
{// 用戶名密碼錯誤if (...)return 1;// 權限不足if (...)return 2;
}
int ServerStart() {if (int ret = ConnnectSql() < 0)return ret;int fd = socket()if(fd < 0)return errno;
}
int main()
{if (ServerStart() < 0)...return 0;
}
- 很多的第三方庫都包含異常,比如boost、gtest、gmock等等常用的庫,那么我們使用它們也需要使用異常。
- 很多測試框架都使用異常,這樣能更好的使用單元測試等進行白盒的測試。
- 部分函數使用異常更好處理,比如構造函數沒有返回值,不方便使用錯誤碼方式處理。比如T&operator這樣的函數,如果pos越界了只能使用異常或者終止程序處理,沒辦法通過返回值表示錯誤。
C++異常的缺點:
- 異常會導致程序的執行流亂跳,并且非常的混亂,并且是運行時出錯拋異常就會亂跳。這會導致我們跟蹤調試時以及分析程序時,比較困難。
- 異常會有一些性能的開銷。當然在現代硬件速度很快的情況下,這個影響基本忽略不計。
- C++沒有垃圾回收機制,資源需要自己管理。有了異常非常容易導致內存泄漏、死鎖等異常安全問題。這個需要使用RAII來處理資源的管理問題。學習成本較高。
- C++標準庫的異常體系定義得不好,導致大家各自定義各自的異常體系,非常的混亂。
- 異常盡量規范使用,否則后果不堪設想,隨意拋異常,外層捕獲的用戶苦不堪言。所以異常規范有兩點:一、拋出異常類型都繼承自一個基類。二、函數是否拋異常、拋什么異常,都使用 func()throw();的方式規范化。
總結: 異常總體而言,利大于弊,所以工程中我還是鼓勵使用異常的。