你的c++團隊還在禁用異常處理嗎?
點擊左上方藍(lán)字關(guān)注我們

什么是異常處理?
異常處理當(dāng)然指的是對異常的處理,異常是指程序在執(zhí)行期間產(chǎn)生的問題,沒有按正確設(shè)想的流程走下去,比如除以零的操作,異常處理提供了一種轉(zhuǎn)移程序控制權(quán)的方式,這里涉及到三個關(guān)鍵字:
throw:當(dāng)問題出現(xiàn)時,程序會通過throw來拋出一個異常
catch:在可能有throw想要處理問題的地方,通過catch關(guān)鍵字來捕獲異常
try:try塊中的代碼標(biāo)識將被激活的特定異常,它后面通常跟著一個或多個catch塊
直接看示例代碼:
void func() {throw exception; // 拋出異常}int main() {try { // try里放置可能拋出異常的代碼,塊中的代碼被稱為保護代碼func();} catch (exception1& e) { // 捕獲異常,異常類型為exception1// code} catch (exception2& e) { // 捕獲異常,異常類型為exception2// code} catch (...) {// code}return 0;}
c++標(biāo)準(zhǔn)都有什么異常?
C++ 提供了一系列標(biāo)準(zhǔn)的異常,定義在<exception> 中,我們可以在程序中使用這些標(biāo)準(zhǔn)的異常。它們是以父子類層次結(jié)構(gòu)組織起來的,如下所示:

圖片來自菜鳥教程
具體異常應(yīng)該不需要特別介紹了吧,看英文名字就可以知道大概意思。
自定義異常
可以通過繼承和重載exception類來自定義異常,見代碼:
class MyException : public std::runtime_error {public:MyException() : std::runtime_error("MyException") { }};void f(){// ...throw MyException();}int main() {try {f();} catch (MyException& e) {// ...} catch (...) {}return 0;}
我們應(yīng)該使用異常嗎?
在c++中關(guān)于是否使用異常一直都有爭議,典型的就是知乎上陳碩大神說的不應(yīng)該使用異常,還有就是google和美國國防部等都明確定義編碼規(guī)范來禁止在c++中使用異常,這里我找了很多中英文資料,在文末參考鏈接列舉了一些。
關(guān)于是否使用異常的討論帖子在這,https://www.zhihu.com/question/22889420
陳碩大神說的什么我就不貼出來了,他水平之高無需置疑,但他說的一些東西還是很有爭議的,關(guān)于異常處理,引用吳詠煒老師的一句話:“陳碩當(dāng)然是個技術(shù)大牛。不過,在編程語言這件事上,我更愿意信任 Bjarne Stroustrup、Herb Sutter、Scott Meyers 和 Andrei Alexandrescu。這些大神們都認(rèn)為異常是比返回錯誤碼更好的錯誤處理方式。”
而google明確禁用異常其實是有歷史包袱的,他們也認(rèn)同異常處理是比錯誤碼更好的處理方式,但他們別無選擇,因為以前的編譯器對異常處理的不好,他們項目里面已經(jīng)有了大量的非異常安全的代碼,如果全改成異常處理的代碼是有很大的工作量的,具體可以看上面的鏈接和我文末引用的一些鏈接。
美國國防部禁用異常是出于實時性能考慮,工具鏈不能保證程序拋出異常時的實時性能,但國防部禁用了很多c++特性,例如內(nèi)存分配,我們真的追求飛機一樣的高性能嗎?
通過上面的介紹大家應(yīng)該能猜到我的結(jié)論了吧,當(dāng)然這不是我的結(jié)論,而是大佬們的結(jié)論:推薦使用異常處理。
異常處理有一些潛在的缺點:
會有限的影響程序的性能,但正常工作流中不拋出異常的時候速度和普通函數(shù)一樣快,甚至更快
會導(dǎo)致程序體積變大10%-20%,但我們真的那么在乎程序的體積嗎(除了移動端)
異常處理相對于使用錯誤碼的好處:
如果不使用trycatch那就需要使用返回錯誤碼的方式,那就必然增加ifelse語句,每次函數(shù)返回后都會增加判斷的開銷,如果可以消除trycatch,代碼可能會更健壯,舉例如下:
void f1(){try {// ...f2();// ...} catch (some_exception& e) {// ...code that handles the error...}}void f2() { ...; f3(); ...; }void f3() { ...; f4(); ...; }void f4() { ...; f5(); ...; }void f5() { ...; f6(); ...; }void f6() { ...; f7(); ...; }void f7() { ...; f8(); ...; }void f8() { ...; f9(); ...; }void f9() { ...; f10(); ...; }void f10(){// ...if ( /*...some error condition...*/ )throw some_exception();// ...}
而使用錯誤碼方式:
int f1(){// ...int rc = f2();if (rc == 0) {// ...} else {// ...code that handles the error...}}int f2(){// ...int rc = f3();if (rc != 0)return rc;// ...return 0;}int f3(){// ...int rc = f4();if (rc != 0)return rc;// ...return 0;}int f4(){// ...int rc = f5();if (rc != 0)return rc;// ...return 0;}int f5(){// ...int rc = f6();if (rc != 0)return rc;// ...return 0;}int f6(){// ...int rc = f7();if (rc != 0)return rc;// ...return 0;}int f7(){// ...int rc = f8();if (rc != 0)return rc;// ...return 0;}int f8(){// ...int rc = f9();if (rc != 0)return rc;// ...return 0;}int f9(){// ...int rc = f10();if (rc != 0)return rc;// ...return 0;}int f10(){// ...if (...some error condition...)return some_nonzero_error_code;// ...return 0;}
錯誤碼方式對于問題的反向傳遞很麻煩,導(dǎo)致代碼腫脹,假如中間有一個環(huán)節(jié)忘記處理或處理有誤就會導(dǎo)致bug的產(chǎn)生,異常處理對于錯誤的處理更簡潔,可以更方便的把錯誤信息反饋給調(diào)用者,同時不需要調(diào)用者使用額外的ifelse分支來處理成功或者不成功的情況。
一般來說使用錯誤碼方式標(biāo)明函數(shù)是否成功執(zhí)行,一個值標(biāo)明函數(shù)成功執(zhí)行,另外一個或者多個值標(biāo)明函數(shù)執(zhí)行失敗,不同的錯誤碼標(biāo)明不同的錯誤類型,調(diào)用者需要對不同的錯誤類型使用多個ifelse分支來處理。如果有更多ifelse,那么必然寫出更多測試用例,必然花費更多精力,導(dǎo)致項目晚上線。
拿數(shù)值運算代碼舉例:
class Number {public:friend Number operator+ (const Number& x, const Number& y);friend Number operator- (const Number& x, const Number& y);friend Number operator* (const Number& x, const Number& y);friend Number operator/ (const Number& x, const Number& y);// ...};
最簡單的可以這樣調(diào)用:
void f(Number x, Number y) {// ...Number sum = x + y;Number diff = x - y;Number prod = x * y;Number quot = x / y;// ...}
但是如果需要處理錯誤,例如除0或者數(shù)值溢出等,函數(shù)得到的就是錯誤的結(jié)果,調(diào)用者需要做處理。
先看使用錯誤碼的方式:
class Number {public:enum ReturnCode {Success,Overflow,Underflow,DivideByZero};Number add(const Number& y, ReturnCode& rc) const;Number sub(const Number& y, ReturnCode& rc) const;Number mul(const Number& y, ReturnCode& rc) const;Number div(const Number& y, ReturnCode& rc) const;// ...};int f(Number x, Number y){// ...Number::ReturnCode rc;Number sum = x.add(y, rc);if (rc == Number::Overflow) {// ...code that handles overflow...return -1;} else if (rc == Number::Underflow) {// ...code that handles underflow...return -1;} else if (rc == Number::DivideByZero) {// ...code that handles divide-by-zero...return -1;}Number diff = x.sub(y, rc);if (rc == Number::Overflow) {// ...code that handles overflow...return -1;} else if (rc == Number::Underflow) {// ...code that handles underflow...return -1;} else if (rc == Number::DivideByZero) {// ...code that handles divide-by-zero...return -1;}Number prod = x.mul(y, rc);if (rc == Number::Overflow) {// ...code that handles overflow...return -1;} else if (rc == Number::Underflow) {// ...code that handles underflow...return -1;} else if (rc == Number::DivideByZero) {// ...code that handles divide-by-zero...return -1;}Number quot = x.div(y, rc);if (rc == Number::Overflow) {// ...code that handles overflow...return -1;} else if (rc == Number::Underflow) {// ...code that handles underflow...return -1;} else if (rc == Number::DivideByZero) {// ...code that handles divide-by-zero...return -1;}// ...}
再看使用異常處理的方式:
void f(Number x, Number y){try {// ...Number sum = x + y;Number diff = x - y;Number prod = x * y;Number quot = x / y;// ...}catch (Number::Overflow& exception) {// ...code that handles overflow...}catch (Number::Underflow& exception) {// ...code that handles underflow...}catch (Number::DivideByZero& exception) {// ...code that handles divide-by-zero...}}
如果有更多的運算,或者有更多的錯誤碼,異常處理的優(yōu)勢會更明顯。
使用異常可以使得代碼邏輯更清晰,將代碼按正確的邏輯列出來,邏輯更緊密代碼更容易讀懂,而錯誤處理可以單獨放到最后做處理。
異常可以選擇自己處理或者傳遞給上層處理
異常處理的關(guān)鍵點
不應(yīng)該使用異常處理做什么?
throw僅用于拋出一個錯誤,標(biāo)識函數(shù)沒有按設(shè)想的方式去執(zhí)行
只有在知道可以處理錯誤時,才使用catch來捕獲錯誤,例如轉(zhuǎn)換類型或者內(nèi)存分配失敗
不要使用throw來拋出編碼錯誤,應(yīng)該使用assert或者其它方法告訴編譯器或者崩潰進(jìn)程收集debug信息
如果有必須要崩潰的事件,或者無法恢復(fù)的問題,不應(yīng)該使用throw拋出,因為拋出來外部也無法處理,就應(yīng)該讓程序崩潰
try、catch不應(yīng)該簡單的用于函數(shù)返回值,函數(shù)的返回值應(yīng)該使用return操作,不應(yīng)該使用catch,這會給編程人員帶來誤解,同時也不應(yīng)該用異常來跳出循環(huán)
構(gòu)造函數(shù)可以拋出異常嗎?可以而且建議使用異常,因為構(gòu)造函數(shù)沒有返回值,所以只能拋出異常,也有另一種辦法就是添加一個成員變量標(biāo)識對象是否構(gòu)造成功,這種方法那就會額外添加一個返回該返回值的函數(shù),如果定義一個對象數(shù)組那就需要對數(shù)組每個對象都判斷是否構(gòu)造成功,這種代碼不太好。
構(gòu)造函數(shù)拋出異常會產(chǎn)生內(nèi)存泄漏嗎?不會,構(gòu)造函數(shù)拋出異常產(chǎn)生內(nèi)存泄漏那是編譯器的bug,已經(jīng)在21世紀(jì)修復(fù),不要聽信謠言。
void f() {X x; // If X::X() throws, the memory for x itself will not leakY* p = new Y(); // If Y::Y() throws, the memory for *p itself will not leak}永遠(yuǎn)不要在析構(gòu)函數(shù)中把異常拋出,還是拿對象數(shù)組舉例,數(shù)組里有多個對象,如果其中一個對象析構(gòu)過程中拋出異常,會導(dǎo)致剩余的對象都無法被析構(gòu),析構(gòu)函數(shù)應(yīng)該捕獲異常并把他們吞下或者終止程序,而不是拋出。
構(gòu)造函數(shù)內(nèi)申請完資源后拋出異常怎么辦?使用智能指針,關(guān)于char*也可以使用std::string代替。
using namespace std;class SPResourceClass {private:shared_ptr<int> m_p;shared_ptr<float> m_q;public:SPResourceClass() : m_p(new int), m_q(new float) { }// Implicitly defined dtor is OK for these members,// shared_ptr will clean up and avoid leaks regardless.};永遠(yuǎn)通過值傳遞方式用throw拋出異常,通過引用傳遞用catch來捕獲異常。
可以拋出基本類型也可以拋出對象,啥都可以
catch(...)可以捕獲所有異常
catch過程中不會觸發(fā)隱式類型轉(zhuǎn)換
異常被拋出,但是直到main函數(shù)也沒有被catch,就會std::terminate()
c++不像java,不會強制檢查異常,throw了外層即使沒有catch也會編譯通過
異常被拋出時,在catch之前,try和throw之間的所有局部對象都會被析構(gòu)
如果一個成員函數(shù)不會產(chǎn)生任何異常,可以使用noexcept關(guān)鍵字修飾
通過throw可以重新拋出異常
異常處理看似簡單好用,但它需要項目成員嚴(yán)格遵守開發(fā)規(guī)范,定好什么時候使用異常,什么時候不使用,而不是既使用異常又使用錯誤碼方式。
int main(){try {try {throw 20;}catch (int n) {cout << "Handle Partially ";throw; //Re-throwing an exception}}catch (int n) {cout << "Handle remaining ";}return 0;}
小測驗
你真的理解異常處理了嗎,我們可以做幾道測驗題:
看這幾段代碼會輸出什么:
測試代碼1:
using namespace std;int main(){int x = -1;// Some codecout << "Before try \n";try {cout << "Inside try \n";if (x < 0){throw x;cout << "After throw (Never executed) \n";}}catch (int x ) {cout << "Exception Caught \n";}cout << "After catch (Will be executed) \n";return 0;}
輸出:
Before tryInside tryException CaughtAfter catch (Will be executed)
throw后面的代碼不會被執(zhí)行
測試代碼2:
using namespace std;int main(){try {throw 10;}catch (char *excp) {cout << "Caught " << excp;}catch (...) {cout << "Default Exception\n";}return 0;}
輸出:
Default Exceptionthrow出來的10首先沒有匹配char*,而catch(...)可以捕獲所有異常。
測試代碼3:
using namespace std;int main(){try {throw 'a';}catch (int x) {cout << "Caught " << x;}catch (...) {cout << "Default Exception\n";}return 0;}
輸出:
Default Exception'a'是字符,不能隱式轉(zhuǎn)換為int型,所以還是匹配到了...中。
測試代碼4:
using namespace std;int main(){try {throw 'a';}catch (int x) {cout << "Caught ";}return 0;}
程序崩潰,因為拋出的異常直到main函數(shù)也沒有被捕獲,std::terminate()就會被調(diào)用來終止程序。
測試代碼5:
using namespace std;int main(){try {try {throw 20;}catch (int n) {cout << "Handle Partially ";throw; //Re-throwing an exception}}catch (int n) {cout << "Handle remaining ";}return 0;}
輸出:
Handle Partially Handle remainingcatch中的throw會重新拋出異常。
測試代碼6:
using namespace std;class Test {public:Test() { cout << "Constructor of Test " << endl; }~Test() { cout << "Destructor of Test " << endl; }};int main() {try {Test t1;throw 10;} catch(int i) {cout << "Caught " << i << endl;}}
輸出:
Constructor of TestDestructor of TestCaught 10
在拋出異常被捕獲之前,try和throw中的局部變量會被析構(gòu)。
小總結(jié)
異常處理對于錯誤的處理更簡潔,可以更方便的把錯誤信息反饋給調(diào)用者,同時不需要調(diào)用者使用額外的ifelse分支來處理成功或者不成功的情況。如果不是特別特別注重實時性能或者特別在乎程序的體積我們完全可以使用異常處理替代我們平時使用的c語言中的那種錯誤碼處理方式。
關(guān)于c++的異常處理就介紹到這里,你都了解了嗎?大家有問題可以
參考資料
https://www.zhihu.com/question/22889420
https://isocpp.org/wiki/faq/
https://docs.microsoft.com/en-us/cpp/cpp/errors-and-exception-handling-modern-cpp?view=vs-2019
https://blog.csdn.net/zhangyifei216/article/details/50410314
https://www.runoob.com/cplusplus/cpp-exceptions-handling.html
https://www.geeksforgeeks.org/exception-handling-c/
END
整理不易,點贊三連↓
