C++中try throw catch異常處理的用法示例
前言
今天在開發過程中呼叫一個庫函式結果庫函式有throw操作,當前程式碼沒有對throw進行捕獲操作,導致程序在main 函式中捕獲到異常導致程序crash。所以藉此記錄下c++關於try,throw,catch的用法。
程式執行時常會碰到一些異常情況,例如:
- 做除法的時候除數為 0;
- 使用者輸入年齡時輸入了一個負數;
- 用 new 運算子動態分配空間時,空間不夠導致無法分配;
- 訪問陣列元素時,下標越界;開啟檔案讀取時,檔案不存在。
這些異常情況,如果不能發現並加以處理,很可能會導致程式崩潰。
所謂“處理”,可以是給出錯誤提示資訊,然後讓程式沿一條不會出錯的路徑繼續執行;也可能是不得不結束程式,但在結束前做一些必要的工作,如將記憶體中的資料寫入檔案、關閉開啟的檔案、釋放動態分配的記憶體空間等。
一發現異常情況就立即處理未必妥當,因為在一個函式執行過程中發生的異常,在有的情況下由該函式的呼叫者決定如何處理更加合適。尤其像庫函式這類提供給程式設計師呼叫,用以完成與具體應用無關的通用功能的函式,執行過程中貿然對異常進行處理,未必符合呼叫它的程式的需要。
此外,將異常分散在各處進行處理不利於程式碼的維護,尤其是對於在不同地方發生的同一種異常,都要編寫相同的處理程式碼也是一種不必要的重複和冗餘。如果能在發生各種異常時讓程式都執行到同一個地方,這個地方能夠對異常進行集中處理,則程式就會更容易編寫、維護。
鑑於上述原因,c++ 引入了異常處理機制。其基本思想是:函式 A 在執行過程中發現異常時可以不加處理,而只是“丟擲一個異常”給 A 的呼叫者,假定為函式 B。
丟擲異常而不加處理會導致函式 A 立即中止,在這種情況下,函式 B 可以選擇捕獲 A 拋出的異常進行處理,也可以選擇置之不理。如果置之不理,這個異常就會被拋給 B 的呼叫者,以此類推。
如果一層層的函式都不處理異常,異常最終會被拋給最外層的 main 函式。main 函式應該處理異常。如果main函式也不處理異常,那麼程式就會立即異常地中止。
C++異常處理基本語法
C++ 通過 throw 語句和 try…catch 語句實現對異常的處理。throw 語句的語法如下:
throw 表示式;
該語句丟擲一個異常。異常是一個表示式,其值的型別可以是基本型別,也可以是類。
try…catch 語句的語法如下:
try {
語句組
}
catch(異常型別) {
異常處理程式碼
}
...
catch(異常型別) {
異常處理程式碼
}
catch 可以有多個,但至少要有一個。
不妨把 try 和其後{}中的內容稱作“try塊”,把 catch 和其後{}中的內容稱作“catch塊”。
try…catch 語句的執行過程是:
- 執行 try 塊中的語句,如果執行的過程中沒有異常丟擲,那麼執行完後就執行最後一個 catch 塊後面的語句,所有 catch 塊中的語句都不會被執行;
- 如果 try 塊執行的過程中拋出了異常,那麼丟擲異常後立即跳轉到第一個“異常型別”和拋出的異常型別匹配的 catch 塊中執行(稱作異常被該 catch 塊“捕獲”),執行完後再跳轉到最後一個 catch 塊後面繼續執行。
例如下面的程式:
#include <iostream> using namespace std; int main() { double m,n; cin >> m >> n; try { cout << "before dividing." << endl; if( n == 0) throw -1; //丟擲int型別異常 else cout << m / n << endl; cout << "after dividing." << endl; } catch(double d) { cout << "catch(double) " << d << endl; } catch(int e) { cout << "catch(int) " << e << endl; } cout << "finished" << endl; return 0; }
程式的執行結果如下:
9 6↙
before dividing.
1.5
after dividing.
finished
說明當 n 不為 0 時,try 塊中不會丟擲異常。因此程式在 try 塊正常執行完後,越過所有的 catch 塊繼續執行,catch 塊一個也不會執行。
程式的執行結果也可能如下:
9 0↙
before dividing.
catch\(int) -1
finished
當 n 為 0 時,try 塊中會丟擲一個整型異常。丟擲異常後,try 塊立即停止執行。該整型異常會被型別匹配的第一個 catch 塊捕獲,即進入 catch(int e) 塊執行,該 catch 塊執行完畢後,程式繼續往後執行,直到正常結束。
如果拋出的異常沒有被 catch 塊捕獲,例如,將catch(int e),改為catch(char e),當輸入的 n 為 0 時,拋出的整型異常就沒有 catch 塊能捕獲,這個異常也就得不到處理,那麼程式就會立即中止,try…catch 後面的內容都不會被執行。
能夠捕獲任何異常的 catch 語句
如果希望不論丟擲哪種型別的異常都能捕獲,可以編寫如下 catch 塊:
catch(...) { ... }
這樣的 catch 塊能夠捕獲任何還沒有被捕獲的異常。例如下面的程式:
#include <iostream> using namespace std; int main() { double m,n; cin >> m >> n; try { cout << "before dividing." << endl; if (n == 0) throw - 1; //丟擲整型異常 else if (m == 0) throw - 1.0; //丟擲 double 型異常 else cout << m / n << endl; cout << "after dividing." << endl; } catch (double d) { cout << "catch (double)" << d << endl; } catch (...) { cout << "catch (...)" << endl; } cout << "finished" << endl; return 0; }
程式的執行結果如下:
9 0↙
before dividing.
catch (...)
finished
當 n 為 0 時,拋出的整型異常被catchy(...)捕獲。
程式的執行結果也可能如下:
0 6↙
before dividing.
catch (double) -1
finished
當 m 為 0 時,丟擲一個 double 型別的異常。雖然catch (double)和catch(...)都能匹配該異常,但是catch(double)是第一個能匹配的 catch 塊,因此會執行它,而不會執行catch(...)塊。
由於catch(...)能匹配任何型別的異常,它後面的 catch 塊實際上就不起作用,因此不要將它寫在其他 catch 塊前面。
異常的再丟擲
如果一個函式在執行過程中拋出的異常在本函式內就被 catch 塊捕獲並處理,那麼該異常就不會拋給這個函式的呼叫者(也稱為“上一層的函式”);如果異常在本函式中沒有被處理,則它就會被拋給上一層的函式。
例如下面的程式:
#include <iostream> #include <string> using namespace std; class CException { public: string msg; CException(string s) : msg(s) {} }; double Devide(double x,double y) { if (y == 0) throw CException("devided by zero"); cout << "in Devide" << endl; return x / y; } int CountTax(int salary) { try { if (salary < 0) throw - 1; cout << "counting tax" << endl; } catch (int) { cout << "salary < 0" << endl; } cout << "tax counted" << endl; return salary * 0.15; } int main() { double f = 1.2; try { CountTax(-1); f = Devide(3,0); cout << "end of try block" << endl; } catch (CException e) { cout << e.msg << endl; } cout << "f = " << f << endl; cout << "finished" << endl; return 0; }
程式的輸出結果如下:
salary < 0
tax counted
devided by zero
f=1.2
finished
CountTa 函式丟擲異常後自行處理,這個異常就不會繼續被拋給呼叫者,即 main 函式。因此在 main 函式的 try 塊中,CountTax 之後的語句還能正常執行,即會執行f = Devide(3,0);。
第 35 行,Devide 函式拋出了異常卻不處理,該異常就會被拋給 Devide 函式的呼叫者,即 main 函式。丟擲此異常後,Devide 函式立即結束,第 14 行不會被執行,函式也不會返回一個值,這從第 35 行 f 的值不會被修改可以看出。
Devide 函式中拋出的異常被 main 函式中型別匹配的 catch 塊捕獲。第 38 行中的 e 物件是用複製建構函式初始化的。
如果拋出的異常是派生類的物件,而 catch 塊的異常型別是基類,那麼這兩者也能夠匹配,因為派生類物件也是基類物件。
雖然函式也可以通過返回值或者傳引用的引數通知呼叫者發生了異常,但採用這種方式的話,每次呼叫函式時都要判斷是否發生了異常,這在函式被多處呼叫時比較麻煩。有了異常處理機制,可以將多處函式呼叫都寫在一個 try 塊中,任何一處呼叫發生異常都會被匹配的 catch 塊捕獲並處理,也就不需要每次呼叫後都判斷是否發生了異常。
有時,雖然在函式中對異常進行了處理,但是還是希望能夠通知呼叫者,以便讓呼叫者知道發生了異常,從而可以作進一步的處理。在 catch 塊中丟擲異常可以滿足這種需要。例如:
#include <iostream> #include <string> using namespace std; int CountTax(int salary) { try { if( salary < 0 ) throw string("zero salary"); cout << "counting tax" << endl; } catch (string s ) { cout << "CountTax error : " << s << endl; throw; //繼續丟擲捕獲的異常 } cout << "tax counted" << endl; return salary * 0.15; } int main() { double f = 1.2; try { CountTax(-1); cout << "end of try block" << endl; } catch(string s) { cout << s << endl; } cout << "finished" << endl; return 0; }
程式的輸出結果如下:
CountTax error:zero salary
zero salary
finished
第 14 行的throw;沒有指明丟擲什麼樣的異常,因此拋出的就是 catch 塊捕獲到的異常,即 string("zero salary")。這個異常會被 main 函式中的 catch 塊捕獲。
函式的異常宣告列表
為了增強程式的可讀性和可維護性,使程式設計師在使用一個函式時就能看出這個函式可能會丟擲哪些異常,C++ 允許在函式宣告和定義時,加上它所能拋出的異常的列表,具體寫法如下:
void func() throw (int,double,A,B,C);
或
void func() throw (int,C){...}
上面的寫法表明 func 可能丟擲 int 型、double 型以及 A、B、C 三種類型的異常。異常宣告列表可以在函式宣告時寫,也可以在函式定義時寫。如果兩處都寫,則兩處應一致。
如果異常宣告列表如下編寫:
void func() throw ();
則說明 func 函式不會拋出任何異常。
一個函式如果不交待能丟擲哪些型別的異常,就可以拋出任何型別的異常。
函式如果拋出了其異常宣告列表中沒有的異常,在編譯時不會引發錯誤,但在執行時, Dev C++ 編譯出來的程式會出錯;用 Visual Studio 2010 編譯出來的程式則不會出錯,異常宣告列表不起實際作用。
C++標準異常類
C++ 標準庫中有一些類代表異常,這些類都是從 exception 類派生而來的。常用的幾個異常類如圖 1 所示。
圖1:常用的異常類
bad_typeid、bad_cast、bad_alloc、ios_base::failure、out_of_range 都是 exception 類的派生類。C++ 程式在碰到某些異常時,即使程式中沒有寫 throw 語句,也會自動拋出上述異常類的物件。這些異常類還都有名為 what 的成員函式,返回字串形式的異常描述資訊。使用這些異常類需要包含標頭檔案 stdexcept。
下面分別介紹以上幾個異常類。本節程式的輸出以 Visual Studio 2010為準,Dev C++ 編譯的程式輸出有所不同。
1) bad_typeid
使用 typeid 運算子時,如果其運算元是一個多型類的指標,而該指標的值為 NULL,則會丟擲此異常。
2) bad_cast
在用 dynamic_cast 進行從多型基類物件(或引用)到派生類的引用的強制型別轉換時,如果轉換是不安全的,則會丟擲此異常。程式示例如下:
#include <iostream> #include <stdexcept> using namespace std; class Base { virtual void func() {} }; class Derived : public Base { public: void Print() {} }; void PrintObj(Base & b) { try { Derived & rd = dynamic_cast <Derived &>(b); //此轉換若不安全,會丟擲 bad_cast 異常 rd.Print(); } catch (bad_cast & e) { cerr << e.what() << endl; } } int main() { Base b; PrintObj(b); return 0; }
程式的輸出結果如下:
Bad dynamic_cast!
在 PrintObj 函式中,通過 dynamic_cast 檢測 b 是否引用的是一個 Derived 物件,如果是,就呼叫其 Print 成員函式;如果不是,就丟擲異常,不會呼叫 Derived::Print。
3) bad_alloc
在用 new 運算子進行動態記憶體分配時,如果沒有足夠的記憶體,則會引發此異常。程式示例如下:
#include <iostream> #include <stdexcept> using namespace std; int main() { try { char * p = new char[0x7fffffff]; //無法分配這麼多空間,會丟擲異常 } catch (bad_alloc & e) { cerr << e.what() << endl; } return 0; }
程式的輸出結果如下:
bad allocation
ios_base::failure
在預設狀態下,輸入輸出流物件不會丟擲此異常。如果用流物件的 exceptions 成員函式設定了一些標誌位,則在出現開啟檔案出錯、讀到輸入流的檔案尾等情況時會丟擲此異常。此處不再贅述。
4) out_of_range
用 vector 或 string 的 at 成員函式根據下標訪問元素時,如果下標越界,則會丟擲此異常。例如:
#include <iostream> #include <stdexcept> #include <vector> #include <string> using namespace std; int main() { vector<int> v(10); try { v.at(100) = 100; //丟擲 out_of_range 異常 } catch (out_of_range & e) { cerr << e.what() << endl; } string s = "hello"; try { char c = s.at(100); //丟擲 out_of_range 異常 } catch (out_of_range & e) { cerr << e.what() << endl; } return 0; }
程式的輸出結果如下:
invalid vector <T> subscript
invalid string position
如果將v.at(100)換成v[100],將s.at(100)換成s[100],程式就不會引發異常(但可能導致程式崩潰)。因為 at 成員函式會檢測下標越界並丟擲異常,而 operator[] 則不會。operator [] 相比 at 的好處就是不用判斷下標是否越界,因此執行速度更快。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對我們的支援。