原文:http://blog.csdn.net/maozefa/article/details/1965755
C是一門古老的、面向過程的語言,相對於它的運行高效率,其開發效率是較低的,所以長期以來,C就主要被定位在系統軟件的開發上,特別是在現代各種可視化編程環境下,C的應用領域也越來越窄,雖然其原因有很多,但是,相對其它現代高級語言而言,其原始的異常處理功能不能不說是低效開發的主要原因之一,如果有一套較完善的異常功能,再配上一套好的常用功能庫,應該能提高其開發效率。
在現代語言中,異常機制包括兩個方面,即拋出異常和處理異常。在C語言程序中,異常類型一般都是靠函數的返回值和一些全局變量(如stdio.h的errno變量)來確定的,這大概算是“異常拋出”吧;處理異常最簡單的辦法就是終止程序,如使用exit、abort函數,雖然還有套較完備的異常處理函數setjmp和longjmp,但是,C的標準庫、一般的應用庫都沒有應用它們,所以我們也只能在自己的開發部分有限地運用一下,而且,很多不太精通C的人,還沒法把它們運用好。我在一些C語言書和網上看到了不少用setjmp和longjmp函數開發C異常機制的文章,有些構思還是很好的,但設計得很粗糙,沒能達到推廣應用的境界。
我也花了幾天功夫研究了一番,利用setjmp和longjmp函數,仿造Delphi的異常機制搞了幾個“宏”,雖還不那麼完美,但已經初具雛形,在我有限的測試中,和Delphi的異常機制完全相似,當然,這裏說的“相似”是我測試的較上層的內容,而Delphi的VCL從最低層的代碼就與異常緊密結合在一起,異常機制已經是VCL的不可缺少的重要組成部分,這一點C++也沒得比(當然內核用VCL的BCB又另當別論)。現將代碼發佈在這裏,望朋友們多提建議來完善它,即使不能達到應用的目的,也可通過這種研究,加深對異常機制的理解。
* *
* except_c.h *
* *
* 定義C語言使用的異常類型、函數和宏 *
* *
* 湖北省公安縣統計局:maozefa 2007.12 於遼寧大連 *
* *
*************************************************************************/
#ifndef __EXCEPT_C_H
#define __EXCEPT_C_H
#include <setjmp.h>
/* 定義全部異常類型,可按需要定義任何異常類型,供ON_EXCEPT宏使用 */
#define EXCEPT_ALL 0
/*************************************************************************
* *
* 定義異常宏: *
* *
* 1、Raise(type, msg):拋出type異常,msg爲異常信息 *
* 2、RaiseMessage(msg):拋出異常,相當於Raise(EXCEPT_ALL, msg) *
* 3、ReRaise():重新拋出以前的異常 *
* *
* 4、異常響應。對可能出現的異常進行處理(無異常時,處理代碼不執行): *
* *
* TRY *
* 正常代碼 *
* ON_EXCEPT(type) *
* 可選項。處理type異常的代碼,可在EXCEPT前連續使用 *
* EXCEPT *
* 可選項。所有異常處理代碼,相當於ON_EXCEPT(EXCEPT_ALL) *
* END_TRY *
* *
* 5、異常保護。無論是否出現異常,均執行的保護性質代碼,如資源釋放: *
* *
* TRY *
* 正常代碼 *
* FINALLY *
* 保護性質代碼 *
* END_TRY *
* *
* 6、套異常可嵌套使用,但不能混用,如: *
* *
* TRY *
* 代碼塊1 *
* TRY *
* 代碼塊2 *
* FINALLY *
* 保護性質代碼 *
* END_TRY *
* EXCEPT *
* 異常處理代碼 *
* END_TRY *
* *
*************************************************************************/
#define TRY except_Set();
if (!except_SetNum(setjmp(*except_Buf())))
{
#define Raise(type, msg) except_Raise(type, msg, __FILE__, __LINE__)
#define RaiseMessage(msg) Raise(EXCEPT_ALL, msg)
#define ReRaise() except_ReRaise()
#define ON_EXCEPT(type)
}
else if (except_On(type))
{
#define EXCEPT ON_EXCEPT(EXCEPT_ALL)
#define FINALLY }
{
#define END_TRY }
except_end();
/* 異常結構 */
typedef struct __Exception
{
int type; /* 異常類型 */
char *message; /* 消息 */
char *soufile; /* 源文件 */
int lineNum; /* 產生異常的行號 */
}Exception;
// 獲取當前異常消息
char* except_Message(void);
// 獲取當前異常結構
Exception *except_Exception(void);
// 以下函數爲內部使用
void except_Set(void);
void except_Raise(int type, const char *message, char *file, int line);
void except_ReRaise(void);
int except_On(int type);
void except_end(void);
jmp_buf* except_Buf(void);
int except_SetNum(int Num);
#endif /* __EXCEPT_C_H */
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "except_c.h"
#define MESSAGE_FORMAT "%s(%d): "
enum {evEnter, evRaise, evExcept = 2};
typedef struct __Exception_Event
{
struct __Exception_Event *prev;
jmp_buf eBuf;
int evNum;
Exception exception;
}Exception_Event;
static Exception_Event *except_ptr = NULL;
static char *except_msg;
static char except_msg_size = 0;
void except_Set(void)
{
Exception_Event *ev;
ev = (Exception_Event*)malloc(sizeof(Exception_Event));
ev->prev = except_ptr;
except_ptr = ev;
}
void except_Clear(void)
{
Exception_Event *ev;
if (except_ptr)
{
ev = except_ptr;
except_ptr = except_ptr->prev;
free(ev);
}
}
void except_Raise(int type, const char *message, char *file, int line)
{
int len = 0, size;
#ifndef NDEBUG
char buf[100];
sprintf(buf, MESSAGE_FORMAT, file, line);
len = strlen(buf);
#endif
size = strlen(message) + len + 1;
if (except_msg_size < size)
{
if (except_msg_size > 0)
free(except_msg);
except_msg_size = size;
except_msg = (char*)malloc(except_msg_size);
}
#ifndef NDEBUG
strcpy(except_msg, buf);
strcat(except_msg, message);
#else
strcpy(except_msg, message);
#endif
if (except_ptr)
{
except_ptr->exception.type = type;
except_ptr->exception.message = &except_msg[len];
except_ptr->exception.soufile = file;
except_ptr->exception.lineNum = line;
longjmp(except_ptr->eBuf, evRaise);
}
else
{
fprintf(stderr, except_msg);
abort();
}
}
void except_ReRaise(void)
{
Exception e;
if (except_ptr)
{
e = except_ptr->exception;
if (except_ptr->prev)
{
except_Clear();
except_ptr->exception = e;
longjmp(except_ptr->eBuf, evRaise);
}
else
{
fprintf(stderr, except_msg);
abort();
}
}
}
int except_On(int type)
{
if (except_ptr->evNum == evRaise &&
(type == EXCEPT_ALL || type == except_ptr->exception.type))
{
except_ptr->evNum = evExcept;
return 1;
}
return 0;
}
void except_end(void)
{
if (except_ptr->evNum == evRaise)
except_ReRaise();
except_Clear();
}
jmp_buf *except_Buf(void)
{
return &except_ptr->eBuf;
}
char *except_Message(void)
{
return except_msg;
}
Exception *except_Exception(void)
{
return &except_ptr->exception;
}
int except_SetNum(int Num)
{
except_ptr->evNum = Num;
return except_ptr->evNum;
}
有關異常的使用在except_c.h文件中已經說得很清楚了,不再闡述,後面再舉例說明。下面簡單說該異常機制的原理:
先說說不嵌套的情況,我們知道setjmp和longjmp必須配合使用,首先調用一次setjmp,用一個jmp_buf類型變量(假定a_buf)保存了當前現場,即計算機的各個寄存器狀態,此時setjmp返回值爲0,如果在程序代碼某些地方用同一變量a_buf調用longjmp函數,那麼,由a_buf保存的現場得以恢復,計算機將跳轉到設置現場變量a_buf的setjmp函數前,導致該函數再次被調用,並返回由longjmp傳遞的值(依靠該值,我們得以區分約定的異常類型或者出處);如果在abuf被設置後,又調用setjmp設置了一個b_buf現場,顯然,用該現場變量調用longjmp函數,只能恢復到b_buf設置的地方,這樣就形成了互不干擾的嵌套異常,假如內層異常機制出現異常,得到處理後,上層異常機制不能捕獲到錯誤,就跳出了這多層異常機制,相當於作了異常相應;如果內層異常機制出現異常,沒能夠得到適當處理,那麼,只需將異常信息傳遞到上一層異常機制(重新拋出異常),並清除內層異常標誌,如此嵌套循環,直到某個異常處理環節進行處理,或者作最後終止程序的處理。
在具體實現時,用一個向上的鏈表結構Exception_Event的靜態變量except_ptr組成異常嵌套的基礎,每次調用TRY宏,都重新設置鏈表尾,碰到END_TRY宏時,如果沒有異常發生,或者異常發生後作了處理,那麼,清除本層的Exception_Event數據,使鏈尾指向上一層,如果發生異常沒作處理,或者處理後重新拋出異常,那麼,END_TRY宏將異常信息向上層移交後,並清除本層數據,使鏈尾指向上一層。
上面說的是設置異常和處理異常的情況,再簡單說說拋出異常Raise宏,Raise宏調用了except_Raise函數,函數中,如果Exception_Event結構的靜態變量except_ptr不爲NULL,也就是設置了異常機制後,將異常信息寫到該變量中,否則,顯示錯誤信息後,調用abort終止程序,所以,在自己寫的庫函數中,可以無顧慮的使用該宏作異常拋出,即使調用庫函數的代碼沒用TRY,Raise也只相當於assert宏而已。
下面給一個演示例子:
#include <stdio.h>
#include <stdlib.h>
#include "except_c.h"
#pragma hdrstop
//---------------------------------------------------------------------------
#define EXCEPT_FILE_IO -1
void FileCopy(char *Source, char *Dest)
{
FILE *fo, *fi;
int ch;
char s[256];
printf("打開源文件... ");
if ((fi = fopen(Source, "rb")) == NULL)
Raise(EXCEPT_FILE_IO, "源文件未找到 ");
TRY
printf("建立目標文件... ");
if ((fo = fopen(Dest, "wb")) == NULL)
Raise(EXCEPT_FILE_IO, "建立目標文件失敗 ");
TRY
printf("開始拷貝文件... ");
// RaiseMessage("拷貝文件錯誤 ");
while (1)
{
ch = fgetc(fi);
if (ch == EOF && ferror(fi))
Raise(EXCEPT_FILE_IO, "讀文件錯誤 ");
if (feof(fi))
break;
if (fputc(ch, fo) == EOF && ferror(fo))
Raise(EXCEPT_FILE_IO, "寫文件錯誤 ");
}
FINALLY
printf("關閉目標文件... ");
fclose(fo);
END_TRY
FINALLY
printf("關閉源文件... ");
fclose(fi);
END_TRY
}
#pragma argsused
int main(int argc, char* argv[])
{
TRY
if (argc < 3)
{
printf("程序功能:拷貝文件格式:%s 源文件 目標文件 ", argv[0]);
RaiseMessage("程序參數錯誤! ");
}
FileCopy(argv[1], argv[2]);
ON_EXCEPT(EXCEPT_FILE_IO)
fprintf(stderr, "處理文件類型錯誤:% s", except_Message());
EXCEPT
fprintf(stderr, "處理全部錯誤:% s", except_Message());
END_TRY
system("pause");
return 0;
}
//---------------------------------------------------------------------------
下面,我們一步步來測試該例子(爲了顯示結果,FileCopy函數中每個步驟都作了顯示):
1、該控制檯例子程序要求帶參數運行,實現文件拷貝。在主函數中,有個TRY結構,首先檢查程序參數個數,如果小於3,拋出異常,該異常會被下面的EXCEPT宏捕獲處理,顯示信息爲:
處理全部錯誤:ExceptMain(55):程序參數錯誤!
2、給定正確的源文件和目標文件,上面的異常不存在了,主函數調用FileCopy函數,此時運行正常,結果爲:
3、修改源文件爲錯誤的路徑,FileCopy函數中產生異常,這個異常是在TRY之前拋出的,所以直接被主函數中異常結構中ON_EXCEPT(EXCEPT_FILE_IO)捕獲,運行結果爲:
4、改回正確的源文件路徑,把已經形成的目標文件改爲只讀屬性,這時產生異常如下顯示,表示目標文件不能建立,這時已經打開的源文件應該關閉,因此導致源文件關閉的FINALLY宏正確執行了,異常被主函數的TRY結構捕獲:
5、取消目標文件只讀屬性,將例子代碼中FileCopy函數中被註釋的語句RaiseMessage("拷貝文件錯誤/n");打開,以模擬產生拷貝過程錯誤,此時產生異常後,應同時關閉源文件和目標文件,結果,2個FINALLY都正確執行,異常被主函數的TRY結構捕獲:
至此,該例子全部測試完畢。該例子實際有3層TRY嵌套,FileCopy函數中是2層TRY...FINALLY異常結構,主函數則是TRY...EXCEPT結構,從測試結果看,完全達到了目的,用Delphi類似的例子運行結果完全一樣!
前面已經說了,該異常機制處理自己寫的代碼應該問題不大,但是對於標準庫的錯誤能捕獲嗎?我想,有些硬件異常應該是能捕獲的,如浮點數錯誤,下面用個例子演示:
#include <stdio.h>
#include <stdlib.h>
#include "except_c.h"
#include <signal.h>
#include <float.h>
#pragma hdrstop
//---------------------------------------------------------------------------
void FPE_Handler(int sig, int num)
{
char err[][32] =
{
"Interrupt on overflow ",
"Integer divide by zero ",
"",
"invalid operation ",
"",
"divide by zero ",
"arithmetic overflow ",
"arithmetic underflow ",
"precision loss ",
"stack overflow ",
};
_fpreset();
if (num >= FPE_INTOVFLOW && num <= FPE_STACKFAULT)
Raise(num, err[num - FPE_INTOVFLOW]);
else
Raise(num, "Other floating point error ");
}
#pragma argsused
int main(int argc, char* argv[])
{
float n = 0;
TRY
if (signal(SIGFPE, FPE_Handler) == SIG_ERR)
RaiseMessage("Couldn't set SIGFPE ");
n = 2 / n;
ON_EXCEPT(FPE_ZERODIVIDE)
fprintf(stderr, "處理浮點數被零除錯誤:% s", except_Message());
EXCEPT
fprintf(stderr, "處理全部錯誤:% s", except_Message());
END_TRY
system("pause");
return 0;
}
//---------------------------------------------------------------------------
該例子安裝了一個浮點數錯誤處理過程FPE_Handler,並在過程中使用了Raise拋出異常;主函數中,我們人爲的製造了一個浮點數被零除的的錯誤n = 2 / n(n = 0.0),FPE_Handler函數用Raise拋出了該異常,異常被主函數內TRY結構的ON_EXCEPT(FPE_ZERODIVIDE)捕獲並處理,顯示爲“divide by zero“錯誤;如果把float改爲int,則錯誤類型不再是FPE_ZERODIVIDE,所以,異常被EXCEPT捕獲,顯示爲“Integer divide by zero”錯誤;如果不用TRY機制,則程序會立即終止,從而失去處理機會。
本文的異常機制作爲一種探討和學習,無論從構思、設計和代碼都不可避免的存在問題,要實用還需要大家的建議,如異常結構類型能否靈活一點;異常拋出時,消息可否提供格式化;其他標準異常怎樣捕獲和規範定義等,都是需要解決的問題。
本文例子使用BCB2007編譯,如有錯誤和建議請來信:[email protected]