程序中通常會出現三種錯誤:用戶錯誤、運行期錯誤以及異常
標准庫函數setjmp和longjmp
在C語言中,標准庫函數setjmp和longjmp形成了結構化異常工具的基礎。簡單的說就是setjmp實例化處理程序,而longjmp產生異常
setjmp和longjmp是C語言所獨有的,它們部分彌補了C語言有限的轉移能力。與刺激的abort()和exit()相比,goto語句看起來是處理異常的更可行方案。不幸的是,goto是本地的:它只能跳到所在函數內部的標號上,而不能將控制權轉移到所在程序的任意地點(當然,除非你的所有代碼都在main體中)。
為了解決這個限制,C函數庫提供了setjmp()和longjmp()函數,它們分別承擔非局部標號和goto作用。頭文件<setjmp.h>申明了這些函數及同時所需的jmp_buf數據類型。
函數說明:
int setjmp(jmp_buf env)
建立本地的jmp_buf緩沖區並且初始化,用於將來跳轉回此處。這個子程序保存程序的調用環境於env參數所指的緩沖區,env將被longjmp使用。如果是從setjmp直接調用返回,setjmp返回值為0。如果是從longjmp恢復的程序調用環境返回,setjmp返回非零值。
void longjmp(jmp_buf env, int value)
恢復env所指的緩沖區中的程序調用環境上下文,env所指緩沖區的內容是由setjmp子程序調用所保存。value的值從longjmp傳遞給setjmp。longjmp完成後,程序從對應的setjmp調用處繼續執行,如同setjmp調用剛剛完成。如果value傳遞給longjmp零值,setjmp的返回值為1;否則,setjmp的返回值為value。
成員類型:
jmp_buf 數組類型,例如:struct int[16]或struct __jmp_buf_tag,用於保存恢復調用環境所需的信息。
jmp_buf 的定義:
typedef struct _jmp_buf
{
int _jp[_JBLEN+1];
} jmp_buf[1];
這個是 setjmp.h 裡的一行定義,把一個 struct 定義成一個數組。這樣,在聲明 jmp_buf 的時候,可以把數據分配到堆棧上。但是作為參數傳遞的時候則作為一個指針。
原理非常簡單:
1.setjmp(j)設置“jump”點,用正確的程序上下文填充jmp_buf對象j。這個上下文包括程序存放位置、棧和框架指針,其它重要的寄存器和內存數據。當初始化完jump的上下文,setjmp()返回0值。
2. 以後調用longjmp(j,r)的效果就是一個非局部的goto或“長跳轉”到由j描述的上下文處(也就是到那原來設置j的setjmp()處)。當作為長跳轉的目標而被調用時,setjmp()返回r或1(如果r設為0的話)。(記住,setjmp()不能在這種情況時返回0。)
通過有兩類返回值,setjmp()讓你知道它正在被怎麼使用。當設置j時,setjmp()如你期望地執行;但當作為長跳轉的目標時,setjmp()就從外面“喚醒”它的上下文。你可以用longjmp()來終止異常,用setjmp()標記相應的異常處理程序。
本文地址:http://www.cnblogs.com/archimedes/p/c-exception-assert.html,轉載請注明源地址。
一個簡單的例子:
#include <stdio.h>
#include <setjmp.h>
static jmp_buf buf;
void second(void) {
printf("second\n"); // 打印
longjmp(buf,1); // 跳回setjmp的調用處 - 使得setjmp返回值為1
}
void first(void) {
second();
printf("first\n"); // 不可能執行到此行
}
int main() {
if ( ! setjmp(buf) ) {
first(); // 進入此行前,setjmp返回0
} else { // 當longjmp跳轉回,setjmp返回1,因此進入此行
printf("main\n"); // 打印
}
return 0;
}
運行結果:
second
main
在下例中,setjmp被用於包住一個例外處理,類似try。longjmp調用類似於throw語句,允許一個異常返回給setjmp一個異常值。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
void first(void);
void second(void);
static jmp_buf exception_env;
static int exception_type;
int main(void) {
void *volatile mem_buffer;
mem_buffer = NULL;
if (setjmp(exception_env)) {
/* 如果運行到這將產生一個異常*/
printf("first failed, exception type %d\n", exception_type);
} else {
printf("calling first\n");
first();
mem_buffer = malloc(300); /* 分配內存 */
printf("%s",strcpy((char*)mem_buffer, "first succeeded!")); /* ... 不會被執行 */
}
if (mem_buffer)
free((void*) mem_buffer); /* 小心釋放內存 */
return 0;
}
void first(void) {
jmp_buf my_env;
printf("calling second\n");
memcpy(my_env, exception_env, sizeof(jmp_buf));
switch (setjmp(exception_env)) {
case 3:
/* 如果運行到這,表示有異常 */
printf("second failed with type 3 exception; remapping to type 1.\n");
exception_type = 1;
default:
memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
longjmp(exception_env, exception_type); /* continue handling the exception */
case 0:
/* normal, desired operation */
second();
printf("second succeeded\n"); /* not reached */
}
memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
}
void second(void) {
printf("entering second\n" );
exception_type = 3;
longjmp(exception_env, exception_type);
printf("leaving second\n");
}
運行結果:
calling first
calling second
entering second
second failed with type 3 exception; remapping to type 1.
first failed, exception type 1
接口
Except接口在一系列宏指令和函數中包裝了setjmp和longjmp,它們一起提供了一個結構化異常處理工具
異常是Except_T類型的一個全局或靜態變量:
#ifndef EXCEPT_INCLUDED
#define EXCEPT_INCLUDED
#include <setjmp.h>
#define T Except_T
typedef struct T {
char *reason;
} T;
Except_T結構只有一個字段,它可以初始化為一個描述異常的字符串,當發生一個未處理的異常時,才把字符串打印出來
異常處理程序處理的是異常的地址。異常必須是全局的或靜態的變量,因此它們的地址唯一地標志了它們,異常e由宏指令引發或由函數引發:
#define RAISE(e) Except_raise(&(e), __FILE__, __LINE__)
void Except_raise(const T *e, const char *file,int line);
處理程序是由TRY-EXCEPT和TRY-FINALLY語句來實例化的,這兩個語句用宏指令實現,這兩個語句可以處理嵌套異常,也可以管理異常狀態的數據
TRY-EXCEPT的語法是:
TRY
S
EXCEPT(e1)
S1
EXCEPT(e2)
S2
……
EXCEPT(en)
Sn
ELSE
S0
END_TRY
看下面的代碼:
int Allocation_handle = 0;
jmp_buf Allocate_Failed;
void *allocate(unsigned n)
{
void *new = malloc(n);
if(new)
return new;
if(Allocation_handle)
longjmp(Allocate_Failed, 1);
assert(0);
}
char *buf;
Allocation_handle = 1;
if(setjmp(Allocate_Failed)) {
fprintf(stderr, "cound't allocate the buff\n");
exit(EXIT_FAILURE);
}
buf = allocate(4096);
Allocation_handle = 0;
上面的代碼沒有提供嵌套的處理程序,Allocation_handle標志的使用也很麻煩。
把Allocation_Failed變成一個異常,該異常是在malloc返回一個空指針時由allocate引發:
Except_T Allocate_Failed = {"Allocation failed"};
void *allocate(unsigned n)
{
void *new = malloc(n);
if(new)
return new;
RAISE(Allocation_Failed);
assert(0);
}
如果客戶調用程序代碼想處理這個異常,那麼它需要在TRY-EXCEPT語句內調用allocate:
extern Except_T Allocate_Failed;
char *buf;
TRY
buf = allocate(4096);
EXCEPT(Allocate_Failed)
fprintf(stderr, "could't allocate the buff\n");
exit(EXIT_FAILURE);
END_TRY;
TRY-EXCEPT語句是用setjmp和longjmp來實現的
TRY-FINALLY語句的語法是:
TRY
S
FINALLY
S1
END_TRY
如果S沒有產生任何異常,那麼執行S1,然後繼續執行END_TRY,如果S產生了異常,那麼S的執行被中斷,控制立即轉給S1。S1執行完後,引起S1執行的異常重新產生,使得它可以由前一個實例化的處理程序來處理。注意:S1是在兩種情況中都必須執行的,處理程序可以用RERAISE宏指令顯示地重新產生異常
#define RERAISE Except_raise(Except_frame.exception, \
Except_frame.file, Except_frame.line)
接口中的最後一個宏指令是:
#define RETURN switch (Except_stack = Except_stack->prev,0) default: return
RETURN宏指令用在TRY語句的內部,用來代替return語句
實現
Except接口中的宏指令和函數一起維護了一個記錄異常狀態以及實例化處理結構的堆棧。結構中的字段env就是setjmp和longjmp使用的某個jmp_buf,這個堆棧可以處理嵌套的異常
typedef struct Except_Frame Except_Frame;
struct Except_Frame {
Except_Frame *prev;
jmp_buf env;
const char *file;
int line;
const T *exception;
};
extern Except_Frame *Except_stack;
Except_stack指向異常棧頂的異常幀,每個幀的prev字段指向它的前一幀,產生一個異常就是將異常的地址存在exception字段中,並分別在file和line字段中保存異常的附屬信息--異常產生的文件以及行號
TRY從句將一個新的Except_Frame壓入異常棧,並調用setjmp,由RAISE和RERAISE調用Except_raise填充棧頂幀的字段exception、file和line,從異常棧中彈出棧頂Exception_Frame,然後調用longjmp,EXCEPT從句檢查該幀中的exception字段,決定應該用哪個處理程序。FINALLY從句執行清除代碼,並重新產生已彈出的異常幀中存儲的異常。
宏指令TRY、EXCEPT、ELSE、FINALLY_TRY一起將TRY-EXCEPT語句轉化成如下形式的語句:
do {
creat and push an Except_Frame
if(first return from setjmp) {
S
} else if (exception is e1) {
S1
……
} else if (exception is en) {
Sn
} else {
S0
}
if(an exception occurrend and wasn't handled)
RERAISE;
} while(0)
Exception_Frame的空間分配很簡單,在由TRY開始的do-while主體中的復合語句內部聲明一個該類型的局部變量即可:
#define TRY do { \
volatile int Except_flag; \
Except_Frame Except_frame; \
Except_frame.prev = Except_stack; \
Except_stack = &Except_frame; \
Except_flag = setjmp(Except_frame.env); \
if (Except_flag == Except_entered) {
在TRY語句內有四種狀態,由下面的枚舉標識符給出
enum { Except_entered=0, Except_raised,
Except_handled, Except_finalized };
setjmp的第一個返回值將Except_flag設置為Except_entered,表示進入TRY語句,並且將某個異常幀壓入異常棧,Except_entered必須為0,因為setjmp首次調用的返回值為0,隨後,setjmp的返回值將被設為Except_raised,表示發生了異常,處理程序將Except_flag的值設成Except_handled,表示處理程序已經對異常進行了處理。
#define TRY do { \
volatile int Except_flag; \
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)
最後實現代碼如下:
#include <stdlib.h>
#include <stdio.h>
#include "assert.h"
#include "except.h"
#define T Except_T
#ifdef WIN32
__declspec(thread)
#endif
Except_Frame *Except_stack = NULL;
void Except_raise(const T *e, const char *file,
int line) {
Except_Frame *p = Except_stack;
assert(e);
if (p == NULL) {
fprintf(stderr, "Uncaught exception");
if (e->reason)
fprintf(stderr, " %s", e->reason);
else
fprintf(stderr, " 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;
longjmp(p->env, Except_raised);
}