你的c++團(tuán)隊(duì)還在禁用異常處理嗎?
關(guān)于c++的異常處理,網(wǎng)上有很多的爭(zhēng)議,本文會(huì)介紹c++的異常處理的使用,以及我們應(yīng)該使用異常處理嗎,以及使用異常處理需要注意的地方。
什么是異常處理?
異常處理當(dāng)然指的是對(duì)異常的處理,異常是指程序在執(zhí)行期間產(chǎn)生的問題,沒有按正確設(shè)想的流程走下去,比如除以零的操作,異常處理提供了一種轉(zhuǎn)移程序控制權(quán)的方式,這里涉及到三個(gè)關(guān)鍵字:
throw:當(dāng)問題出現(xiàn)時(shí),程序會(huì)通過throw來拋出一個(gè)異常
catch:在可能有throw想要處理問題的地方,通過catch關(guān)鍵字來捕獲異常
try:try塊中的代碼標(biāo)識(shí)將被激活的特定異常,它后面通常跟著一個(gè)或多個(gè)catch塊
直接看示例代碼:
void func() {
throw exception; // 拋出異常
}
int main() {
try { // try里放置可能拋出異常的代碼,塊中的代碼被稱為保護(hù)代碼
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)于是否使用異常一直都有爭(zhēng)議,典型的就是知乎上陳碩大神說的不應(yīng)該使用異常,還有就是google和美國國防部等都明確定義編碼規(guī)范來禁止在c++中使用異常,這里我找了很多中英文資料,在文末參考鏈接列舉了一些。
關(guān)于是否使用異常的討論帖子在這,https://www.zhihu.com/question/22889420
陳碩大神說的什么我就不貼出來了,他水平之高無需置疑,但他說的一些東西還是很有爭(zhēng)議的,關(guān)于異常處理,引用吳詠煒老師的一句話:“陳碩當(dāng)然是個(gè)技術(shù)大牛。不過,在編程語言這件事上,我更愿意信任 Bjarne Stroustrup、Herb Sutter、Scott Meyers 和 Andrei Alexandrescu。這些大神們都認(rèn)為異常是比返回錯(cuò)誤碼更好的錯(cuò)誤處理方式?!?/span>
而google明確禁用異常其實(shí)是有歷史包袱的,他們也認(rèn)同異常處理是比錯(cuò)誤碼更好的處理方式,但他們別無選擇,因?yàn)橐郧暗木幾g器對(duì)異常處理的不好,他們項(xiàng)目里面已經(jīng)有了大量的非異常安全的代碼,如果全改成異常處理的代碼是有很大的工作量的,具體可以看上面的鏈接和我文末引用的一些鏈接。
美國國防部禁用異常是出于實(shí)時(shí)性能考慮,工具鏈不能保證程序拋出異常時(shí)的實(shí)時(shí)性能,但國防部禁用了很多c++特性,例如內(nèi)存分配,我們真的追求飛機(jī)一樣的高性能嗎?
通過上面的介紹大家應(yīng)該能猜到我的結(jié)論了吧,當(dāng)然這不是我的結(jié)論,而是大佬們的結(jié)論:推薦使用異常處理。
異常處理有一些潛在的缺點(diǎn):
會(huì)有限的影響程序的性能,但正常工作流中不拋出異常的時(shí)候速度和普通函數(shù)一樣快,甚至更快
會(huì)導(dǎo)致程序體積變大10%-20%,但我們真的那么在乎程序的體積嗎(除了移動(dòng)端)
異常處理相對(duì)于使用錯(cuò)誤碼的好處:
如果不使用trycatch那就需要使用返回錯(cuò)誤碼的方式,那就必然增加ifelse語句,每次函數(shù)返回后都會(huì)增加判斷的開銷,如果可以消除trycatch,代碼可能會(huì)更健壯,舉例如下:
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();
// ...
}
而使用錯(cuò)誤碼方式:
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;
}
錯(cuò)誤碼方式對(duì)于問題的反向傳遞很麻煩,導(dǎo)致代碼腫脹,假如中間有一個(gè)環(huán)節(jié)忘記處理或處理有誤就會(huì)導(dǎo)致bug的產(chǎn)生,異常處理對(duì)于錯(cuò)誤的處理更簡(jiǎn)潔,可以更方便的把錯(cuò)誤信息反饋給調(diào)用者,同時(shí)不需要調(diào)用者使用額外的ifelse分支來處理成功或者不成功的情況。
一般來說使用錯(cuò)誤碼方式標(biāo)明函數(shù)是否成功執(zhí)行,一個(gè)值標(biāo)明函數(shù)成功執(zhí)行,另外一個(gè)或者多個(gè)值標(biāo)明函數(shù)執(zhí)行失敗,不同的錯(cuò)誤碼標(biāo)明不同的錯(cuò)誤類型,調(diào)用者需要對(duì)不同的錯(cuò)誤類型使用多個(gè)ifelse分支來處理。如果有更多ifelse,那么必然寫出更多測(cè)試用例,必然花費(fèi)更多精力,導(dǎo)致項(xiàng)目晚上線。
拿數(shù)值運(yùn)算代碼舉例:
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);
// ...
};
最簡(jiǎn)單的可以這樣調(diào)用:
void f(Number x, Number y) {
// ...
Number sum = x + y;
Number diff = x - y;
Number prod = x * y;
Number quot = x / y;
// ...
}
但是如果需要處理錯(cuò)誤,例如除0或者數(shù)值溢出等,函數(shù)得到的就是錯(cuò)誤的結(jié)果,調(diào)用者需要做處理。
先看使用錯(cuò)誤碼的方式:
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ùn)算,或者有更多的錯(cuò)誤碼,異常處理的優(yōu)勢(shì)會(huì)更明顯。
使用異常可以使得代碼邏輯更清晰,將代碼按正確的邏輯列出來,邏輯更緊密代碼更容易讀懂,而錯(cuò)誤處理可以單獨(dú)放到最后做處理。
異常可以選擇自己處理或者傳遞給上層處理
異常處理的關(guān)鍵點(diǎn)
不應(yīng)該使用異常處理做什么?
throw僅用于拋出一個(gè)錯(cuò)誤,標(biāo)識(shí)函數(shù)沒有按設(shè)想的方式去執(zhí)行
只有在知道可以處理錯(cuò)誤時(shí),才使用catch來捕獲錯(cuò)誤,例如轉(zhuǎn)換類型或者內(nèi)存分配失敗
不要使用throw來拋出編碼錯(cuò)誤,應(yīng)該使用assert或者其它方法告訴編譯器或者崩潰進(jìn)程收集debug信息
如果有必須要崩潰的事件,或者無法恢復(fù)的問題,不應(yīng)該使用throw拋出,因?yàn)閽伋鰜硗獠恳矡o法處理,就應(yīng)該讓程序崩潰
try、catch不應(yīng)該簡(jiǎn)單的用于函數(shù)返回值,函數(shù)的返回值應(yīng)該使用return操作,不應(yīng)該使用catch,這會(huì)給編程人員帶來誤解,同時(shí)也不應(yīng)該用異常來跳出循環(huán)
異常處理看似簡(jiǎn)單好用,但它需要項(xiàng)目成員嚴(yán)格遵守開發(fā)規(guī)范,定好什么時(shí)候使用異常,什么時(shí)候不使用,而不是既使用異常又使用錯(cuò)誤碼方式。
構(gòu)造函數(shù)可以拋出異常嗎?可以而且建議使用異常,因?yàn)闃?gòu)造函數(shù)沒有返回值,所以只能拋出異常,也有另一種辦法就是添加一個(gè)成員變量標(biāo)識(shí)對(duì)象是否構(gòu)造成功,這種方法那就會(huì)額外添加一個(gè)返回該返回值的函數(shù),如果定義一個(gè)對(duì)象數(shù)組那就需要對(duì)數(shù)組每個(gè)對(duì)象都判斷是否構(gòu)造成功,這種代碼不太好。
構(gòu)造函數(shù)拋出異常會(huì)產(chǎn)生內(nèi)存泄漏嗎?不會(huì),構(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 leak
Y* p = new Y(); // If Y::Y() throws, the memory for *p itself will not leak
}
永遠(yuǎn)不要在析構(gòu)函數(shù)中把異常拋出,還是拿對(duì)象數(shù)組舉例,數(shù)組里有多個(gè)對(duì)象,如果其中一個(gè)對(duì)象析構(gòu)過程中拋出異常,會(huì)導(dǎo)致剩余的對(duì)象都無法被析構(gòu),析構(gòu)函數(shù)應(yīng)該捕獲異常并把他們吞下或者終止程序,而不是拋出。
構(gòu)造函數(shù)內(nèi)申請(qǐng)完資源后拋出異常怎么辦?使用智能指針,關(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來捕獲異常。
可以拋出基本類型也可以拋出對(duì)象,啥都可以
catch(...)可以捕獲所有異常
catch過程中不會(huì)觸發(fā)隱式類型轉(zhuǎn)換
異常被拋出,但是直到main函數(shù)也沒有被catch,就會(huì)std::terminate()
c++不像java,不會(huì)強(qiáng)制檢查異常,throw了外層即使沒有catch也會(huì)編譯通過
異常被拋出時(shí),在catch之前,try和throw之間的所有局部對(duì)象都會(huì)被析構(gòu)
如果一個(gè)成員函數(shù)不會(huì)產(chǎn)生任何異常,可以使用noexcept關(guān)鍵字修飾
通過throw可以重新拋出異常
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;
}
小測(cè)驗(yàn)
你真的理解異常處理了嗎,我們可以做幾道測(cè)驗(yàn)題:
看這幾段代碼會(huì)輸出什么:
測(cè)試代碼1:
using namespace std;
int main()
{
int x = -1;
// Some code
cout << "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 try
Inside try
Exception Caught
After catch (Will be executed)
throw后面的代碼不會(huì)被執(zhí)行
測(cè)試代碼2:
using namespace std;
int main()
{
try {
throw 10;
}
catch (char *excp) {
cout << "Caught " << excp;
}
catch (...) {
cout << "Default Exception\n";
}
return 0;
}
輸出:
Default Exception
throw出來的10首先沒有匹配char*,而catch(...)可以捕獲所有異常。
測(cè)試代碼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型,所以還是匹配到了...中。
測(cè)試代碼4:
using namespace std;
int main()
{
try {
throw 'a';
}
catch (int x) {
cout << "Caught ";
}
return 0;
}
程序崩潰,因?yàn)閽伋龅漠惓V钡絤ain函數(shù)也沒有被捕獲,std::terminate()就會(huì)被調(diào)用來終止程序。
測(cè)試代碼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 remaining
catch中的throw會(huì)重新拋出異常。
測(cè)試代碼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 Test
Destructor of Test
Caught 10
在拋出異常被捕獲之前,try和throw中的局部變量會(huì)被析構(gòu)。
小總結(jié)
異常處理對(duì)于錯(cuò)誤的處理更簡(jiǎn)潔,可以更方便的把錯(cuò)誤信息反饋給調(diào)用者,同時(shí)不需要調(diào)用者使用額外的ifelse分支來處理成功或者不成功的情況。如果不是特別特別注重實(shí)時(shí)性能或者特別在乎程序的體積我們完全可以使用異常處理替代我們平時(shí)使用的c語言中的那種錯(cuò)誤碼處理方式。
關(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/
本文授權(quán)轉(zhuǎn)載自公眾號(hào)“程序喵大人”,作者程序喵大人
-END-
推薦閱讀
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!