可嵌套的C語言異常處理機制



原文: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 <malloc.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]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章