1. 程式人生 > >C++中try--catch用法

C++中try--catch用法

 在c++中,可以直接丟擲異常之後自己進行捕捉處理,如:(這樣就可以在任何自己得到不想要的結果的時候進行中斷,比如在進行資料庫事務操作的時候,如果某一個語句返回SQL_ERROR則直接丟擲異常,在catch塊中進行事務回滾(回滾怎麼理解?))。

#include <iostream>  
#include <exception>  
using namespace std;  
int main () {  
    try  
    {  
        throw 1;  
        throw "error";  
    }  
    catch(char *str)  
    {  
        cout << str << endl;  
    }  
    catch(int i)  
    {  
        cout << i << endl;  
    }  
}

也可以自己定義異常類來進行處理:

#include <iostream>  
#include <exception>  
using namespace std;  
  
//可以自己定義Exception  
class myexception: public exception  
{  
    virtual const char* what() const throw()  
    {  
        return "My exception happened";  
    }  
}myex;  
  
int main () {  
    try  
    {      
        if(true)    //如果,則丟擲異常;  
            throw myex;  
    }  
    catch (exception& e)  
    {  
        cout << e.what() << endl;  
    }  
    return 0;  
}

同時也可以使用標準異常類進行處理:

#include <iostream>  
#include <exception>  
using namespace std;  
  
int main () {  
    try  
    {  
        int* myarray= new int[100000];  
    }  
    catch (exception& e)  
    {  
        cout << "Standard exception: " << e.what() << endl;  
    }  
    return 0;  
}

一、簡單的例子

首先通過一個簡單的例子來熟悉C++ 的 try/catch/throw(可根據單步除錯來熟悉,try catch throw部分是如何執行的):

#include <stdlib.h>
#include "iostream"
using namespace std;

double fuc(double x, double y)                        //定義函式
{
    if(y==0)
    {
        throw y;                                    //除數為0,丟擲異常
    }
    return x/y;                                    //否則返回兩個數的商
}

int _tmain(int argc, _TCHAR* argv[])
{
    double res;
    try                                            //定義異常
    {
        res=fuc(2,3);
        cout<<"The result of x/y is : "<<res<<endl;
        res=fuc(4,0);                                //出現異常
    }
    catch(double)                                    //捕獲並處理異常
    {
        cerr<<"error of dividing zero.\n";
        exit(1);                                    //異常退出程式
    }
    return 0;
}

catch 的資料型別需要與throw出來的資料型別相匹配的。

二、catch(...)的作用

  catch(…)能夠捕獲多種資料型別的異常物件,所以它提供給程式設計師一種對異常物件更好的控制手段,使開發的軟體系統有很好的可靠性。因此一個比較有經驗的程式設計師通常會這樣組織編寫它的程式碼模組,如下:

void Func()
{
  try
  {
    // 這裡的程式程式碼完成真正複雜的計算工作,這些程式碼在執行過程中
    // 有可能丟擲DataType1、DataType2和DataType3型別的異常物件。
  }
  catch(DataType1& d1)
  {
  }
  catch(DataType2& d2)
  {
  }
  catch(DataType3& d3)
  {
  }
  /*********************************************************
  注意上面try block中可能丟擲的DataType1、DataType2和DataType3三
  種類型的異常物件在前面都已經有對應的catch block來處理。但為什麼
  還要在最後再定義一個catch(…) block呢?這就是為了有更好的安全性和
  可靠性,避免上面的try block丟擲了其它未考慮到的異常物件時導致的程
  序出現意外崩潰的嚴重後果,而且這在用VC開發的系統上更特別有效,因
  為catch(…)能捕獲系統出現的異常,而系統異常往往令程式設計師頭痛了,現
  在系統一般都比較複雜,而且由很多人共同開發,一不小心就會導致一個
  指標變數指向了其它非法區域,結果意外災難不幸發生了。catch(…)為這種
  潛在的隱患提供了一種有效的補救措施。
  *********************************************************/

  catch(…)
  {
  }
}

三、異常中採用面向物件的處理

首先看下面的例子:

void OpenFile(string f)
{
    try
  {
     // 開啟檔案的操作,可能丟擲FileOpenException
  }
  catch(FileOpenException& fe)
  {
     // 處理這個異常,如果這個異常可以很好的得以恢復,那麼處理完畢後函式
     // 正常返回;否則必須重新丟擲這個異常,以供上層的呼叫函式來能再次處
     // 理這個異常物件
     int result = ReOpenFile(f);
     if (result == false) throw;
  }
}

void ReadFile(File f)
{
  try
  {
     // 從檔案中讀資料,可能丟擲FileReadException
  }
  catch(FileReadException& fe)
  {
     // 處理這個異常,如果這個異常可以很好的得以恢復,那麼處理完畢後函式
     // 正常返回;否則必須重新丟擲這個異常,以供上層的呼叫函式來能再次處
     // 理這個異常物件
     int result = ReReadFile(f);
     if (result == false) throw;
  }
}

void WriteFile(File f)
{
  try
  {
     // 往檔案中寫資料,可能丟擲FileWriteException
  }
  catch(FileWriteException& fe)
  {
      // 處理這個異常,如果這個異常可以很好的得以恢復,那麼處理完畢後函式
      // 正常返回;否則必須重新丟擲這個異常,以供上層的呼叫函式來能再次處理這個異常物件
    int result = ReWriteFile(f);
      if (result == false) throw; 
  } 
}

void Func()
{
  try
  {
     // 對檔案進行操作,可能出現FileWriteException、FileWriteException
     // 和FileWriteException異常
     OpenFile(…);
     ReadFile(…);
     WriteFile(…);
  }
  // 注意:FileException是FileOpenException、FileReadException和FileWriteException
  // 的基類,因此這裡定義的catch(FileException& fe)能捕獲所有與檔案操作失敗的異
  // 常。
  catch(FileException& fe)
  {
     ExceptionInfo* ef = fe.GetExceptionInfo();
     cout << “操作檔案時出現了不可恢復的錯誤,原因是:”<< fe << endl;
  }
}

下面是更多面向物件和異常處理結合的例子:

#include <iostream.h>
class ExceptionClass
{
    char* name;
public:
    ExceptionClass(const char* name="default name") 
    {
             cout<<"Construct "<<name<<endl;
             this->name=name;
    }
   ~ExceptionClass()
    {
             cout<<"Destruct "<<name<<endl;
    }
    void mythrow()
   {
            throw ExceptionClass("my throw");
   }
}

void main()
{
       ExceptionClass e("Test");
       try
       {
           e.mythrow();
       }  
       catch(...)
      {
         cout<<”*********”<<endl;
       }
}

這是輸出資訊:
Construct Test
Construct my throw
Destruct my throw
****************
Destruct my throw   (這裡是異常處理空間中對異常類的拷貝的析構)
Destruct Test
======================================

不過一般來說我們可能更習慣於把會產生異常的語句和要throw的異常類分成不同的類來寫,下面的程式碼可以是我們更願意書寫的:

複製程式碼

class ExceptionClass
{
public:
    ExceptionClass(const char* name="Exception Default Class")
   {
       cout<<"Exception Class Construct String"<<endl;
   }
   ~ExceptionClass()
   {
      cout<<"Exception Class Destruct String"<<endl;
   }
   void ReportError()
   {
      cout<<"Exception Class:: This is Report Error Message"<<endl;
   }
};

class ArguClass
{
   char* name;
public:
   ArguClass(char* name="default name")
   {
      cout<<"Construct String::"<<name<<endl;
      this->name=name;
   }
   ~ArguClass()
   {
      cout<<"Destruct String::"<<name<<endl;
   }
   void mythrow()
   {
      throw ExceptionClass("my throw");
   }      
};

_tmain()
{
   ArguClass e("haha");
   try
   {
     e.mythrow();
   }
   catch(int)
   {
     cout<<"If This is Message display screen, This is a Error!!"<<endl;  //這行不會執行
   }
   catch(ExceptionClass pTest)
   {
      pTest.ReportError();
   }
   catch(...)
  {
       cout<<"***************"<<endl;  
  }
}

複製程式碼

輸出Message:
Construct String::haha
Exception Class Construct String
Exception Class Destruct String
Exception Class:: This is Report Error Message
Exception Class Destruct String
Destruct String::haha

四、構造和析構中的異常丟擲
先看個程式,假如我在建構函式的地方丟擲異常,這個類的析構會被呼叫嗎?可如果不呼叫,那類裡的東西豈不是不能被釋放了?

複製程式碼

#include <iostream.h>
#include <stdlib.h>

class ExceptionClass1
{
       char* s;
public:
       ExceptionClass1()
      {
              cout<<"ExceptionClass1()"<<endl;
              s=new char[4];
              cout<<"throw a exception"<<endl;
              throw 18;
       }
       ~ExceptionClass1()
      {
              cout<<"~ExceptionClass1()"<<endl;
              delete[] s;
       }
};

void main()
{
       try
       {
             ExceptionClass1 e;
       }
       catch(...)
       {}
}

複製程式碼

結果為:
ExceptionClass1()
throw a exception

在這兩句輸出之間,我們已經給S分配了記憶體,但記憶體沒有被釋放(因為它是在解構函式中釋放的)。應該說這符合實際現象,因為物件沒有完整構造。

為了避免這種情況,我想你也許會說:應避免物件通過本身的建構函式涉及到異常丟擲。即:既不在建構函式中出現異常丟擲,也不應在建構函式呼叫的一切東西中出現異常丟擲。
但是在C++中可以在建構函式中丟擲異常,經典的解決方案是使用STL的標準類auto_ptr。

那麼,在解構函式中的情況呢?我們已經知道,異常丟擲之後,就要呼叫本身的解構函式,如果這解構函式中還有異常丟擲的話,則已存在的異常尚未被捕獲,會導致異常捕捉不到。

五、標準C++異常類

標準異常都派生自一個公共的基類exception。基類包含必要的多型性函式提供異常描述,可以被過載。下面是exception類的原型:

複製程式碼

class exception
{
public:
    exception() throw();
    exception(const exception& rhs) throw();
    exception& operator=(const exception& rhs) throw();
    virtual ~exception() throw();
    virtual const char *what() const throw();
};
 
C++有很多的標準異常類:
namespace std
{
    //exception派生
    class logic_error; //邏輯錯誤,在程式執行前可以檢測出來
 
    //logic_error派生
    class domain_error; //違反了前置條件
    class invalid_argument; //指出函式的一個無效引數
    class length_error; //指出有一個超過型別size_t的最大可表現值長度的物件的企圖
    class out_of_range; //引數越界
    class bad_cast; //在執行時型別識別中有一個無效的dynamic_cast表示式
    class bad_typeid; //報告在表達試typeid(*p)中有一個空指標p
   
    //exception派生
    class runtime_error; //執行時錯誤,僅在程式執行中檢測到
   
    //runtime_error派生
    class range_error; //違反後置條件
    class overflow_error; //報告一個算術溢位
    class bad_alloc; //儲存分配錯誤
}

複製程式碼

標準庫異常類定義在以下四個標頭檔案中

    1、exception標頭檔案:定義了最常見的標準異常類,其類名為exception。只通知異常的產生,但不會提供更多的資訊

    2、stdexcept標頭檔案定義了以下幾種常見異常類

函式                                                 功能或作用

exception                                            最常見的問題

runtime_error                                     執行時錯誤:僅在執行時才能檢測到的問題

range_error                                        執行時錯誤:生成的結果超出了有意義的值域範圍

overflow_error                                    執行時錯誤:計算上溢

underflow_error                                  執行時錯誤:計算下溢

logic_error                                           邏輯錯誤:可在執行前檢測到的問題

domain_error                                       邏輯錯誤:引數的結果值不存在

invalid_argument                                 邏輯錯誤:不合適的引數

length_error                                        邏輯錯誤:試圖生成一個超出該型別最大長度的物件

out_of_range                                       邏輯錯誤:使用一個超出有效範圍的值

    3、new標頭檔案定義了bad_alloc異常型別,提供因無法分配記憶體而由new丟擲的異常

    4、type_info標頭檔案定義了bad_cast異常型別(要使用type_info必須包含typeinfo標頭檔案)

    下面是使用異常類的例子:

    首先,我定義了幾個異常類,這些類也可以從標準異常類進行派生,如下

複製程式碼

class BadInitializers
{
public:
 BadInitializers() {}
};
class OutOfBounds
{
public:
 OutOfBounds(int i) { cout<<"Size "<<i<<" is illegal!!!"<<endl; }
};
class SizeMismatch
{
public:
 SizeMismatch() {}
};

複製程式碼

然後要在程式中需要的地方使用throw來丟擲異常類,兩個丟擲異常類的例子如下

複製程式碼

template <class T>
Array1D<T>::Array1D(int sz)
{
  if(sz<0)
  {
    //throw BadInitializers();
    throw invalid_argument("Size has to be bigger than 0!!!");

  }
 size=sz;
 element=new T[size];
}
template <class T>
T &Array1D<T>::operator[](int i) const
{
 if(i<0||i>=size)
 {
    throw OutOfBounds(i);
 }
 return element[i];
}

複製程式碼

然後在主程式中使用try...catch...來捕獲異常,並進行相應的處理,如下

複製程式碼

try
{
  int i=0;
  Array1D<int> a1(5);
  a1[0]=1;
  a1[1]=3;
  a1[2]=5;
  a1[3]=7;
  a1[4]=8;
  Array1D<int> a2(a1);
  for(i=0;i<a2.Size();i++)
  {
   cout<<a2[i]<<" ";
  }
  cout<<endl;
  Array1D<int> a3(5);
  a3=a1+a2;
  cout<<a3;
}
 catch(BadInitializers)
 {
  cout<<"Error:BadInitializers!!!"<<endl;
 }
 catch(OutOfBounds &e)
 {
  cout<<"Error:OutOfBounds!!!"<<endl;
 }
 catch(SizeMismatch &e)
 {
  cout<<"Error:SizeMismatch!!!"<<endl;
 }
 catch(invalid_argument &e)
 {
  cout<<"Error:"<<e.what()<<endl;
 }
 catch(...)
 {
  cout<<"An unknown error!!!"<<endl;
 }

複製程式碼

六、try finally使用

__try
{
   file://保護塊
}
__finally
{
  file://結束處理程式
}
在上面的程式碼段中,作業系統和編譯程式共同來確保結束處理程式中的__f i n a l l y程式碼塊能夠被執行,不管保護體(t r y塊)是如何退出的。不論你在保護體中使用r e t u r n,還是g o t o,或者是longjump,結束處理程式(f i n a l l y塊)都將被呼叫。

我們來看一個實列:(返回值:10, 沒有Leak,效能消耗:小)

複製程式碼

DWORD Func_SEHTerminateHandle()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem =  CreateSemaphore(NULL, 1, 1, lpSemName);
__try
{
  WaitForSingleObject(hSem,INFINITE);
  dwReturnData = 5;
}
__finally
{
  ReleaseSemaphore(hSem,1,NULL);
  CloseHandle(hSem);
}
dwReturnData += 5;
return dwReturnData;
}

複製程式碼

這段程式碼應該只是做為一個基礎函式,我們將在後面修改它,來看看結束處理程式的作用:
====================
在程式碼加一句:(返回值:5, 沒有Leak,效能消耗:中下)

複製程式碼

DWORD Func_SEHTerminateHandle()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem =  CreateSemaphore(NULL, 1, 1, lpSemName);
__try
{
  WaitForSingleObject(hSem,INFINITE);
  dwReturnData = 5;
  return dwReturnData;
}
__finally
{
  ReleaseSemaphore(hSem,1,NULL);
  CloseHandle(hSem);
}
dwReturnData += 5;
return dwReturnData;
}

複製程式碼

  在try塊的末尾增加了一個return語句。這個return語句告訴編譯程式在這裡要退出這個函式並返回dwTemp變數的內容,現在這個變數的值是5。但是,如果這個return語句被執行,該執行緒將不會釋放信標,其他執行緒也就不能再獲得對信標的控制。可以想象,這樣的執行次序會產生很大的問題,那些等待信標的執行緒可能永遠不會恢復執行。
  通過使用結束處理程式,可以避免return語句的過早執行。當return語句試圖退出try塊時,編譯程式要確保finally塊中的程式碼首先被執行。要保證finally塊中的程式碼在try塊中的return語句退出之前執行。在程式中,將ReleaseSemaphore的呼叫放在結束處理程式塊中,保證信標總會被釋放。這樣就不會造成一個執行緒一直佔有信標,否則將意味著所有其他等待信標的執行緒永遠不會被分配CPU時間。
  在finally塊中的程式碼執行之後,函式實際上就返回。任何出現在finally塊之下的程式碼將不再執行,因為函式已在try塊中返回。所以這個函式的返回值是5,而不是10。
  讀者可能要問編譯程式是如何保證在try塊可以退出之前執行finally塊的。當編譯程式檢查原始碼時,它看到在try塊中有return語句。這樣,編譯程式就生成程式碼將返回值(本例中是5)儲存在一個編譯程式建立的臨時變數中。編譯程式然後再生成程式碼來執行f i n a l l y塊中包含的指令,這稱為區域性展開。更特殊的情況是,由於try塊中存在過早退出的程式碼,從而產生區域性展開,導致系統執行finally塊中的內容。在finally塊中的指令執行之後,編譯程式臨時變數的值被取出並從函式中返回。
  可以看到,要完成這些事情,編譯程式必須生成附加的程式碼,系統要執行額外的工作。

finally塊的總結性說明
我們已經明確區分了強制執行finally塊的兩種情況:
• 從try塊進入finally塊的正常控制流。
• 區域性展開:從try塊的過早退出(goto、long jump、continue、break、return等)強制控制轉移到finally塊。
第三種情況,全域性展開( global unwind),這個以後再看。

七、C++異常引數傳遞

從語法上看,在函式裡宣告引數與在catch子句中宣告引數是一樣的,catch裡的引數可以是值型別,引用型別,指標型別。例如:

複製程式碼

try
{
   .....
}
catch(A a)
{
}
catch(B& b)
{
}
catch(C* c)
{
}

複製程式碼

  儘管表面是它們是一樣的,但是編譯器對二者的處理卻又很大的不同。呼叫函式時,程式的控制權最終還會返回到函式的呼叫處,但是丟擲一個異常時,控制權永遠不會回到丟擲異常的地方。

複製程式碼

class A;
void func_throw()
{
     A a;
     throw a;  //丟擲的是a的拷貝,拷貝到一個臨時物件裡
}
try
{
    func_throw();
}
catch(A a)  //臨時物件的拷貝
{
}

複製程式碼

  當我們丟擲一個異常物件時,丟擲的是這個異常物件的拷貝。當異常物件被拷貝時,拷貝操作是由物件的拷貝建構函式完成的。該拷貝建構函式是物件的靜態型別(static type)所對應類的拷貝建構函式,而不是物件的動態型別(dynamic type)對應類的拷貝建構函式。此時物件會丟失RTTI資訊。
  異常是其它物件的拷貝,這個事實影響到你如何在catch塊中再丟擲一個異常。比如下面這兩個catch塊,乍一看好像一樣:

複製程式碼

catch (A& w) // 捕獲異常
{
 // 處理異常
 throw; // 重新丟擲異常,讓它繼續傳遞
}
catch (A& w) // 捕獲Widget異常
{
 // 處理異常
 throw w; // 傳遞被捕獲異常的拷貝
}

複製程式碼

  第一個塊中重新丟擲的是當前異常(current exception),無論它是什麼型別。(有可能是A的派生類)  
  第二個catch塊重新丟擲的是新異常,失去了原來的型別資訊。
  一般來說,你應該用throw來重新丟擲當前的異常,因為這樣不會改變被傳遞出去的異常型別,而且更有效率,因為不用生成一個新拷貝。
看看以下這三種宣告:
catch (A w) ... // 通過傳值
catch (A& w) ... // 通過傳遞引用,一個被異常丟擲的物件(總是一個臨時物件)可以通過普通的引用捕獲
catch (const A& w) ... //const引用

  回到異常物件拷貝上來。我們知道,當用傳值的方式傳遞函式的引數,我們製造了被傳遞物件的一個拷貝,並把這個拷貝儲存到函式的引數裡。同樣我們通過傳值的方式傳遞一個異常時,也是這麼做的當我們這樣宣告一個catch子句時:
catch (A w) ... // 通過傳值捕獲
會建立兩個被丟擲物件的拷貝,一個是所有異常都必須建立的臨時物件,第二個是把臨時物件拷貝進w中。實際上,編譯器會優化掉一個拷貝。同樣,當我們通過引用捕獲異常時,
catch (A& w) ... // 通過引用捕獲
catch (const A& w) ... //const引用捕獲
這仍舊會建立一個被丟擲物件的拷貝:拷貝是一個臨時物件。相反當我們通過引用傳遞函式引數時,沒有進行物件拷貝。話雖如此,但是不是所有編譯器都如此。

  另外,通過指標丟擲異常與通過指標傳遞引數是相同的。不論哪種方法都是一個指標的拷貝被傳遞。你不能認為丟擲的指標是一個指向區域性物件的指標,因為當異常離開區域性變數的生存空間時,該區域性變數已經被釋放。Catch子句將獲得一個指向已經不存在的物件的指標。這種行為在設計時應該予以避免。
  另外一個重要的差異是在函式呼叫者或丟擲異常者與被呼叫者或異常捕獲者之間的型別匹配的過程不同。在函式傳遞引數時,如果引數不匹配,那麼編譯器會嘗試一個型別轉換,如果存在的話。而對於異常處理的話,則完全不是這樣。見一下的例子:

複製程式碼

void func_throw()
{
     CString a;
     throw a;  //丟擲的是a的拷貝,拷貝到一個臨時物件裡
}

try
{
func_throw();
}
catch(const char* s)
{
}

複製程式碼

  丟擲的是CString,如果用const char*來捕獲的話,是捕獲不到這個異常的。
  儘管如此,在catch子句中進行異常匹配時可以進行兩種型別轉換。第一種是基類與派生類的轉換,一個用來捕獲基類的catch子句也可以處理派生類型別的異常。反過來,用來捕獲派生類的無法捕獲基類的異常。
  第二種是允許從一個型別化指標(typed pointer)轉變成無型別指標(untyped pointer),所以帶有const void* 指標的catch子句能捕獲任何型別的指標型別異常:
catch (const void*) ... //可以捕獲所有指標異常
  另外,你還可以用catch(...)來捕獲所有異常,注意是三個點。
  傳遞引數和傳遞異常間最後一點差別是catch子句匹配順序總是取決於它們在程式中出現的順序。因此一個派生類異常可能被處理其基類異常的catch子句捕獲,這叫異常截獲,一般的編譯器會有警告。

複製程式碼

class A {
public:
A()
{
cout << "class A creates" << endl;
}
void print()
{
cout << "A" << endl;
}
~A()
{
cout << "class A destruct" << endl;
}
};
class B: public A
{
public:
B()
{
cout << "class B create" << endl;
}
void print()
{
cout << "B" << endl;
}
~B()
{
cout << "class B destruct" << endl;
}
};
void func()
{
    B b;
    throw b;
}
try
{
    func();
}
catch( B& b) //必須將B放前面,如果把A放前面,B放後面,那麼B型別的異常會先被截獲。
{
    b.print();
}
catch (A& a)
{
    a.print() ;
}