C++ 程序拋出異常後執行順序說明

1 析構函數中是否可以拋出異常

首先我們看一個常見的問題,析構函數中是否可以拋出異常。答案是C++標準指明析構函數不能、也不應該拋出異常!

C++異常處理模型是為C++語言量身設計的,更進一步的說,它實際上也是為C++語言中面向對象而服務的。

C++異常處理模型最大的特點和優勢就是對C++中的面向對象提供瞭最強大的無縫支持。

那麼如果對象在運行期間出現瞭異常,C++異常處理模型有責任清除那些由於出現異常所導致的已經失效瞭的對象(也即對象超出瞭它原來的作用域),並釋放對象原來所分配的資源, 這就是調用這些對象的析構函數來完成釋放資源的任務,所以從這個意義上說,析構函數已經變成瞭異常處理的一部分。

下面我們來看看析構函數中不能拋出異常的兩個理由:

1)如果析構函數拋出異常,則異常點之後的程序不會執行,如果析構函數在異常點之後執行瞭某些必要的動作比如釋放某些資源,則這些動作不會執行,會造成諸如資源泄漏的問題。

2)通常異常發生時,c++的機制會調用已經構造對象的析構函數來釋放資源,此時若析構函數本身也拋出異常,則前一個異常尚未處理,又有新的異常,會造成程序崩潰的問題。

那麼當無法保證在析構函數中不發生異常時, 該怎麼辦?

其實還是有很好辦法來解決的。那就是把異常完全封裝在析構函數內部,決不讓異常拋出函數之外。這是一種非常簡單,也非常有效的方法。

//析構函數
~Class()
{
 try{
 }
 catch(){ //這裡可以什麼都不做,隻是保證catch塊的程序拋出的異常不會被扔出析構函數之外。
 }
}

2 程序拋出異常後會怎樣

下面我們通過一個程序來觀察當程序中拋出異常瞭是否會調用析構函數,異常拋出中throw()後面的語句是否還會執行。

程序如下,我們創建一個類,然後構造一個類對象,當拋出異常我們看程序是否會進入析構函數以及throw()拋出異常後面的程序:

#include<iostream>
using namespace std;
class setTry{
public:
 setTry(){ //構造函數
  cout << "start!" << endl; // 1
 }
 ~setTry(){ //析構函數
  cout << "end!" << endl; // 4
 }
 void dosomething(){
  cout << "do something!" << endl; //類方法
 }
};
int main(void)
{
 setTry newOne;
 try{
  throw("error!"); //直接拋出異常 
  newOne.dosomething();
 }
 catch (char* one){ //接收char*類異常 
  cout << one << endl; // 2
 }
 catch (...){   //接收其他類型異常
  cout << "..." << endl;
 }
 cout << "return 0!"<<endl; // 3
 return 0;
}

上面程序運行結就是按標註的1、2、3、4步驟輸出的,結果如下圖所示:

從運行結果就可以看出,拋出異常try內部的throw()後面程序不會再執行,而try外部後面的程序會繼續執行。另外,析構函數在生存期結束也會被調用。

補充:C++異常捕獲和處理

0. 寫在前面

異常,讓一個函數可以在發現自己無法處理的錯誤時拋出一個異常,希望它的調用者可以直接或者間接處理這個問題。而傳統錯誤處理技術,檢查到一個錯誤,返回退出碼或者終止程序等等,此時我們隻知道有錯誤,但不能更清楚的知道哪種錯誤,因此,使用異常,就把錯誤和處理分開來,由庫函數拋出異常,由調用者捕獲這個異常,調用者就可以知道程序函數庫調用出現錯誤瞭,並去處理,而是否終止程序就把握在調用者手裡瞭。

1. 異常的拋出和處理

1. 異常處理的語句

try區段:這個區段中包含瞭可能發生異常的代碼,在發生瞭異常之後,需要通過throw拋出。

throw子句:throw 子句用於拋出異常,被拋出的異常可以是C++的內置類型(例如: throw int(1);),也可以是自定義類型。

catch子句:每個catch子句都代表著一種異常的處理。catch子句用於處理特定類型的異常。

例2:

 #include <iostream>
 using namespace std;
 void Test1()
 {
  try
  {
   char* p = new char[0x7fffffff]; //拋出異常
  }
  catch (exception e)
  {
   cout << e.what() << endl; //捕獲異常,然後程序結束
  }
 }
 int main()
 {
  Test1();
  system("pause");
  return 0;
 }

結果:

當使用new進行開空間時,申請內存失敗,就會拋出異常,此時捕獲到異常時,就可告訴使用者是哪裡的錯誤,便於修改

2. 異常的處理規則

異常是通過拋出對象而引發的,該對象的類型決定瞭應該激活哪個處理代碼。

被選中的處理代碼是調用鏈中與該對象類型匹配且離拋出異常位置最近的那一個。

拋出異常後會釋放局部存儲對象,所以被拋出的對象也就還給系統瞭,throw表達式會初始化一個拋出特殊的匿名對象,異常對象由編譯管理,異常對象在傳給對應的catch處理之後撤銷。

例2:

class Exception//異常類
{
public:
 Exception(const string& msg, int id)
 {
  _msg = msg;
  _id = id;
 }
 const char* What() const
 {
  return _msg.c_str();
 }
protected:
 string _msg;
 int _id;
};
template<size_t N = 10>
class Array
{
public:
 int& operator[](size_t pos)
 {
  if (pos >= N)
  {
   Exception e("下標不合法", 1); //出瞭這個作用域,拋出的異常對象就銷毀瞭,這時會生成一個匿名對象先接受這個對象,並傳到外層棧幀。
   throw e;
  }
  return a[pos];
 }
protected:
 int a[N];
};
int f()
{
 try
 {
  Array<> a;
  a[11];
 }
 catch (exception& e)
 {
  cout << e.what() << endl; //類型不匹配,找離拋出異常位置最近且類型匹配的那個。
 }
 return 0;
}
int main()
{
 try
 {
  f();
 }
 catch (Exception& e)
 {
  cout << e.What() << endl;
 }
 system("pause");
 return 0;
} 

結果:

f()函數中捕獲的異常是標準庫裡面的異常,但拋出異常的對象是自己定義的異常類,故類型不匹配,找離拋出異常最近的且類型匹配的Exception

3. 異常處理棧展開

1.在try的語句塊內聲明的變量在外部是不可以訪問的,即使是在catch子句內也不可以訪問。  

2.棧展開(尋找異常處理(exception handling)代碼)

棧展開會沿著嵌套函數的調用鏈不斷查找,知道找到瞭已拋出的異常匹配的catch子句。如果在最後還是沒有找到對應的catch子句的話,則退出主函數後查找過程終止,程序調用標準函數庫的terminate()函數,終止該程序的執行

具體過程:

當一個exception被拋出的時候,控制權會從函數調用中釋放出來,並需找一個可以處理的catch子句

對於一個拋出異常的try區段,程序會先檢查與該try區段關聯的catch子句,如果找到瞭匹配的catch子句,就使用這個catch子句處理這個異常。

沒有找到匹配的catch子句,如果這個try區段嵌套在其他try區段中,則繼續檢查與外層try匹配的catch子句。如果仍然沒有找到匹配的catch子句,則退出當前這個主調函數,並在調用瞭剛剛退出的這個函數的其他函數中尋找。

3. catch子句的查找:  

catch子句是按照出現的順序進行匹配的(以例2來說,異常先會匹配catch(exception e)子句,然後在匹配 catch (Exception e)子句,一步一步的棧展開)。在尋找catch子句的過程中,拋出的異常可以進行類型轉換,但是比較嚴格:

允許從非常量轉換到常量的類型轉換(權限縮小)

允許從派生類到基類的轉換。

允許數組被轉換成為指向數組(元素)類型的指針,函數被轉換成指向該函數類型的指針(降級問題)

標準算術類型的轉換(比如:把bool型和char型轉換成int型)和類類型轉換(使用類的類型轉換運算符和轉換構造函數)。

4. 異常處理中需要註意的問題

如果拋出的異常一直沒有函數捕獲(catch),則會一直上傳到c++運行系統那裡,導致整個程序的終止

一般在異常拋出後資源可以正常被釋放,但註意如果在類的構造函數中拋出異常,系統是不會調用它的析構函數的,處理方法是:如果在構造函數中要拋出異常,則在拋出前要記得刪除申請的資源。

異常處理僅僅通過類型而不是通過值來匹配的,所以catch塊的參數可以沒有參數名稱,隻需要參數類型。

函數原型中的異常說明要與實現中的異常說明一致,否則容易引起異常沖突。

應該在throw語句後寫上異常對象時,throw先通過Copy構造函數構造一個新對象,再把該新對象傳遞給 catch.  

註:那麼當異常拋出後新對象如何釋放?

異常處理機制保證:異常拋出的新對象並非創建在函數棧上,而是創建在專用的異常棧上,因此它才可以跨接多個函數而傳遞到上層,否則在棧清空的過程中就會被銷毀。所有從try到throw語句之間構造起來的對象的析構函數將被自動調用。但如果一直上溯到main函數後還沒有找到匹配的catch塊,那麼系統調用terminate()終止整個程序,這種情況下不能保證所有局部對象會被正確地銷毀。

catch塊的參數推薦采用地址傳遞而不是值傳遞,不僅可以提高效率,還可以利用對象的多態性。另外,派生類的異常撲獲要放到父類異常撲獲的前面,否則,派生類的異常無法被撲獲。

編寫異常說明時,要確保派生類成員函數的異常說明和基類成員函數的異常說明一致,即派生類改寫的虛函數的異常說明至少要和對應的基類虛函數的異常說明相同,甚至更加嚴格,更特殊。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。

推薦閱讀: