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類都屬于非檢測異常。