C++中的try throw catch 異常處理

今天在開發過程當中調用一個庫函數結果庫函數有throw操做,當前代碼沒有對throw進行捕獲操做,致使進程在main 函數中捕獲到異常致使進程crash。因此藉此記錄下c++關於try,throw,catch的用法。html

 

程序運行時常會碰到一些異常狀況,例如:ios

  • 作除法的時候除數爲 0;
  • 用戶輸入年齡時輸入了一個負數;
  • 用 new 運算符動態分配空間時,空間不夠致使沒法分配;
  • 訪問數組元素時,下標越界;打開文件讀取時,文件不存在。


這些異常狀況,若是不能發現並加以處理,極可能會致使程序崩潰。

所謂「處理」,能夠是給出錯誤提示信息,而後讓程序沿一條不會出錯的路徑繼續執行;也多是不得不結束程序,但在結束前作一些必要的工做,如將內存中的數據寫入文件、關閉打開的文件、釋放動態分配的內存空間等。

一發現異常狀況就當即處理未必穩當,由於在一個函數執行過程當中發生的異常,在有的狀況下由該函數的調用者決定如何處理更加合適。尤爲像庫函數這類提供給程序員調用,用以完成與具體應用無關的通用功能的函數,執行過程當中貿然對異常進行處理,未必符合調用它的程序的須要。

此外,將異常分散在各處進行處理不利於代碼的維護,尤爲是對於在不一樣地方發生的同一種異常,都要編寫相同的處理代碼也是一種沒必要要的重複和冗餘。若是能在發生各類異常時讓程序都執行到同一個地方,這個地方可以對異常進行集中處理,則程序就會更容易編寫、維護。

鑑於上述緣由,c++ 引入了異常處理機制。其基本思想是:函數 A 在執行過程當中發現異常時能夠不加處理,而只是「拋出一個異常」給 A 的調用者,假定爲函數 B。

拋出異常而不加處理會致使函數 A 當即停止,在這種狀況下,函數 B 能夠選擇捕獲 A 拋出的異常進行處理,也能夠選擇置之不理。若是置之不理,這個異常就會被拋給 B 的調用者,以此類推。

若是一層層的函數都不處理異常,異常最終會被拋給最外層的 main 函數。main 函數應該處理異常。若是main函數也不處理異常,那麼程序就會當即異常地停止。c++

 

C++異常處理基本語法

C++ 經過 throw 語句和 try...catch 語句實現對異常的處理。throw 語句的語法以下:程序員

throw  表達式;

該語句拋出一個異常。異常是一個表達式,其值的類型能夠是基本類型,也能夠是類。數組

try...catch 語句的語法以下:安全

try {
    語句組
}
catch(異常類型) {
    異常處理代碼
}
...
catch(異常類型) {
    異常處理代碼
}

catch 能夠有多個,但至少要有一個。

不妨把 try 和其後{}中的內容稱做「try塊」,把 catch 和其後{}中的內容稱做「catch塊」。

try...catch 語句的執行過程是:ide

  • 執行 try 塊中的語句,若是執行的過程當中沒有異常拋出,那麼執行完後就執行最後一個 catch 塊後面的語句,全部 catch 塊中的語句都不會被執行;
  • 若是 try 塊執行的過程當中拋出了異常,那麼拋出異常後當即跳轉到第一個「異常類型」和拋出的異常類型匹配的 catch 塊中執行(稱做異常被該 catch 塊「捕獲」),執行完後再跳轉到最後一個 catch 塊後面繼續執行。

例以下面的程序:函數

 1 #include <iostream>
 2 using namespace std;
 3 int main()
 4 {
 5     double m ,n;
 6     cin >> m >> n;
 7     try {
 8         cout << "before dividing." << endl;
 9         if( n == 0)
10             throw -1; //拋出int類型異常
11         else
12             cout << m / n << endl;
13         cout << "after dividing." << endl;
14     }
15     catch(double d) {
16         cout << "catch(double) " << d <<  endl;
17     }
18     catch(int e) {
19         cout << "catch(int) " << e << endl;
20     }
21     cout << "finished" << endl;
22     return 0;
23 }

 

程序的運行結果以下:spa

9 6↙
before dividing.
1.5
after dividing.
finished

說明當 n 不爲 0 時,try 塊中不會拋出異常。所以程序在 try 塊正常執行完後,越過全部的 catch 塊繼續執行,catch 塊一個也不會執行。

程序的運行結果也可能以下:.net

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 塊可以捕獲任何尚未被捕獲的異常。例以下面的程序:

 1 #include <iostream>
 2 using namespace std;
 3 int main()
 4 {
 5     double m, n;
 6     cin >> m >> n;
 7     try {
 8         cout << "before dividing." << endl;
 9         if (n == 0)
10             throw - 1;  //拋出整型異常
11         else if (m == 0)
12             throw - 1.0;  //拋出 double 型異常
13         else
14             cout << m / n << endl;
15         cout << "after dividing." << endl;
16     }
17     catch (double d) {
18         cout << "catch (double)" << d << endl;
19     }
20     catch (...) {
21         cout << "catch (...)" << endl;
22     }
23     cout << "finished" << endl;
24     return 0;
25 }

 

程序的運行結果以下:

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 塊捕獲並處理,那麼該異常就不會拋給這個函數的調用者(也稱爲「上一層的函數」);若是異常在本函數中沒有被處理,則它就會被拋給上一層的函數。

例以下面的程序:

 1 #include <iostream>
 2 #include <string>
 3 using namespace std;
 4 class CException
 5 {
 6 public:
 7     string msg;
 8     CException(string s) : msg(s) {}
 9 };
10 double Devide(double x, double y)
11 {
12     if (y == 0)
13         throw CException("devided by zero");
14     cout << "in Devide" << endl;
15     return x / y;
16 }
17 int CountTax(int salary)
18 {
19     try {
20         if (salary < 0)
21             throw - 1;
22         cout << "counting tax" << endl;
23     }
24     catch (int) {
25         cout << "salary < 0" << endl;
26     }
27     cout << "tax counted" << endl;
28     return salary * 0.15;
29 }
30 int main()
31 {
32     double f = 1.2;
33     try {
34         CountTax(-1);
35         f = Devide(3, 0);
36         cout << "end of try block" << endl;
37     }
38     catch (CException e) {
39         cout << e.msg << endl;
40     }
41     cout << "f = " << f << endl;
42     cout << "finished" << endl;
43     return 0;
44 }

 

程序的輸出結果以下:

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 塊中拋出異常能夠知足這種須要。例如:

 1 #include <iostream>
 2 #include <string>
 3 using namespace std;
 4 int CountTax(int salary)
 5 {
 6     try {
 7         if( salary < 0 )
 8             throw string("zero salary");
 9         cout << "counting tax" << endl;
10 
11     }
12     catch (string s ) {
13         cout << "CountTax error : " << s << endl;
14         throw; //繼續拋出捕獲的異常
15     }
16     cout << "tax counted" << endl;
17     return salary * 0.15;
18 }
19 int main()
20 {
21     double f = 1.2;
22     try {
23         CountTax(-1);
24         cout << "end of try block" << endl;
25     }
26     catch(string s) {
27         cout << s << endl;
28     }
29     cout << "finished" << endl;
30     return 0;
31 }

 

程序的輸出結果以下:

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, double, A, B, 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 進行從多態基類對象(或引用)到派生類的引用的強制類型轉換時,若是轉換是不安全的,則會拋出此異常。程序示例以下:

 1 #include <iostream>
 2 #include <stdexcept>
 3 using namespace std;
 4 class Base
 5 {
 6     virtual void func() {}
 7 };
 8 class Derived : public Base
 9 {
10 public:
11     void Print() {}
12 };
13 void PrintObj(Base & b)
14 {
15     try {
16         Derived & rd = dynamic_cast <Derived &>(b);
17         //此轉換若不安全,會拋出 bad_cast 異常
18         rd.Print();
19     }
20     catch (bad_cast & e) {
21         cerr << e.what() << endl;
22     }
23 }
24 int main()
25 {
26     Base b;
27     PrintObj(b);
28     return 0;
29 }

程序的輸出結果以下:

Bad dynamic_cast!

在 PrintObj 函數中,經過 dynamic_cast 檢測 b 是否引用的是一個 Derived 對象,若是是,就調用其 Print 成員函數;若是不是,就拋出異常,不會調用 Derived::Print。

3) bad_alloc

在用 new 運算符進行動態內存分配時,若是沒有足夠的內存,則會引起此異常。程序示例以下:

 1 #include <iostream>
 2 #include <stdexcept>
 3 using namespace std;
 4 int main()
 5 {
 6     try {
 7         char * p = new char[0x7fffffff];  //沒法分配這麼多空間,會拋出異常
 8     }
 9     catch (bad_alloc & e)  {
10         cerr << e.what() << endl;
11     }
12     return 0;
13 }

程序的輸出結果以下:

bad allocation
ios_base::failure

在默認狀態下,輸入輸出流對象不會拋出此異常。若是用流對象的 exceptions 成員函數設置了一些標誌位,則在出現打開文件出錯、讀到輸入流的文件尾等狀況時會拋出此異常。此處再也不贅述。

4) out_of_range

用 vector 或 string 的 at 成員函數根據下標訪問元素時,若是下標越界,則會拋出此異常。例如:

 1 #include <iostream>
 2 #include <stdexcept>
 3 #include <vector>
 4 #include <string>
 5 using namespace std;
 6 int main()
 7 {
 8     vector<int> v(10);
 9     try {
10         v.at(100) = 100;  //拋出 out_of_range 異常
11     }
12     catch (out_of_range & e) {
13         cerr << e.what() << endl;
14     }
15     string s = "hello";
16     try {
17         char c = s.at(100);  //拋出 out_of_range 異常
18     }
19     catch (out_of_range & e) {
20         cerr << e.what() << endl;
21     }
22     return 0;
23 }

程序的輸出結果以下:

invalid vector <T> subscript
invalid string position


若是將v.at(100)換成v[100],將s.at(100)換成s[100],程序就不會引起異常(但可能致使程序崩潰)。由於 at 成員函數會檢測下標越界並拋出異常,而 operator[] 則不會。operator [] 相比 at 的好處就是不用判斷下標是否越界,所以執行速度更快。

轉自:http://c.biancheng.net/view/422.html

相關文章
相關標籤/搜索