一文總結C++中的異常
一、C語言傳統(tǒng)的處理錯誤的方式
傳統(tǒng)的錯誤處理機制:
終止程序:如 assert,缺點是用戶難以接受,如發(fā)生內存錯誤,除 0 錯誤時就會終止程序。
返回錯誤碼:缺陷是,需要程序員自己去查找錯誤碼對應的錯誤,其次錯誤碼需要層層返回,這就要求我們要去接收和檢查返回值。如系統(tǒng)的很多庫的接口函數(shù)都是通過把錯誤碼放到 errno 中,表示錯誤。
實際中 C 語言基本都是使用返回錯誤碼的方式處理錯誤,部分情況下使用終止程序處理非常嚴重的錯誤。
二、C++異常
異常是一種處理錯誤的方式,當一個函數(shù)發(fā)現(xiàn)自己無法處理的錯誤時就可以拋出異常,讓函數(shù)的直接或間接調用者處理這個錯誤。
throw:當問題出現(xiàn)時,程序會拋出一個異常,這是通過使用 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 塊 }
三、異常的使用
3.1 異常的拋出和捕獲
3.1.1 異常的拋出和匹配原則
異常是通過拋出對象而引發(fā)的,該對象的類型決定了應該激活哪個 catch 的處理代碼。
被選中的處理代碼是調用鏈中與該對象類型匹配且離拋出異常位置最近的那一個。
拋出異常對象后,會生成一個異常對象的拷貝,因為拋出的異常對象可能是一個臨時對象,所以會生成一個拷貝對象,這個拷貝的臨時對象會在被 catch 以后銷毀(這里的處理類似于函數(shù)的傳值返回)。
catch(…) 可以捕獲任意類型的異常,問題是不知道異常錯誤是什么。
實際中拋出和捕獲的匹配原則有個例外,并不都是類型完全匹配,可以拋出派生類對象使用基類捕獲,這個在實際中非常實用,后面會進行詳細講解。
小Tips:異??梢話伋鋈魏晤愋偷膶ο?,即 throw 后面可以跟任意類型。異常拋出后如果沒有被捕獲,那么程序將會報錯。沒有捕獲可能是因為類型不匹配導致沒有捕獲,也可能是壓根就沒有 catch 語句去捕獲。catch(…) 只能放在所有 catch 的最后。
3.1.2 在函數(shù)調用鏈中異常棧展開匹配原則
首先檢查 throw 本身是否在 try 塊內部,如果是,再查找匹配的 catch 語句。如果有匹配的,則調到 catch 的地方進行處理。
沒有匹配的 catch 則退出當前函數(shù)棧幀,繼續(xù)在調用函數(shù)的棧幀中進行查找匹配的 catch。
如果到達 main 函數(shù)的棧幀,依舊沒有匹配的,則終止程序。上述這個沿著調用鏈查找匹配的 catch 子句的過程稱為棧展開。所以實際中我們最后都要加一個catch(…)捕獲任意類型的異常,否則當有異常沒捕獲,程序就會直接終止。
找到匹配的 catch 字句并處理以后,會繼續(xù)沿著 catch 字句后面繼續(xù)執(zhí)行。
如上圖所示:有三個函數(shù) func1()、func2()、func3()。在 func2() 中調用 func1(),func3() 中調用 func2(),在 main() 中調用 func3(),并在 func1() 中拋出一個異常,在 main() 中用 catch 語句捕獲。
#include <iostream> using namespace std; double Division(int a, int b) { // 當b == 0時拋出異常 if (b == 0) throw "Division by zero condition!"; else return ((double)a / (double)b); } void Func() { int len, time; cin >> len >> time; cout << Division(len, time) << endl; } int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } catch (...) { cout << "unkown exception" << endl; } return 0; }
棧展開的過程如下:首先檢查 throw 本身是否在 try 塊內部,如果是,再查找匹配的 catch 語句。如果有匹配的,則處理。如果沒有則退出當前函數(shù)棧,繼續(xù)在調用函數(shù)的棧中進行查找,不斷重復上述過程,若到達 main 函數(shù)額的棧,依舊沒有找到匹配的,則終止程序。
3.2 異常的重新拋出
有可能單個的 catch 不能完全處理一個異常,在進行一些校正后,希望在交給更外層的調用鏈函數(shù)來處理,catch 則可以通過重新拋出將異常傳遞給更上層的函數(shù)進行處理。
double Division(int a, int b) { // 當b == 0時拋出異常 if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } void Func() { // 這里可以看到如果發(fā)生除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; }
小Tips:如上面的帶所示,在 Func() 層捕獲了異常之后,并沒有對異常進行“處理”,而是將 array 進行了釋放,然后再將異常重新拋出去。Func() 層如果不對異常進行捕獲,那么在發(fā)生除零錯誤后,array 指向的空間就不會被釋放,最終造成內存泄漏。
3.3 異常安全
構造函數(shù)完成對象的構造和初始化,最好不要在構造函數(shù)中拋出異常,否則可能導致對象不完整或沒有完全初始化。
析構函數(shù)主要完成資源的清理,最好不要在析構函數(shù)中拋出異常,否則可能導致資源泄露(內存泄漏、句柄未關閉等)。
C++中異常經(jīng)常會導致資源泄露的問題,比如在 new 和 delete 中拋出異常,導致內存泄漏,在 lock 和 unlock 之間拋出了異常導致死鎖,C++經(jīng)常使用 RAII 來解決以上問題,關于 RAII 將在智能指針中為大家講解。
3.4 異常規(guī)范
異常規(guī)范說明的目的是為了讓函數(shù)使用者知道該函數(shù)可能拋出的異常有哪些。可以在函數(shù)的后面接 throw(類型),列出這個函數(shù)可能拋出的所有異常類型。
函數(shù)的后面接 throw(),表示函數(shù)不拋異常。
若無異常接口聲明,則此函數(shù)可以拋出任何類型的異常。
// 這里表示這個函數(shù)會拋出A/B/C/D中的某種類型的異常 void fun() throw(A,B,C,D); // 這里表示這個函數(shù)只會拋出bad_alloc的異常 void* operator new (std::size_t size) throw (std::bad_alloc); // 這里表示這個函數(shù)不會拋出異常 void* operator delete (std::size_t size, void* ptr) throw(); // C++11 中新增的noexcept,表示不會拋異常 thread() noexcept; thread (thread&& x) noexcept;
小Tips:C++11之后,一個函數(shù)如果不拋異常,就在其函數(shù)后面加上一個 noexcept,如果不拋異??梢圆蛔鋈魏翁幚怼?/p>
四、自定義異常體系
實際使用中,很多公司都會自定義自己的異常體系進行規(guī)范的異常管理,因為一個項目中如果大家隨意拋異常,那么外層的調用者基本就沒辦法玩了,所以實際中都會定義一套繼承的規(guī)范體系,這樣大家拋出的都是繼承自父類的派生類對象,捕獲一個基類就可以了。
// 服務器開發(fā)中通常使用的異常繼承體系 class Exception { public: Exception(const string& errmsg, int id) :_errmsg(errmsg) , _id(id) {} virtual string what() const { return _errmsg; } protected: string _errmsg; int _id; }; class SqlException : public Exception { public: SqlException(const string& errmsg, int id, const string& sql) :Exception(errmsg, id) , _sql(sql) {} virtual string what() const { string str = "SqlException:"; str += _errmsg; str += "->"; str += _sql; return str; } private: const string _sql; }; class CacheException : public Exception { public: CacheException(const string& errmsg, int id) :Exception(errmsg, id) {} virtual string what() const { string str = "CacheException:"; str += _errmsg; return str; } }; class HttpServerException : public Exception { public: HttpServerException(const string& errmsg, int id, const string& type) :Exception(errmsg, id) , _type(type) {} virtual string what() const { string str = "HttpServerException:"; str += _type; str += ":"; str += _errmsg; return str; } private: const string _type; }; void SQLMgr() { srand(time(0)); if (rand() % 7 == 0) { throw SqlException("權限不足", 100, "select * from name = '張三'"); } //throw "xxxxxx"; cout << "執(zhí)行成功" << endl; } void CacheMgr() { srand(time(0)); if (rand() % 5 == 0) { throw CacheException("權限不足", 100); } else if (rand() % 6 == 0) { throw CacheException("數(shù)據(jù)不存在", 101); } SQLMgr(); } void HttpServer() { // ... srand(time(0)); if (rand() % 3 == 0) { throw HttpServerException("請求資源不存在", 100, "get"); } else if (rand() % 4 == 0) { throw HttpServerException("權限不足", 101, "post"); } CacheMgr(); } int main() { while (1) { Sleep(500); try { HttpServer(); } catch (const Exception& e) // 這里捕獲父類對象就可以 { // 多態(tài) cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } } return 0; }
五、C++標準庫的異常體系
C++提供了一些列標準的異常,定義在 exception 中,我們可以在程序中使用這些標準的異常,它們是以父子類層次結構組織起來的,如下所示:
小Tips:實際中我們可以通過去繼承 exception 類實現(xiàn)自己的異常類。但是實際中很多公司像上面一樣,自己定義一套異常繼承體系。因為 C++ 標準庫設計的不夠好用。
int main() { try { vector<int> v(10, 5); // 這里如果系統(tǒng)內存不夠也會拋異常 v.reserve(1000000000); // 這里越界會拋異常 v.at(10) = 100; } catch (const exception& e) // 這里捕獲父類對象就可以 { cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } return 0; }
六、異常的優(yōu)缺點
6.1 優(yōu)點
異常對象定義好了,相比錯誤碼的方式可以清晰準確的展示出錯誤的各種信息,甚至可以包含堆棧調用的信息,這樣可以幫助更好的定位程序的 bug。
很多的第三方庫都包含異常,比如 boost、gtest、gmock 等等常用的庫,那么我們使用他們也需要使用異常。
部分函數(shù)使用異常更好處理,比如構造函數(shù)沒有返回值,不方便使用錯誤碼的方式處理。比如
T& operator[ ]
這樣的函數(shù),如果 pos 越界了只能使用異?;蛘呓K止程序處理,沒辦法通過返回值表示錯誤。返回錯誤碼的傳統(tǒng)方式有個很大的問題就是,在函數(shù)調用鏈中,深層的函數(shù)返回了錯誤,那么我們得層層返回錯誤,最外層才能拿到錯誤,具體看下面的詳細解釋。
// 1.下面這段偽代碼我們可以看到ConnnectSql中出錯了,先返回給ServerStart, //ServerStart再返回給main函數(shù),main函數(shù)再針對問題處理具體的錯誤。 // 2.如果是異常體系,不管是ConnnectSql還是ServerStart及調用函數(shù)出錯,都不用檢查,因 //為拋出的異常異常會直接跳到main函數(shù)中catch捕獲的地方,main函數(shù)直接處理錯誤。 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; }
6.2 缺點
異常會導致程序的執(zhí)行流亂跳,并且非常的混亂,并且是運行時出錯拋異常就會亂跳。這會導致我們跟蹤調試時以及分析程序時,比較困難。
異常會有一些性能的開銷(拋出異常后,會生成一個異常對象的拷貝)。當然在現(xiàn)代硬件速度很快的情況下,這個影響基本忽略不計。
C++沒有垃圾回收機制,資源需要自己管理,有了異常非常容易導致內存泄漏、死鎖等異常安全問題。這個需要使用 RAII 來處理資源的管理問題。學習成本較高。
C++標準庫的異常體系定義的不好,導致大家各自定義各自的異常體系,非常的混亂。
異常盡量規(guī)范使用,否則后果不堪設想,隨意拋異常,外層捕獲的用戶苦不堪言。所以異常規(guī)范有兩點:一是:拋出的異常類型都繼承自一個基類;二是:函數(shù)是否拋異常、拋什么異常,都使用 func() throw(); 的方式規(guī)范化。
總結:異??傮w而言,利大于弊,所以工程中我們還是鼓勵使用異常的。另外 OO 的語言基本都是用異常處理錯誤,這也可以看出這是大勢所趨。
七、結語
以上就是一文總結C++中的異常的詳細內容,更多關于C++異常的資料請關注腳本之家其它相關文章!