C、C++、Java語言中異常處理機制淺析
一、 異常處理 (ExceptionalHandling)概述
1. 異常處理
異常處理又稱異常錯誤處理,它提供了處理程序運行時出現任何意外或異常情況的方法。異常處理通常是防止未知錯誤的發生所采取的處理措施,對于某一類型的錯誤,異常處理應該提供相應的處理方法。例如,在設計程序時,如果可能會碰到除0錯誤或者數組訪問越界錯誤,程序員應該在程序中設計相應的異常處理代碼以便發生異常情況時,程序做出相應的處理。
2. 異常處理的兩類模型
(1)終止模型
在這種模型中,異常是致命的,它一旦發生,將導致程序終止。這種模型被C++和Java語言所支持。
(2)恢復模型
當發生異常時,由異常處理方法進行處理,處理完畢后程序返回繼續執行。
二、 C語言異常處理
1. 常用方法
(1)使用abort()和exit()兩個函數,他們聲明在<stdlib.h>中;
(2)使用assert宏調用,它位于<assert.h>中。assert(expression)當expression為0時,就好引發abort();
(3)使用全局變量errno,它由C語言庫函數提供,位于<errno.h>中;
(4)使用goto語用局部跳轉到異常處理代碼處;
(5)使用setjmp和longjmp實現全局跳轉,它們聲明<setjmp.h>中,一般由setjmp保存jmp_buf上下文結構體,然后由longjmp跳回到此時。
2. 實例演示
實例一 :使用exit()終止程序運行
#include<stdio.h> #include<stdlib.h> voidDivideError(void) { printf("divide 0 error!\n"); } doubledivide(double x,double y) { if(y==0) exit(EXIT_FAILURE);//此時EXIT_FAILURE=1 //也可以使用atexit()函數來注冊異常處理函數,但此時異常處理函//數必須形如voidfun(void); else return x/y; } intmain() { double x,y,res; printf("x="); scanf("%lf",&x); printf("y="); scanf("%lf",&y); atexit(DivideError); res=divide(x,y); printf("result=%lf\n",res); return 0; } 實例二:使用assert(expression) #include<stdio.h> #include<assert.h> intmain() { int a,b,res; res=scanf("%d,%d",&a,&b); //scnaf函數返回從stdin流中成功讀入的數據個數 assert(res==2); //如果res!=2,則出現異常 return 0; } 實例三:使用全局變量errno來獲取異常情況的編號 #include<stdio.h> #include<errno.h> intmain() { char filename[80]; errno=0; scanf("%s",filename); FILE* fp=fopen(filename,"r"); printf("%d\n",errno); //如果此時文件打不開,那么errno=2 return 0; } 實例四:使用goto實現局部跳轉 #include<stdio.h> #include<stdlib.h> intmain() { double x,y,res; int tag=0; if(tag==1) { Error: printf("divide0 error!\n"); exit(1); } printf("x="); scanf("%lf",&x); printf("y="); scanf("%lf",&y); if(y==0) { tag=1; goto Error; } else { res=divide(x,y); printf("result =%lf\n",res); } return 0; } 實例五:使用setjmp和longjmp實現全局跳轉 #include<stdio.h> #include<setjmp.h> jmp_buf mark; //保存跳轉點上下文環境的結構體 void DivideError() { longjmp(mark,1); } intmain() { double a,b,res; printf("a="); scanf("%lf",&a); printf("b="); scanf("%lf",&b); if(setjmp(mark)==0) { if(b==0) DivideError(); else { res=a/b; printf("the result is%lf\n",res); } } else printf("Divide 0 error!\n"); return 0; }
三、 C++異常處理
1. C++異常類的編寫
#include<iostream> #include<exception> using namespacestd; class DivideError:public exception //E從exception類派生而來 { public: const char* what() //必須實現虛函數,它在exception類中定義, //函數原型是 virtual const char* what() const throw() { return "除數為0錯誤\n"; } }; double divide(doublex,double y) { if(y==0) throw DivideError(); //拋出異常 else return x/y; } void main() { double x,y; double res; try { cin>>x>>y; res=divide(x,y); cout<<res<<endl; } catch(DivideError& e) { cerr<<e.what(); } }
2. 對try與catch的說明
程序員應該把可能會出現異常的代碼段放入try { }中,當try { }語句塊中出現異常時,編譯器將找相應的catch(Exception& e )來捕獲異常。注意不管是用throw Exception()主動拋出異常還是在try{ }語句塊中出現異常,此時異常類型必須與相應的catch(Exception& e)中異常類型一致,或者定義catch(…) { }語句塊,這表明編譯器在本函數中找不到異常處理,則到catch(…) { }中按照相應的代碼去處理。如果這些都沒有,編譯器會返回上一級調用函數尋找匹配的catch,這樣一級一級往上找,都找不到,則系統調用terminate,terminate調用abort()終止整個程序。
實例:
void func1() { throw 1; } void func2() { throw “helloworld”; } void func3() { throwException(); } void main() { try { func1(); func2(); func3(); } catch(int e) //捕獲func1()中異常 { //To do Something } catch(const char* str) //捕獲func2()中異常 { //To do Something } catch(Exception& e) //捕獲func3()中異常 { //To do Something } catch(…) //都不匹配則執行此處代碼 { // To do Something } }
3. 對throw的理解
(1) 當我們在自己定義的函數中拋出(throw)一個異常對象時,如果此異常對象在本函數定義,那么編譯器會拷貝此對象到某個特定的區域。因為當此函數返回時,原本在該函數定義的對象空間將被釋放,對象也就不存在了。編譯器拷貝了對象,在其他函數使用catch語句時可以訪問到該對象副本。如:
void func()
{
Exception e;
throw e; //當func()返回時,e就不存在了
}
(2) 盡量避免throw對象的指針,如下例:
#include <iostream> #include <exception> using namespace std; class Exception: public exception { public: constchar* what() { return "異常出現了\n"; } }; void func() { thrownew E(); //拋出一個對象指針 } void main() { try { func(); } catch(E *p) { cerr<<p->what(); int x,y; x=1; y=0; x=x/y; //出現新的異常 deletep; //delete p得不到執行,此時申請對象的空間不會被釋放, } }
解決方案之一:
在程序中定義一個異常處理函數,如void handler(void);
并且在main函數中加入代碼:
catch(…)
{
handler();
}
所以我們在拋出異常時,推薦使用throw Exception(參數),相應的catch(constException& e),這樣在拋出異常時,編譯器會對沒有看到具體名字的臨時變量做出一些優化措施,同時在catch中也避免了無謂的對象拷貝。
(3)不要在析構函數中throw異常,如下例:
#include <iostream> #include <exception> #include <string> using namespace std; class E { public: E( ) { } ~E () { throw string("123"); } }; void main() { try { Ee; throwstring("abc"); //此時拋出的異常會被下面的catch捕獲 } catch(string& s) { cout<<s<<endl; } } //對象e的生命周期結束,系統調用其析構函數釋放空間,但卻throw了異常,沒有catch捕獲,造成程序崩潰。
解決方案一:
增加一個異常處理函數
void handler()
{
//To do Something
abort( );
}
在main函數開始處加入代碼:set_terminate(handler),這樣在main函數結束前,系統調用handler處理異常。
解決方案二:
有時我們要編寫建立數據庫連接的程序,此時我們定義一個Database類來管理我們的數據庫,在Database類的析構函數中,我們通常希望將打開的數據庫連接關閉,如果數據庫關閉時出現異常,那么我們就需要處理。如下例:
#include <iostream> #include <exception> using namespace std; class Database { public: Database& CreateConn() { //To do Something return*this; } ~Database() { if(isclosed)//數據庫確實關閉 { //Todo Something } else { try { close(); } catch(...) { //做出處理,如寫日志文件 } } } private: void close() //關閉連接 { //To do Something } bool isclosed; }; void main() { Database db; }
也就是說在析構函數中并不是拋出異常,取而代之的是處理異常。
(4)在構造函數中拋出異常
構造函數的主要作用是利用構造函數參數來初始化對象,如果此時給出的參數不合法,那么應該對其進行處理。我們信奉的原則是問題早發現,早解決。如下例:
#include <iostream> #include <exception> #include <string> using namespace std; const int max=1000; class InputException: public exception { public: const char* what() { return "輸入錯誤!\n"; } }; class Point { private: int x,y; public: Point(int _x,int _y) { if(_x<0|| _x>=max || _y<0 || _y>=max) throw InputException(); else { x=_x; y=_y; } } }; void main() { int x,y; cout<<"x="; cin>>x; cout<<"y="; cin>>y; try { Point p(x,y); } catch(InputException& e) { cerr<<e.what(); } }
4. 異常使用的成本
在沒有異常被拋出的情況下,使用try{ }語句塊,整體代碼大約膨脹了5%~10%,執行的速度也大約下降這個數。和正常函數返回相比,拋出異常導致的函數返回,其速度可能比正常情況慢三個數量級,所以在程序中使用異常處理有利有弊。
四、 Java異常處理
1. try…catch…finally的使用
Java的異常處理與C++類似,try…catch子句與C++中的try…catch很相似,finally{ }表示無論是否出現異常,最終必須執行的語句塊。
實例如下:
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; class Myclass { publicstaticvoid main(String[]args) { InputStreamReaderisr=new InputStreamReader(System.in); BufferedReader inputReader=new BufferedReader(isr); String line = null; try { line=inputReader.readLine(); } catch(IOException e) { e.printStackTrace(); } finally { System.out.print(line); } } }
2. throw和throws的使用
這里的throw和C++中的throw是一樣的,用于拋出異常,但Java的throw用在方法體內部,throws用在方法定義處,如下例:
void func()throws IOException
{
thrownew IOException();
}
3. Java異常類圖
java.lang.Object
---java.lang.Throwable
---java.lang.Exception
---java.lang.RuntimeException java.lang.Errorjava.lang.ThreadDeath
4. 異常處理的分類
(1)可檢測異常
此類異常屬于編譯器強制捕獲類,一旦拋出,那么拋出異常的方法必須使用catch捕獲,不然編譯器就會報錯。如sqlException,它是一個可檢測異常,當程序員連接到JDBC,不捕捉到這個異常,編譯器就會報錯。
(2)非檢測異常
當產生此類異常時,編譯器也能編譯通過,但要靠程序員自己去捕獲。如數組越界或除0異常等。Error類和RuntimeException類都屬于非檢測異常。