一 前言:
異常處理,對於做面向對象開發的開發者來說是再熟悉不過了,例如在C#中有
try
{
...
}
catch( Exception e){...}
finally{
.....
}
在C++中,我們常常會使用
try{}
...
catch(){}
塊來進行異常處理。
說了那麼多,那麼到底什麼是異常處理呢?
異常處理(又稱爲錯誤處理)功能提供了處理程序運行時出現的任何意外或異常情況的方法。
異常處理一般有兩種模型,一種是"終止模型",一種是"恢復模型"
"終止模型":在這種模型中,將假設錯誤非常關鍵,將以致於程序無法返回到異常發生的地方繼續執行.一旦異常被拋出,就表明錯誤已無法挽回,也不能回來繼續執行.
"恢復模型":異常處理程序的工作是修正錯誤,然後重新嘗試調動出問題的方法,並認爲的二次能成功. 對於恢復模型,通常希望異常被處理之後能繼續執行程序.在這種情況下,拋出異常更像是對方法的調用--可以在Java裏用這種方法進行配置,以得到類似恢復的行爲.(也就是說,不是拋出異常,而是調用方法修正錯誤.)或者,把try塊放在while循環裏,這樣就可以不斷的進入try塊,直到得到滿意的結果.
二 面向對象中的異常處理
大致瞭解了什麼是異常處理後,由於異常處理在面嚮對象語言中使用的比較普遍,我們就先以C++爲例,做一個關於異常處理的簡單例子:
問題:求兩個數相除的結果。
這裏,隱藏這一個錯誤,那就是當除數爲0時,會出現,所以,我們得使用異常處理來捕捉這個異常,並拋出異常信息。
具體看代碼:
#include <iostream>
#include <exception>
using namespace std;
class DivideError:public exception
{
public:
DivideError::DivideError():exception(){}
const char* what(){
return "試圖去除一個值爲0的數字";
}
};
double quotion(int numerator,int denominator)
{
if(0==denominator) //當除數爲0時,拋出異常
throw DivideError();
return static_cast<double>(numerator)/denominator;
}
int main()
{
int number1; //第一個數字
int number2; //第二個數字
double result;
cout<<"請輸入兩個數字:" ;
while(cin>>number1>>number2){
try{
result=quotion(number1,number2);
cout<<"結果是 :"<<result<<endl;
} //end try
catch(DivideError &divException){
cout<<"產生異常:"
<<divException.what()<<endl;
}
}
}
在這個例子中,我們使用了<expection>頭文件中的exception類,並使DivideError類繼承了它,同時重載了虛方法what(),以給出特定的異常信息。
而C#中的異常處理類則封裝的更有全面,裏面封裝了常用的異常處理信息,這裏就不多說了。
三 C語言中的異常處理
在C語言中異常處理一般有這麼幾種方式:
1.使用標準C庫提供了abort()和exit()兩個函數,它們可以強行終止程序的運行,其聲明處於<stdlib.h>頭文件中。
2.使用assert(斷言)宏調用,位於頭文件<assert.h>中,當程序出錯時,就會引發一個abort()。
3.使用errno全局變量,由C運行時庫函數提供,位於頭文件<errno.h>中。
4.使用goto語句,當出錯時跳轉。
5.使用setjmp,longjmp進行異常處理。
接下來,我們就依次對這幾種方式來看看到底是怎麼做的:
我們仍舊以前面處理除數爲0的異常爲例子。
1.使用exit()函數進行異常終止:
#include <stdio.h>
#include <stdlib.h>
double diva(double num1,double num2) //兩數相除函數
{
double re;
re=num1/num2;
return re;
}
int main()
{
double a,b,result;
printf("請輸入第一個數字:");
scanf("%lf",&a);
printf("請輸入第二個數字:");
scanf("%lf",&b);
if(0==b) //如果除數爲0終止程序
exit(EXIT_FAILURE);
result=diva(a,b);
printf("相除的結果是: %.2lf\n",result);
return 0;
}
其中exit的定義如下:
_CRTIMP void __cdecl __MINGW_NOTHROW exit (int) __MINGW_ATTRIB_NORETURN;
exit的函數原型:void exit(int)由此,我們也可以知道EXIT_FAILURE宏應該是一個整數,exit()函數的傳遞參數是兩個宏,一個是剛纔看到的EXIT_FAILURE,還有一個是EXIT_SUCCESS從字面就可以看出一個是出錯後強制終止程序,而一個是程序正常結束。他們的定義是:
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
到此,當出現異常的時候,程序是終止了,但是我們並沒有捕獲到異常信息,要捕獲異常信息,我們可以使用註冊終止函數atexit(),它的原型是這樣的:intatexit(atexit_t func);
具體看如下程序:
#include <stdio.h>
#include <stdlib.h>
void Exception(void) //註冊終止函數,通過掛接到此函數,捕獲異常信息
{
printf("試圖去除以一個爲0的數字,出現異常!\n");
}
int main()
{
double a,b,result;
printf("請輸入第一個數字:");
scanf("%lf",&a);
printf("請輸入第二個數字:");
scanf("%lf",&b);
if(0==b) //如果除數爲0終止程序 ,並掛接到模擬異常捕獲的註冊函數
{
atexit(Exception);
exit(EXIT_FAILURE);
}
result=diva(a,b);
printf("相除的結果是: %.2lf\n",result);
return 0;
}
這裏需要注意的是,atexit()函數總是被執行的,就算沒有exit()函數,當程序結束時也會被執行。並且,可以掛接多個註冊函數,按照堆棧結構進行執行。abort()函數與exit()函數類似,當出錯時,能使得程序正常退出,這裏就不多說了。
2.使用assert()進行異常處理:
assert()是一個調試程序時經常使用的宏,切記,它不是一個函數,在程序運行時它計算括號內的表達式,如果表達式爲FALSE (0), 程序將報告錯誤,並終止執行。如果表達式不爲0,則繼續執行後面的語句。這個宏通常原來判斷程序中是否出現了明顯非法的數據,如果出現了終止程序以免導致嚴重後果,同時也便於查找錯誤。
另外需要注意的是:assert只有在Debug版本中才有效,如果編譯爲Release版本則被忽略。
我們就前面的問題,使用assert斷言進行異常終止操作:構造可能出現出錯的斷言表達式:assert(number!=0)這樣,當除數爲0的時候,表達式就爲false,程序報告錯誤,並終止執行。
代碼如下:
#include <stdio.h>
#include <assert.h>
double diva(double num1,double num2) //兩數相除函數
{
double re;
re=num1/num2;
return re;
}
int main()
{
printf("請輸入第一個數字:");
scanf("%lf",&a);
printf("請輸入第二個數字:");
scanf("%lf",&b);
assert(0!=b); //構造斷言表達式,捕獲預期異常錯誤
result=diva(a,b);
printf("相除的結果是: %.2lf\n",result);
return 0;
}
3.使用errno全局變量,進行異常處理:
errno全局變量主要在調式中,當系統API函數發生異常的時候,將errno變量賦予一個整數值,根據查看這個值來推測出錯的原因。
其中的各個整數值都有一個相應的宏定義,表示不同的異常原因:
代碼
#define EPERM 1 /* Operation not permitted */
#define ENOFILE 2 /* No such file or directory */
#define ENOENT 2
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted function call */
#define EIO 5 /* Input/output error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Arg list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file descriptor */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Resource temporarily unavailable */
#define ENOMEM 12 /* Not enough space */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
/* 15 - Unknown Error */
#define EBUSY 16 /* strerror reports "Resource device" */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Improper link (cross-device link?) */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* Too many open files in system */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Inappropriate I/O control operation */
/* 26 - Unknown Error */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Invalid seek (seek on a pipe?) */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Domain error (math functions) */
#define ERANGE 34 /* Result too large (possibly too small) */
/* 35 - Unknown Error */
#define EDEADLOCK 36 /* Resource deadlock avoided (non-Cyg) */
#define EDEADLK 36
/* 37 - Unknown Error */
#define ENAMETOOLONG 38 /* Filename too long (91 in Cyg?) */
#define ENOLCK 39 /* No locks available (46 in Cyg?) */
#define ENOSYS 40 /* Function not implemented (88 in Cyg?) */
#define ENOTEMPTY 41 /* Directory not empty (90 in Cyg?) */
#define EILSEQ 42 /* Illegal byte sequence */
這裏我們就不以前面的除數爲0的例子來進行異常處理了,因爲我不知道如何定義自己特定錯誤的errno,如果哪位知道,希望能給出方法。我以一個網上的例子來說明它的使用方法:
代碼
#include <errno.h>
#include <math.h>
#include <stdio.h>
int main(void)
{
errno = 0;
if (NULL == fopen("d:\\1.txt", "rb"))
{
printf("%d", errno);
}
else
{
printf("%d", errno);
}
return 0; }
這裏試圖打開一個d盤的文件,如果文件不存在,這是查看errno的值,結果是2、
當文件存在時,errno的值爲初始值0。然後查看值爲2的錯誤信息,在宏定義那邊#define ENOFILE 2 /* No such file or directory */
便知道錯誤的原因了。
4.使用goto語句進行異常處理:
goto語句相信大家都很熟悉,是一個跳轉語句,我們還是以除數爲0的例子,來構造一個異常處理的例子:
代碼
#include <stdio.h>
double diva(double num1,double num2) //兩數相除函數
{
double re;
re=num1/num2;
return re;
}
int main()
{
int tag=0;
double a,b,result;
if(1==tag)
{
Throw:
printf("除數爲0,出現異常\n");
}
tag=1;
printf("請輸入第一個數字:");
scanf("%lf",&a);
printf("請輸入第二個數字:");
scanf("%lf",&b);
if(b==0) //捕獲異常(或許這麼說並不恰當,暫且這麼理解)
goto Throw; //拋出異常
result=diva(a,b);
printf("%d\n",errno);
printf("相除的結果是: %.2lf\n",result);
return 0;
}
5.使用setjmp和longjmp進行異常捕獲與處理:
setjmp和longjmp是非局部跳轉,類似goto跳轉作用,但是goto語句具有侷限性,只能在局部進行跳轉,當需要跳轉到非一個函數內的地方時就需要用到setjmp和longjmp。setjmp函數用於保存程序的運行時的堆棧環境,接下來的其它地方,你可以通過調用longjmp函數來恢復先前被保存的程序堆棧環境。異常處理基本方法:
使用setjmp設置一個跳轉點,然後在程序其他地方調用longjmp跳轉到該點(拋出異常).
代碼如下所示:
#include <stdio.h>
#include <setjmp.h>
jmp_buf j;
void Exception(void)
{
longjmp(j,1);
}
double diva(double num1,double num2) //兩數相除函數
{
double re;
re=num1/num2;
return re;
}
int main()
{
double a,b,result;
printf("請輸入第一個數字:");
scanf("%lf",&a);
printf("請輸入第二個數字:");
if(setjmp(j)==0)
{
scanf("%lf",&b);
if(0==b)
Exception();
result=diva(a,b);
printf("相除的結果是: %.2lf\n",result);
}
else
printf("試圖除以一個爲0的數字\n");
return 0;
}
四 總結:
除了以上幾種方法之外,另外還有使用信號量等等方法進行異常處理。當然在實際開發中每個人都有各種調式的技巧,而且這文章並不是說明異常處理一定要這樣做,這只是對一般做法的一些總結,也不要亂使用異常處理,如果弄的不好就嚴重影響了程序的效率和結構,就像設計模式一樣,不能胡亂使用。