c語言接口與實現--異常與斷言(異常部分)的理解,含實例

1、 這部分對於初學者(包括我)來說是不太好理解的,我斷斷續續的看了幾天時間才基本把“異常部分”看懂,把我個人的理解寫下來,一是記錄,二是希望能幫助到有同樣困惑的人。
2、個人覺得人郵出版社2011年9月第1版在本章節中存在翻譯錯誤,指出來大家一起看看,也可能是我錯了。
3、本章節異常的處理機制是基於setjmp 和longjmp實現的,所以大家需要對setjmp使用有了解,如果不清楚請書中的例子except_alloc.c,註釋部分如果打開,則模擬allocate失敗,調用跳轉命令

#include <setjmp.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
int Allocation_handled = 0;
jmp_buf Allocate_Failed;

void * allocate(unsigned n)
{
    void * new = malloc(n);
    //new = NULL;
    if(new) 
        return new;
    if(Allocation_handled)
        longjmp(Allocate_Failed, 1);
    assert(0);
}

int main(void)
{
    char *buff;
    Allocation_handled = 1;
    if(setjmp(Allocate_Failed))
    {
        fprintf(stderr, "couldn't allocate the buffer\n");
        exit(EXIT_FAILURE);
    }
    buff = (char *)allocate(4096);
    Allocation_handled = 0;
}

我們看setjmp函數的定義,函數原型如下

#include <setjmp.h>
int setjmp(jmp_buf env);

4、參數 env 即爲保存上下文的 jmp_buf 結構體變量,如果直接調用該函數,返回值爲 0; 若該函數從 longjmp 調用返回,返回值爲非零,由 longjmp 函數提供。根據函數的返回值,我們就可以知道 setjmp 函數調用是第一次直接調用,還是由其它地方跳轉過來的。

5、例子,if(setjmp(Allocate_Failed))執行後返回結果爲0,跳過if的代碼執行allocate,代碼中爲了模擬allocate失敗,使用new=NULL,程序執行到longjmp處,根據env=Allocate_Failed,跳到main函數的if(setjmp(Allocate_Failed))處執行,此時setjmp的返回值爲longjmp(Allocate_Failed, 1)指定的1,所以if裏面的語句執行,打印錯誤信息,exit退出函數。

6、接下來分析核心的except.h和except.c,實際c爲了設計成和c++類似(還有java)的try catch結構,使用了宏定義將try catch等c語言中沒有的關鍵字用宏定義的方式實現。
except.h

   #ifndef EXCEPT_INCLUDED
#define EXCEPT_INCLUDED
#include <setjmp.h>

#define T Except_T
// 具體的錯誤原因或者錯誤標誌,用於捕獲錯誤時進行比對,const char* 字符串
// 書中給出例子 Except_T Allocate_Failed = {"Allocation failed"};
typedef struct T
{
    const char * reason;
}T;

// 此處的設計有點怪,其實如果換一種常用方式大家估計更好理解
/*
typedef struct Except_Frame{
    struct Except_Frame *prev;
    jmp_buf env;
    const char *file;
    int line;
    const T *exception;
}Except_Frame;
這樣是否更符合大家的使用習慣?
*/
typedef struct Except_Frame Except_Frame;
struct Except_Frame{
    struct Except_Frame *prev;
    jmp_buf env;
    const char *file;
    int line;
    const T *exception;
};
/*
  Except_Frame結構體包含 *prev,一個逆向的單向鏈表,通過鏈表的結尾單元來添加和刪除;
  書中將其視爲棧是更加科學的描述,只需記住棧頂單元
  jmp_buf env是上下文環境變量,即longjmp跳轉的尋址目標;
  const char *file 出錯的文件,int line出錯的行,const T*exception出錯的具體信息,
  後面三個變量是根據需要來設計的,你也可以有自己的變量設計,
  比如 exception可以設計爲int型的錯誤id
*/

/*
  用枚舉類型來定義程序執行中的錯誤處理(跳轉)標誌的幾種狀態,
  Except_entered 必須爲0,它等於第一次調用setjmp的返回值
  Except_raised 表示錯誤產生,即執行過程中出現了錯誤
  Except_handled 表示捕獲的錯誤已處理
  Except_finalized 表示異常處理結束
*/
enum 
{
    Except_entered = 0,
    Except_raised,
    Except_handled,
    Except_finalized
};
// 外部定義的棧頂結構體,具體在except.c中定義
extern Except_Frame * Except_stack;

// 錯誤捕獲函數,包含longjmp的調用,程序執行過程中出現異常,需要調用此函數來觸發異常
void Except_raise(const T *e , const char *file, int line);

// 捕獲異常的封裝宏定義,加入了__FILE__和__LINE__,allocate中可以使用此函數替代
#define RAISE(e)  Except_raise( &(e), __FILE__, __LINE__)

// 如果RAISE捕獲的異常未被處理,則執行RERAISE再次處理,直到異常棧Except_stack的棧頂爲空NULL
// 宏定義中明確使用了Except_frame變量,所以必須與下面的#define TRY等一起使用,
// 否則程序報錯:Except_frame undefined
#define RERAISE   Except_raise( Except_frame.exception, \
        Except_frame.file, Except_frame.line)

// 通過宏定義的方式實現TRY EXCEPT(x) ELSE FINALLY END_TRY的自定義關鍵字
// 後面先講一下整體的結構,然後再對每個宏定義的進行說明,書中也是這樣進行的,如果我講的不細的地方請對照書中的內容來看
#define TRY do { \
    volatile int Except_flag;  \
    struct Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;   \
    Except_flag = setjmp(Except_frame.env);  \
    if (Except_flag == Except_entered)  {

#define EXCEPT(e) \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    }else if (Except_frame.exception == &(e)) { \
        Except_flag = Except_handled;

#define ELSE \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    }else{ \
        Except_flag = Except_handled;

#define FINALLY   \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    }   \
    { \
       if (Except_flag == Except_entered) \
        Except_flag = Except_finalized;

#define END_TRY  \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } if (Except_flag == Except_raised) RERAISE; \
}while(0)

#define RETURN switch ( Except_stack = Except_stack-prev, 0) default: return


#undef T
#endif

下面對except.h的宏做重點介紹
7、TRY可以和後面幾個任意組合,TRY必須要且是打頭,END_TRY必須要且在結尾 書中列了幾種結構,如TRY-EXCEPT TRY-FINALLY,do {}while(0) 是宏定義的常見用法,具體可以百度瞭解TRY…END-TRY核心結構是c語言的條件語句,判斷準則是Except_flag的值

爲了TRY EXCEPT(e) ELSE FINALLY 和END_TRY能夠組合,這些定義總是將if的兩個{}分開,簡單一點說更好理解

    *******************
     TRY = do{xxxx
                if (xxx)
               {
                   xxx
    *******************
     EXCEPT(e) = }else if (xxx)
                 { 
                    xxx
    ********************
     ELSE = }else 
            {
               xxx
    ********************            
    FINALLY = } {
                xxx
    ********************
    END_TRY = } xxx 
            }while(0)
    ********************

這樣子TRY打頭 END_TRY結尾,中間無論插入EXCEPT(e) ELSE 或者FINALLY都可以是的if的{}閉環, 形成了if ..else if …else…的結構(else if 允許多個),其中FINALLY由於是必執行項,所以其代碼程序只是用{}做了包含。

TRY的定義

#define TRY do { \
    volatile int Except_flag;  \         
    struct Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;   \
    Except_flag = setjmp(Except_frame.env);  \................1
    if (Except_flag == Except_entered)  {

Except_flag:定義異常狀態標誌 取值範圍爲之前enum枚舉類型定義的值,此處定義爲volatile 自動變量,告訴優化器不優化,此變量會被頻繁修改,每次都要重新獲取值,不能用優化過後的值。
Except_frame: 創建新的異常結構體變量
Except_frame.prev = Except_stack;
Except_stack = &Except_frame; : 這兩行結合起來將新建的異常結構體變量設置爲棧頂
Except_flag = setjmp(Except_frame.env); :設置跳轉點,將返回的結果賦值給Except_flag,(具體上下文的環境屬性保存在Except_frame.env中)
if (Except_flag == Except_entered) { : 第一個if分支,第一次執行setjmp時返回的是0,因此條件成立,進入執行用戶程序,書中指代爲S; S執行如果無異常則需要跳轉到FINALLY(如果有)或者END_TRY, 如果S執行有異常需要調用RAISE(e)跳轉回setjmp,根據返回值和異常信息判斷進行不同的else 分支。

EXCEPT(e)的定義

#define EXCEPT(e) \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    }else if (Except_frame.exception == &(e)) { \
        Except_flag = Except_handled;

if (Except_flag == Except_entered) Except_stack = Except_stack->prev;:如果類型是TRY-EXCEPT,那麼TRY S執行後無異常,則執行此行代碼 無錯誤產生,將此前壓棧的異常結構體變量彈出丟棄;如果有異常,那麼程序從S調回到TRY中的1處,在S中longjmp會將setjmp的返回值設置爲Except_raised

}else if(Except_frame.exception == &(e)) :若Except_flag爲Except_raised,判斷Except_frame.exception與給定的e錯誤信息是否一致,不一致程序跳過進入下一個判斷,可能是EXCEPT(e) 或者ELSE 或FINALLY 或 END_TRY

Except_flag = Except_handled; :如果上面的判斷錯誤一致,則進入到“{”裏面執行,將Except_flag 設置爲Except_handled

ELSE定義

#define ELSE \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    }else{ \
        Except_flag = Except_handled;

if (Except_flag == Except_entered) Except_stack = Except_stack->prev; : 如果是TRY-ELSE結構,TRY S ELSE S1則此處和EXCEPT(e)作用一致,如果TRY S的 S執行無錯誤,則執行此行代碼,將棧頂異常彈出丟棄,否則將執行else 後的代碼,先將

Except_flag = Except_handled;:Except_flag設置爲Except_handled狀態,表示異常已捕獲且處理。執行S1程序

FINALLY定義

#define FINALLY   \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    }   \
    { \
       if (Except_flag == Except_entered) \
        Except_flag = Except_finalized;

Except_stack = Except_stack->prev; : 執行到此處證明上面如果未產生異常,則異常棧頂彈出並丟棄
if (Except_flag == Except_entered) Except_flag = Except_finalized; :作用和前面一樣; TRY S FINALLY S1

對於FINALLY的解釋,此處人郵出版社 2011年第一版的翻譯,個人覺得容易誤解,在P37頁, 翻譯後的截圖如下
這裏寫圖片描述

英文原版如下
這裏寫圖片描述

抱歉,原來以爲是翻譯錯了,後來對比後發現是我理解錯了,“在S1執行後,導致S1**執行的異常**將被再次引發(re-raised)”,少看了個“的”字,誤認爲是導致S1的執行異常,所以原來覺得奇怪S1有啥異常需要處理。不過人郵的這個版本在本章另外一個地方確實翻譯的會讓人誤解,後面會指出來。

END_TRY

#define END_TRY  \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } if (Except_flag == Except_raised) RERAISE; \
}while(0)

if (Except_flag == Except_entered) Except_stack = Except_stack->prev; : 不解釋和前面一樣的作用
if (Except_flag == Except_raised) RERAISE; : 如果條件成立,證明之前的異常沒有被處理,此處通過RERAISE再次引發,如果處理不了,最後會在RERAISE中異常退出,本書中的例子使用的是abort函數退出.

RETURN定義

#define RETURN switch ( Except_stack = Except_stack-prev, 0) default: return

咋一看switch()裏面怎麼有”,”,還有兩個“參數”,實際這個是“,”號運算符,優先級最低,從左往右執行,先執行出棧操作,執行0的switch判斷,書中指出由於TRY-ELSE-END_TRY等結構是宏定義,如果直接使用return編譯會報錯,所以需要進行封裝
可以用個小程序來驗證一下這種使用方法

#include <stdio.h>

int main(void)
{
    int a = 0;

    switch (a=1,0)
    {
    case 0:
        printf("a=%d, 0\n", a); 
        break;
    default:printf("a=%d, OK\n", a);
        return;
    }

    switch (a=2,0)
    default:
        printf("a=%d, OK\n", a);
        return;
    return 0;
}

對RETURN的解釋,人郵版的翻譯是會讓人產生誤解,截圖如下
這裏寫圖片描述
機械工業出版的截圖如下
這裏寫圖片描述
英文原版如圖
這裏寫圖片描述

如果按照人郵版的翻譯,會誤解爲RETURN必須包含在TRY中,而實際英文原版和機械工業出版的都只是說RETURN是return的替代,而且很明顯將後面的a return翻譯成返回是不合適的,機械出版的不翻譯直接用return顯得更專業。

以上是except.h的分析和理解

下面是except.c

//#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include "except.h"

#define T Except_T

Except_Frame *Except_stack = NULL;

void Except_raise(const T *e, const char *file, int line)
{
    //Except_Frame *p = malloc(sizeof(Except_Frame));
    Except_Frame *p = Except_stack; // 設置p爲異常的棧頂
    assert(e);

    if(p == NULL)  // 如果p爲NULL,則直接退出,此時應該是異常被re-raised,且無相應的解決方案,所以系統選擇退出
    {
        fprintf(stderr, "Uncaught exception");
        if(e->reason)
        {
            fprintf(stderr, " %s", e->reason);
        }
        else
        {
            fprintf(stder, " at 0x%p", e); 
        }

        if (file && line > 0)
        {
            fprintf(stderr, " raised at %s:%d\n", file, line);
        }
        fprintf(stderr, "aborting...\n");
        fflush(stderr);
        abort();
    }
    p->exception = e; // 對異常進行賦值操作
    p->file = file;
    p->line = line;
    Except_stack = Except_stack->prev; // 與TRY配合使用,如果出現異常需要處理,彈出棧頂
    longjmp(p->env, Except_raised);// 跳轉指令,同時設置setjmp返回的值爲Except_raised
}

可以看到主要是函數Except_raise的實現
首先創建一個異常堆棧,初始化棧頂爲NULL
Except_raise函數中註釋掉的是網上其他人的代碼“Except_Frame *p = malloc(sizeof(Except_Frame));”,個人認爲這個應該是多餘的,而且書中也沒有這項。
下面寫一個簡單的main進行測試
except_main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "except.h"

struct Except_T allocate_failed_reason1 = {"para error"};
struct Except_T allocate_failed_reason2  = {"memory is not enough"};

void * allocate(int n)
{
    void * new = malloc(n);

    new = NULL;

    if(new)
    {
        return new;
    }else if (n <= 0)
    {
        RAISE(allocate_failed_reason1);
    }else 
    {
        RAISE(allocate_failed_reason2);
    }
}

int main(int argc, char ** argv)
{
   char * buff;

   int n;

   if(argc == 2)
   {
       n = atoi(argv[1]);
   }

   TRY
   {
      allocate(n);
      buff="allocate OK!";
   }
   EXCEPT(allocate_failed_reason1)
   {
       buff = (char *)allocate_failed_reason1.reason;
   }ELSE
   {
       buff = (char *)allocate_failed_reason2.reason;
   }
   FINALLY
   {
       printf("buff:%s\n", buff);
   }
   END_TRY;

   return 0;
}

捕獲兩種錯誤“para error”和“memory is not enough”,仍然是以書中的allocate爲例,如果將代碼中斷new = NULL註釋掉且輸入的參數n是對的話是沒有錯誤的,程序正常執行;此處模擬異常所以需要new=NULL
編譯並執行
gcc except_main.c except.c -o except_main
./except_main 0
結果爲
顯示para error

./except_main 100
結果爲
顯示 memory is not enough

程序執行了TRY-EXCEPT(e)-ELSE-FINALLY,你也可以創建一個新的Except_T的異常,不捕獲,然後執行END_TRY中的RERAISE。書中的這種設計支持嵌套,即TRY-XXX-END_TRY中包含TRY結構,也可以嘗試驗證一下。

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