異常的基本處理方式、異常的重解釋(在 catch 語句塊中將捕獲到的異常再次拋出)、自定義/標準庫異常類的使用(註意:賦值相容性原則)、異常的另類寫法(try...catch...)、異常函數聲明的方式(通過 throw 關鍵字指定可拋出的具體異常類型) ...
目錄
2、c++中的異常處理方式(try ... catch ...)
5、try..catch 另類寫法 和 函數異常聲明/定義 throw()
1、 異常 與 Bug 的區別
“異常”是我們在程式開發中必須考慮的一些特殊情況,是程式運行時就可預料的執行分支(註:異常是不可避免的,如程式運行時產生除 0 的情況;打開的外部文件不存在;數組訪問的越界等等);
“Bug”是程式的缺陷,是程式運行時不被預期的運行方式(註:Bug是人為的、可避免的;如使用野指針;堆數組使用結束後未釋放等等);
無論是“異常”還是“Bug”,都是程式正常運行過程中可能出現的意外情況。區別是“異常”可以捕獲,並做出合適的處理,“Bug"所帶來的後果是無法預測的,需要重寫相應的代碼。
2、c++中的異常處理方式(try ... catch ...)
(1)基本語法
1 // 異常基本語法 2 try 3 { 4 // 可能產生異常的代碼,若發生異常,通過 throw 關鍵字拋出異常 5 } 6 catch(異常類型) // 異常捕獲,註(...)代表捕獲所有異常 7 { 8 // 處理異常的代碼,該異常由try語句塊產生 9 }
(2)基本規則
1)同一個 try 語句可以跟上多個 catch 語句(在工程中,將可能產生異常的代碼放在 try 語句塊中,然後後面跟上多個 catch 語句);
2)try 語句中通過 throw 關鍵字 可以拋出任何類型的異常(int、字元串、對象等等);
3)catch 語句可以定義具體處理的異常類型,如 catch( int ) 只捕獲 int 類型異常, 但無法進一步獲取異常信息;catch( int a ) 只捕獲 int 類型異常,可以進一步獲取異常信息;
4)不同類型的異常由不同的 catch 語句負責處理;
5)catch(...) 用於處理所有類型的異常(只能放在所有 catch語句 的後面);
6)任何異常都只能被捕獲(catch)一次;
7)只要被 catch 捕獲一次,其它的 catch 就沒有捕獲機會了;
8)throw 拋出異常的類型 與 catch 捕獲異常的類型 必須嚴格匹配(不能進行類型轉換);若匹配成功,則能夠捕獲該異常;否則捕獲失敗,程式停止執行。
其實,為了更好的理解 異常拋出 和 異常捕獲 這兩個動作,我們可以將其想象成 函數調用,拋出異常好比是函數中實參,捕獲異常好比是函數中形參,只有當實參的類型 與 形參的類型嚴格匹配時,這次捕獲才能成功。為什麼說想象成函數調用,而不是正真的函數調用呢?原因就是:函數調用時,用實參初始化形參時可以進行隱式的類型轉換;但是在異常捕獲時,必須嚴格匹配異常類型。
(3)異常拋出(throw exception)的邏輯分析
情況1:異常代碼沒有放到 try{ } 語句中,這也意味著沒有對應的 catch 語句,其實就是普通函數調用,若此時拋出異常(throw),則程式停止執行;
情況2:異常代碼放到 try{ throw exception... } 語句中,這也意味著有對應的 catch 語句,則拋出異常時就會與 catch語句嚴格匹配;若匹配成功,則可以捕獲該異常,否則不能捕獲該異常,程式停止執行。
throw 拋出異常後,在發生異常的函數中至上而下的嚴格匹配每一個 catch 語句捕獲的異常類型,來判斷是否能夠捕獲該異常;
1) 若發生異常的函數中 能夠捕獲該異常,則程式接著往下執行;
2) 若發生異常的函數中 不能捕獲該異常,則未被處理的異常會順著函數調用棧向上傳播,直到該異常被捕獲為止,否則程式將停止執行;
總結:throw 拋出的異常必須被 對應的 catch 捕獲,否則程式將停止執行;
通過上圖來說明異常拋出後的執行順序:
1)在 函數 function3 中 拋出異常 throw 1;但是在 function3 中並不能捕獲該異常,則異常繼續向外層函數 function2 拋出
2)在 函數 function2 中,異常依舊沒有被捕獲,則異常繼續向外層函數 function1 拋出;
3)在 函數 function1 中,異常被 catch 捕獲和處理,然後程式繼續向下執行;
註:若在 函數 function1 中,異常還是沒有被捕獲,則異常會一直向外層函數拋出,直到該異常被捕獲為止,否在程式停止執行。
代碼展示:
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 double divide(double a, double b) 7 { 8 const double delta = 0.000000001; 9 double ret = 0; 10 11 if( !((-delta < b) && (b < delta)) ) 12 { 13 ret = a / b; 14 } 15 else 16 { 17 throw "Divided by zero..."; 18 } 19 20 cout << "divide(double a, double b)" << endl; 21 22 return ret; 23 } 24 25 double ExceptionFunc() 26 { 27 double d = divide(2, 0); 28 29 cout << "ExceptionFunc()" << endl; 30 31 return d; 32 } 33 34 int main(int argc, char *argv[]) 35 { 36 double d = ExceptionFunc(); 37 38 cout << "result = " << d << endl; 39 40 return 0; 41 } 42 43 /** 44 * 運行結果: 45 * terminate called after throwing an instance of 'char const*' 46 * Aborted (core dumped) 47 */ 48 49 /** 50 * 分析: 51 * throw "Divided by zero..."; 拋出異常後,divide(double a, double b) 函數不能捕獲該異常,則異常繼續拋給 ExceptionFunc(); 52 * 在 ExceptionFunc() 中,也不能捕獲該異常,則異常繼續拋給 main(); 53 * 在 main()中,也不能捕獲該異常,則程式停止執行。 54 */情況1 :異常案列復現
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 double divide(double a, double b) 7 { 8 const double delta = 0.000000001; 9 double ret = 0; 10 11 try 12 { 13 if( !((-delta < b) && (b < delta)) ) 14 { 15 ret = a / b; 16 } 17 else 18 { 19 throw "Divided by zero..."; 20 } 21 } 22 catch(char const* s) 23 { 24 cout << s << endl; 25 } 26 27 cout << "divide(double a, double b)" << endl; 28 29 return ret; 30 } 31 32 double ExceptionFunc() 33 { 34 double d = divide(2, 0); 35 36 cout << "ExceptionFunc()" << endl; 37 38 return d; 39 } 40 41 int main(int argc, char *argv[]) 42 { 43 ExceptionFunc(); 44 45 cout << "test end!" << endl; 46 47 return 0; 48 } 49 50 /** 51 * 運行結果: 52 * Divided by zero... 53 * divide(double a, double b) 54 * ExceptionFunc() 55 * test end! 56 */ 57 58 /** 59 * 分析: 60 * throw "Divided by zero..."; 拋出異常後,在divide(double a, double b) 中,異常被捕獲,則程式繼續向下執行; 61 * catch(char const* s) // throw "Divided by zero..." 62 * { 63 * cout << s << endl; 64 * } 65 * cout << "divide(double a, double b)" << endl; 66 * 67 * divide(2, 0); 函數調用結束,返回到 ExceptionFunc() 中, 68 * cout << "ExceptionFunc()" << endl; 69 * 70 * ExceptionFunc()調用結束,返回 main()中,繼續向下執行; 71 * cout << "test end!" << endl; 72 */情況2 :異常案列復現1
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 double divide(double a, double b) 7 { 8 const double delta = 0.000000001; 9 double ret = 0; 10 11 if( !((-delta < b) && (b < delta)) ) 12 { 13 ret = a / b; 14 } 15 else 16 { 17 throw "Divided by zero..."; 18 } 19 20 cout << "divide(double a, double b)" << endl; 21 22 return ret; 23 } 24 25 double ExceptionFunc() 26 { 27 double d; 28 29 try 30 { 31 d = divide(2, 0); 32 33 cout << "d = " << d << endl; 34 } 35 catch(char const* s) 36 { 37 cout << s << endl; 38 } 39 40 cout << "ExceptionFunc()" << endl; 41 42 return d; 43 } 44 45 int main(int argc, char *argv[]) 46 { 47 ExceptionFunc(); 48 49 cout << "test end!" << endl; 50 51 return 0; 52 } 53 54 /** 55 * 運行結果: 56 * Divided by zero... 57 * ExceptionFunc() 58 * test end! 59 */ 60 61 /** 62 * 分析: 63 * throw "Divided by zero..."; 拋出異常後,divide(double a, double b) 函數不能捕獲該異常,則異常繼續拋給 ExceptionFunc(); 64 * 在 ExceptionFunc() 中,異常被捕獲,則程式繼續向下執行; 65 * catch(char const* s) // throw "Divided by zero..." 66 * { 67 * cout << s << endl; 68 * } 69 * cout << "ExceptionFunc()" << endl; 70 * ExceptionFunc()調用結束,返回 main()中,繼續向下執行; 71 * cout << "test end!" << endl; 72 */情況2 :異常案列復現2
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 double divide(double a, double b) 7 { 8 const double delta = 0.000000001; 9 double ret = 0; 10 11 if( !((-delta < b) && (b < delta)) ) 12 { 13 ret = a / b; 14 } 15 else 16 { 17 throw "Divided by zero..."; 18 } 19 20 cout << "divide(double a, double b)" << endl; 21 22 return ret; 23 } 24 25 double ExceptionFunc() 26 { 27 double d = divide(2, 0); 28 29 cout << "ExceptionFunc()" << endl; 30 31 return d; 32 } 33 34 int main(int argc, char *argv[]) 35 { 36 try 37 { 38 double d = ExceptionFunc(); 39 40 cout << "d = " << d << endl; 41 } 42 catch(char const* s) 43 { 44 cout << s << endl; 45 } 46 47 cout << "test end!" << endl; 48 49 return 0; 50 } 51 52 /** 53 * 運行結果: 54 * Divided by zero... 55 * test end! 56 */ 57 58 /** 59 * 分析: 60 * throw "Divided by zero..."; 拋出異常後,divide(double a, double b) 函數不能捕獲該異常,則異常繼續拋給 ExceptionFunc(); 61 * 在 ExceptionFunc() 中,也不能捕獲該異常,則異常繼續拋給 main(); 62 * 在 main()中,異常與被捕獲,則程式繼續向下執行 63 * catch(char const* s) // throw "Divided by zero..." 64 * { 65 * cout << s << endl; 66 * } 67 * cout << "test end!" << endl; 68 */情況2 :異常案列復現3
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 void Demo1() 7 { 8 try 9 { 10 throw 2.0; 11 } 12 catch(char c) 13 { 14 cout << "catch(char c)" << endl; 15 } 16 catch(short c) 17 { 18 cout << "catch(short c)" << endl; 19 } 20 catch(double c) // throw 2.0; 21 { 22 cout << "catch(double c)" << endl; 23 } 24 catch(...) // 表示捕獲任意類型的異常 25 { 26 cout << "catch(...)" << endl; 27 } 28 } 29 30 void Demo2() 31 { 32 throw string("D.T.Software"); 33 } 34 35 int main(int argc, char *argv[]) 36 { 37 Demo1(); 38 39 try 40 { 41 Demo2(); 42 } 43 catch(char* s) 44 { 45 cout << "catch(char *s)" << endl; 46 } 47 catch(const char* cs) 48 { 49 cout << "catch(const char *cs)" << endl; 50 } 51 catch(string ss) // throw string("D.T.Software"); 52 { 53 cout << "catch(string ss)" << endl; 54 } 55 56 return 0; 57 } 58 /** 59 * 運行結果: 60 * catch(double c) 61 * catch(string ss) 62 */ 63 64 // 結論:異常類型嚴格匹配,(...)表示捕獲任意類型的異常情況2 :多分支的異常捕獲
總結:
情況1:只拋出異常,沒有對應地異常捕獲;( 沒有 try ... catch ... 結構 )
情況2:在try語句塊中拋出異常(直接在try語句塊中使用 throw 拋出異常;或者try語句塊中放入有異常的函數,間接地在函數中使用 throw 拋出異常),然後通過catch語句捕獲同類型的異常併進行異常處理;( 現象: 一個 try ... catch ... 結構 )
那麼現在我們考慮能不能在情況2的基礎上,將捕獲到異常繼續拋出呢?(在 catch 語句塊中重新拋出異常?)
可以在 catch 語句中重新拋出異常,此時需要外層的 try ... catch ...捕獲這個異常;
註:catch 語句中只拋出異常,什麼也不做;當捕獲任意類型的異常時,直接使用 throw 就行。
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 void Demo() 7 { 8 try 9 { 10 throw 'c'; 11 } 12 catch(int i) 13 { 14 cout << "Inner: catch(int i)" << endl; 15 throw i; 16 } 17 catch(...) 18 { 19 cout << "Inner: catch(...)" << endl; 20 throw; 21 } 22 } 23 24 int main(int argc, char *argv[]) 25 { 26 Demo(); 27 28 return 0; 29 } 30 31 /** 32 * 運行結果: 33 * Inner: catch(...) 34 * terminate called after throwing an instance of 'char' 35 * Aborted (core dumped) 36 */ 37 // 錯誤原因:異常重解釋之後,沒有被捕獲異常的重新解釋案列(error)
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 void Demo() 7 { 8 try 9 { 10 try 11 { 12 throw 'c'; 13 } 14 catch(int i) 15 { 16 cout << "Inner: catch(int i)" << endl; 17 throw i; 18 } 19 catch(...) 20 { 21 cout << "Inner: catch(...)" << endl; 22 throw; 23 } 24 } 25 catch(...) 26 { 27 cout << "Outer: catch(...)" << endl; 28 } 29 } 30 31 int main(int argc, char *argv[]) 32 { 33 Demo(); 34 35 return 0; 36 } 37 /** 38 * 運行結果: 39 * Inner: catch(...) 40 * Outer: catch(...) 41 */異常的重新解釋案列
為什麼要在 catch 語句塊中重新拋出異常?
在工程中,利用在 catch 語句塊中重新解釋異常並拋出這一特性,可以統一異常類型。(很晦澀,看下麵解釋....)
我們通過上圖來詳細說明這個情況:
背景介紹:出於開發效率考慮,在工程開發中一般會基於第三方庫來開發,但是,第三方庫中的某些功能並不完善(可讀性差),此時就需要封裝第三方庫中的這個功能。
在上圖中,由於第三方庫中 func()函數的異常類型是 int 類型,可讀性很差,不能夠直接通過異常結果知道該異常所代表的意思;基於這種情況,我們通過私有庫中的 MyFunc()函數對第三方庫中func()進行了封裝,使得 MyFunc()函數中的異常類型可以顯示更多的異常信息(MyFunc() 函數中的異常類型是自定義類型,可以是字元串、類類型等等),增強代碼的可讀性;為了將 func()中的異常類型 和 MyFunc() 中異常類型統一起來,我們可以在私有庫中的MyFunc() 函數中去捕獲第三方庫中的 func() 函數拋出的異常,然後根據捕獲到的異常重新解釋為我們想要的異常,這樣我們工程開發中所面對的異常類型就是一致的;接下來我們用代碼復現這個場景:
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 /* 7 假設:當前的函數是第三方庫中的函數,因此,我們無法修改源代碼 8 9 函數名: void func(int i) 10 拋出異常的類型: int 11 -1 ==》 參數異常 12 -2 ==》 運行異常 13 -3 ==》 超時異常 14 */ 15 void func(int i) 16 { 17 if( i < 0 ) 18 { 19 throw -1; 20 } 21 22 if( i > 100 ) 23 { 24 throw -2; 25 } 26 27 if( i == 11 ) 28 { 29 throw -3; 30 } 31 32 cout << "Run func..." << endl; 33 } <