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