異常處理

 

第十三章 異常處理

 

  本章內容包括:

nJAVA異常處理

nC++異常處理

nC語言異常處理

 

一、C++異常處理

 

異常一般指的是程序運行期(Run-Time)發生的非正常情況。

 

1 JAVA異常處理

   先回顧一下JAVA異常處理的要點:

編寫JAVA異常處理程序,要藉助於JAVA的try,catch,finally,throw,throws幾個關鍵字和JDK中一些異常處理類共同完成。

   JAVA最重要的異常類是Exception,Exception類是異常的根類,有獲得異常信息、標識出現異常的對象是誰,異常棧跟蹤等功能。

Exception類下面又有很多子類定義了一些常用的、具體的異常,比如

算術異常ArithmeticException、

類型強制轉換異常類ClassCastException、

空對象異常NullPointException、

文件未找到異常FileNotFoundException、

文件結束異常EOFException等等。

如果需要,程序員也可以繼承Exception,編寫自已的異常處理子類。

 

上述異常對象是怎麼被創建(實例化)?又怎麼被處理的?有兩種方法:

(1)使用關鍵字throw,在語句內創建,在函數內處理:

      public void set(int age) {

        if(age>=0&&age<100)

            this.age=age;

        else

         throw new Exception(“輸入的年齡非法”)

            

   在運行上述函數時,如果實參是111,則throw創建一個Exception類(或其子類的)異常對象,這種創建異常對象的過程叫“拋出異常”,那麼這個異常對象被拋到哪裏去了?就在函數內部:

  public void set(int age) {

    try{  //有可能拋出異常的語句塊

if(age>=0&&age<100)

  this.age=age;

else

 throw new Exception(“輸入的年齡非法”);

 }

catch (Exception e)  //捕獲異常

     {//處理異常對象的語句

}

}

 

(2)使用關鍵字throws,在語句內創建,在函數外處理:

   在大多數實際編程中,異常對象在語句內創建,在函數外處理,我們之所以在調用JDK類庫的函數(或一些操作符比如“/”)時,能處理它們可能處現的異常,原因就是這些函數把它們內部語句中創建的異常對象拋到函數外面。

  public void set(int age) throws Exception{

    try{  //有可能拋出異常的語句塊

if (age>=0&&age<100)  this.age=age;

else  throw new Exception(“輸入的年齡非法”);

} }

在另外的函數中處理異常,可以是本類,也可以是其它類的函數:

public void handel() {  

catch (Exception e)  //捕獲異常

     {//處理異常對象的語句

}}

 

最後,複習一下try,catch,finally語句塊

  try  //可能出現異常的語句塊

    {

  //可能出現X1Exception,X2 Exception,X3 Exception

}

  catch(X1Exception e) 

    {//處理X1Exception異常}

  catch(X2Exception e) 

    {//處理X2Exception異常}

  catch(X3Exception e) 

    {//處理X3Exception異常}

  catch(Exception e)    //根異常類對象 

{//如果前面的catch塊未處理異常

//在這裏處理所有異常}

 finally

    {// 無論是否有異常,都必需執行}

 

 

 

 


二、C++異常處理

C++的異常處理概念、思想與JAVA很類似,語法設計也接近。

C++的異常處理納入標準比較晚,一些早期的C++編程書中沒有介紹,有的C++編譯器也不支持。

編寫C++異常處理程序,要藉助於try,catch,,throw,三個關鍵字,有時也需要藉助標準庫中的一些異常處理類共同完成。

 

1.標準庫中的異常處理類

   C++標準庫定義了一套異常類體系,其根類是exception頭文件下的exception類,這是個抽象類,有一個虛函數 what(),會返回一個const char*,用於表示被拋出的異常信息。如果用繼承exception方式寫自已的異常處理類,就要覆蓋what()。但C++允許程序員不用繼承exception也可以寫自已的異常處理類。

   exception類下有一個很常用的子類:bad_alloc, 當new創建對象失敗時拋出該異常。另外還有bad_cast, 當dynamic_cast失敗時拋出該異常;bad_typeid, 當typeid失敗時拋出該異常;等等。

下面的代碼樣式是很常見的:

calss A{……}   

int main( )

{

  try{

A *a=new A; //有可能出現內存不足等原因創建失敗,

//new拋出異常

 } 

catch(bad_alloc){  // bad_alloc是類

  //處理異常的代碼  }

}

 

2. 自定義異常類 

 

例2-1:不繼承exception類,自定義異常類:

class E1{

   private: const char *message;

   public: E1() {char=“E1提示:年齡太大”}

   const char *mywhat() const {return message}  

};

 

例2-2:繼承exception類,自定義異常類

class E2:public exception{

   private: const char *message;

   public: E1() {char=“E2提示:年齡錯”}

   const char* what() const {char=“E2提示:年齡太小”};    

}

繼承exception類寫法的好處是:可以“免費”利用exception類的其它功能。

  JAVA/C++自定義異常類都不困難,無非是一些提示信息,再高級的點的可以知道出現異常的對象的信息(大概要用到RTTI)、更高級的可以出現運行時堆棧信息以便爲調試提供幫助。

   怎麼使用自定義異常類?

 

3拋出異常:使用關鍵字throw在語句中拋出異常

例2-3

 public void set(int age)

 {

   if (age>100) throw E1(); //創建異常對象並將其拋出

if (age<0) throw E2(); //創建異常對象並將其拋出

if (age=100) throw“長命百歲”; //可以拋出字符串

if (age=0) throw 0 ;      //可以拋出整數

//其它代碼 ;

}

  

   這裏有點語法問題:在JAVA裏,語句拋出異常應爲throw new E1(參數列表), 意爲創建並拋出對象。

而C++不用new也可以創建對象,所以可以用throw E1(),E1()是構造函數,如有參數的話,應爲throw E1(參數列表)。

對於簡單的異常,不用拋出異常對象,拋出字串或整數也可以。

   在C++裏用throw new E1(參數列表) 拋出異常也是可以的,這樣好不好?

 

[觀點:] exception翻譯爲“異常”容易給人造成誤解,以爲只有程序出現錯誤時纔要做異常處理。翻譯爲“例外”或許更好,意爲非常規的、意料之外的、少見的情況,不一定非得是程序出錯。比如上例,0歲和100歲都是正確的,但很少見很不常規,很可能與主代碼要解決的不是一類問題。主代碼裏對這種“不屬於一類問題”的問題只拋出不做處理,也可以寫一個exception類單獨處理。

 

創建的異常對象被拋到何處?又怎樣被捕獲及處理?

 

4 異常對象的捕獲及處理

(1)在拋出異常的函數內處理

  public void set(int age)

 {

  try{

    if (age>100) throw E1(); //創建異常對象並將其拋出

if (age<0) throw E2(); //創建異常對象並將其拋出

if (age=100) throw“長命百歲”; //可以拋出字符串

if (age=0) throw 0 ;      //可以拋出整數

 }

//其它代碼 ;

catch(E1 ex ){ //處理E1異常   }

catch(E2 ex ){ //處理E2異常   }

catch(chost chr *str ){ //處理字符串異常—長命百歲   }

catch(int a){ //處理整數異常--0   }

catch(…)  { //處理所有異常,“…”是必需的   }

}

(2)在拋出異常的函數外處理

  函數要作以下聲明:

void GetTag() throw(int);表示只拋出int類型異常

void GetTag() throw(int,char,……);表示可以拋出int,char,……類型異常

void GetTag() throw();表示不會拋出任何類型異常

void GetTag() throw(...);表示可以拋出任何類型異常,”…”是必需的。

  C++編譯器會根據不同的throw形態作優化。

例:

public void set(int age) throw(…) { //可拋出任何異常

if (age>100) throw E1();

if (age<0) throw E2();

if (age=100) throw“長命百歲”;

if (age=0) throw 0 ;

//其它代碼 ;

}

//處理異常的可以是本類或其它類的或獨立的函數

public void handle(int age){

try{

  //調用set函數的代碼,可能會拋出異常

 }

catch(E1 ex ){ //處理E1異常   }

catch(E2 ex ){ //處理E2異常   }

catch(chost chr *str ){ //處理字符串異常—長命百歲}

catch(int a){ //處理整數異常--0  }

catch(…)  { //處理所有異常,“…”是必需的   }

} //C++沒有finally語句塊

 

(3)幾個問題

try語句塊裏不要放不可能出現異常的代碼。

C++沒有垃圾自動回收,所以很可能會出現處理完異常後應釋放的對象沒有釋放(因爲delete沒有執行等原因),可以在catch(…)塊中(處理所有異常的語句塊)把釋放資源的代碼再寫一遍。

當前catch塊異常如沒處理完,可以在再拋出,交給其它同類型catch塊繼續處理,比如:
catch(ex){
  …….
  throw;  //只需寫throw即可,只能寫在catch塊內
 …….

      }

如果異常出現而沒有合適的catch匹配它,系統會調用標準庫中的terminate()函數,使程序終止。

通常定義自己的Exception類的時候,都要有一個公共的基類, 這樣能夠保證寫代碼的時候catch所有的你自定義的Exception,以是一個完整的catch體系:

try { ...}

catch( ExceptionDerived  ex ) {……}

// catch其它派生類

catch(ExceptionBase  ex ) {……} //可以捕捉所有派生類的異常

catch( ... ) {  // 捕捉所有類的異常 };

 

 

 

 

 


三、C語言異常處理

 

C語言沒有專門的異常處理機制,但它通過標準庫頭文件setjmp.h中的兩個函數,setjmp和longjmp,可以提供異常處理的功能。在C++中,這兩個函數在頭文件csetjmp裏,主要是爲了與C兼容,C++本身不常用它。

 

本小節目地是通過學習setjmp和longjmp函數

瞭解C++/JAVA異常處理的底層原理;

與操作系統課程有關進程的某些知識建立關聯;

知道有一種很特別的函數:它可以返回兩次。

 

使用setjmp和longjmp函數實現異常處理的原理是:
(1) 首先程序員調用setjmp函數保存進程運行環境,並設置一個跳轉點,該跳轉點可以作爲異常處理程序的入口標識使用。

(2)當進程某處發生異常時,程序員調用longjmp函數,longjmp跳轉到setjmp設置的跳轉點上(相當於拋出異常),異常處理程序被觸發。異常處理程序處理完異常後,longjmp負責恢復由setjmp保存的進程現場數據,進程從程序被中斷的地方繼續進行。

(3)setjmp() 與 longjmp() 可以跨函數跳轉,又叫“非本地跳轉”。
     

setjmp函數的原型是:

int setjmp(jmp_buf envbuf);

envbuf(jmp_buf 結構體類型,在setjmp.h頭文件中聲明)是保存一份進程當前運行環境的數據緩衝區,以便後續的longjmp函數使用。setjmp函數初次啓用時先返回0,然後在longjmp被調用後再返回一次,第二次返回值是由longjmp函數指定的非0整型值。

  

longjmp函數的原型是:

void longjmp(jmp_buf envbuf, int status);

參數envbuf是setjmp函數所保存的數據緩衝區,參數status 設置setjmp函數的返回值。longjmp函數本身沒有返回值,它執行後跳轉到由setjmp函數設的跳轉點。

longjmp函數名意味着“遠程跳轉”或“非本地跳轉” ,可以跳出函數作用域以外執行功能。

setjmp() 與 longjmp() 函數調用關係是:
    首先調用 setjmp() 函數來初始化結構變量envbuf,保存進程運行環境,爲 longjmp() 函數提供跳轉點。setjmp() 函數是一個有趣的函數,它能返回兩次:第一次初始化時返回0,第二次遇到 longjmp() 函數調用後,longjmp() 函數使 setjmp() 函數發生第二次返回,返回值由 longjmp() 的status參數給出(整型,非零),第二次返回的正是爲longjmp() 函數提供跳轉點,可以作爲異常處理程序的入口標識。
    在使用 setjmp初始化envbuf後,可以在其後的程序中任意地方使用 longjmp函數跳轉回 setjmp函數提供的跳轉點,使該處的異常處理程序被執行,然後longjmp函數進行進程恢復工作(恢復envbuf保存的進程運行環境),程序回到原來位置繼續進行。

 

簡而言之,setjmp函數把進程運行環境保存到緩衝區envbuf並通過函數返回值提供異常處理程序的入口參考,而longjmp函數則跳轉到該入口啓動異常處理程序,然後恢復進程運行環境,使進程正常進行。這正是異常處理的一般機制:當異常發生時,先保存進程運行環境,處理異常,然後恢復進程運行環境,進程繼續正常進行。

 

setjmp函數與longjmp函數總是組合起來使用,它們是一對操作,可以使程序控制流有效轉移,可以有許多應用,但最常用於異常處理。它們不僅能實現非本地跳轉,當然它也能本地跳轉(goto語句能本地跳轉,不能非本地跳轉)

 下面是一個本地跳轉(在一個函數內跳轉)處理異常的僞代碼例子:

jmp_buf  mark; //緩衝區應是全局的

void main( void )  {
int  jmpref;

jmpref = setjmp( mark ); //第一次調用先返回0
if( jmpret == 0 ) {  //程序正常執行流
   // 其它代碼的執行…
   if( //程序中有異常A)    //相當於try

longjmp(mark, 1);  //使setjmp返回1,相當於throw

   }

   if( //程序中有異常B)    //相當於try

longjmp(mark, 2);

   }

 

else
if(jmpref ==1) {// 錯誤處理模塊,相當於catch }

  if(jmpref ==2) {// 錯誤處理模塊,相當於catch }

}

  

 

下面是一個非本地跳轉(在多個函數內跳轉)處理異常的僞代碼例子:

jmp_buf  mark; //緩衝區應是全局的

void Fun(){

// 其它代碼
If( //程序中有異常)

longjmp(mark, 1); //相當於throw異常到函數體外

}

void main( void )
{
int jmpret;

jmpret = setjmp( mark );
if( jmpret == 0 ) //程序正常執行流
  {// 其它代碼的執行

Fun ();  //調用該函數有可能throw異常

}
else
  if(jmpref ==1) {//處理Fun的異常,相當於catch }

}

 

通過對setjmp和longjmp函數的介紹,對C++(JAVA等)語言異常處理的一般性機制有所瞭解,這種機制實際上與中斷處理機制是相似的:當異常(或中斷)出現時,先保存進程現場,再非本地(或本地)跳轉到異常(或中斷)處理程序,最後恢復進程現場,使進程按原有順序繼續推進。

 

 

四、本章小結

通過對JAVA/C++/C異常處理的學習,我們看到異常處理的思想是:異常出現以後,正常程序的執行被suspended,與此同時,異常處理機制開始尋找程序中有能力處理這一異常的地點,異常被處理完成後,正常程序的執行便會被重新激活。這個過程和一般的中斷處理區別過程不大,try/catch語句塊掩蓋了底層細節,而C語言標準庫的setjmp和longjmp函數更接近底層一些。

 

本章課後習題:無

發佈了7 篇原創文章 · 獲贊 2 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章