第十三章 異常處理
本章內容包括:
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)幾個問題
l try語句塊裏不要放不可能出現異常的代碼。
l C++沒有垃圾自動回收,所以很可能會出現處理完異常後應釋放的對象沒有釋放(因爲delete沒有執行等原因),可以在catch(…)塊中(處理所有異常的語句塊)把釋放資源的代碼再寫一遍。
l 當前catch塊異常如沒處理完,可以在再拋出,交給其它同類型catch塊繼續處理,比如:
catch(ex){
…….
throw; //只需寫throw即可,只能寫在catch塊內
…….
}
l 如果異常出現而沒有合適的catch匹配它,系統會調用標準庫中的terminate()函數,使程序終止。
l 通常定義自己的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函數
l 瞭解C++/JAVA異常處理的底層原理;
l 與操作系統課程有關進程的某些知識建立關聯;
l 知道有一種很特別的函數:它可以返回兩次。
使用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函數更接近底層一些。
本章課後習題:無