C語言異常處理機(jī)制——為您的C程序添加異常處理
1、什么是異常
異常一般指的是程序運(yùn)行期(Run-Time)發(fā)生的非正常情況。
異常一般是不可預(yù)測(cè)的,如:內(nèi)存不足、打開文件失敗、范圍溢出等。
UNIX 使用信號(hào)給出異常,并當(dāng)發(fā)生異常時(shí)轉(zhuǎn)跳到信號(hào)處理過程進(jìn)行異常處理。DOS下的信號(hào)對(duì)比UNIX系統(tǒng)而已相對(duì)較少。
C標(biāo)準(zhǔn)庫(kù)提供兩個(gè)特殊的函數(shù):setjmp() 及 longjmp(),這兩個(gè)函數(shù)是結(jié)構(gòu)化異常的基礎(chǔ),正是利用這兩個(gè)函數(shù)的特性來實(shí)現(xiàn)異常。
所以,異常的處理過程可以描述為這樣:
首先設(shè)置一個(gè)跳轉(zhuǎn)點(diǎn)(setjmp() 函數(shù)可以實(shí)現(xiàn)這一功能),然后在其后的代碼中任意地方調(diào)用 longjmp() 跳轉(zhuǎn)回這個(gè)跳轉(zhuǎn)點(diǎn)上,以此來實(shí)現(xiàn)當(dāng)發(fā)生異常時(shí),轉(zhuǎn)到處理異常的程序上,在其后的介紹中將介紹如何實(shí)現(xiàn)。
setjmp() 為跳轉(zhuǎn)返回保存現(xiàn)場(chǎng)并為異常提供處理程序,longjmp() 則進(jìn)行跳轉(zhuǎn)(拋出異常),setjmp() 與 longjmp() 可以在函數(shù)間進(jìn)行跳轉(zhuǎn),這就像一個(gè)全局的 goto 語句,可以跨函數(shù)跳轉(zhuǎn)。
舉個(gè)例子,程序在 main() 函數(shù)內(nèi)使用 setjmp() 設(shè)置跳轉(zhuǎn),并調(diào)用另一函數(shù)A,函數(shù)A內(nèi)調(diào)用B,B拋出異常(調(diào)用longjmp() 函數(shù)),則程序直接跳回到 main() 函數(shù)內(nèi)使用 setjmp() 的地方返回,并且返回一個(gè)值。
2、jmp_buf 異常結(jié)構(gòu)
使用 setjmp() 及 longjmp() 函數(shù)前,需要先認(rèn)識(shí)一下 jmp_buf 異常結(jié)構(gòu)。jmp_buf 將使用在 setjmp() 函數(shù)中,用于保存當(dāng)前程序現(xiàn)場(chǎng)(保存當(dāng)前需要用到的寄存器的值),jmp_buf 結(jié)構(gòu)在 setjmp.h 文件內(nèi)聲明:
typedef struct
{
unsigned j_sp; // 堆棧指針寄存器
unsigned j_ss; // 堆棧段
unsigned j_flag; // 標(biāo)志寄存器
unsigned j_cs; // 代碼段
unsigned j_ip; // 指令指針寄存器
unsigned j_bp; // 基址指針
unsigned j_di; // 目的指針
unsigned j_es; // 附加段
unsigned j_si; // 源變址
unsigned j_ds; // 數(shù)據(jù)段
} jmp_buf;
jmp_buf 結(jié)構(gòu)存放了程序當(dāng)前寄存器的值,以確保使用 longjmp() 后可以跳回到該執(zhí)行點(diǎn)上繼續(xù)執(zhí)行。
3、setjmp() 與 longjmp() 函數(shù)詳細(xì)說明
setjmp() 與 longjmp() 函數(shù)原型如下:
void _Cdecl longjmp(jmp_buf jmpb, int retval);
int _Cdecl setjmp(jmp_buf jmpb);
_Cdecl 聲明函數(shù)的參數(shù)使用標(biāo)準(zhǔn)C的進(jìn)棧方式(由右向左)壓棧。
setjmp() 與 longjmp() 函數(shù)都使用了 jmp_buf 結(jié)構(gòu)作為形參,它們的調(diào)用關(guān)系是這樣的:
首先調(diào)用 setjmp() 函數(shù)來初始化 jmp_buf 結(jié)構(gòu)變量 jmpb,將當(dāng)前CPU中的大部分影響到程序執(zhí)行的積存器存入 jmpb,為 longjmp() 函數(shù)提供跳轉(zhuǎn),setjmp() 函數(shù)是一個(gè)有趣的函數(shù),它能返回兩次,它應(yīng)該是所有庫(kù)函數(shù)中唯一一個(gè)能返回兩次的函數(shù),第一次是初始化時(shí),返回零,第二次遇到 longjmp() 函數(shù)調(diào)用后,longjmp() 函數(shù)使 setjmp() 函數(shù)發(fā)生第二次返回,返回值由 longjmp() 的第二個(gè)參數(shù)給出(整型,這時(shí)不應(yīng)該再返回零)。
在使用 setjmp() 初始化 jmpb 后,可以其后的程序中任意地方使用 longjmp() 函數(shù)跳轉(zhuǎn)會(huì) setjmp() 函數(shù)的位置,longjmp() 的第一個(gè)參數(shù)便是 setjmp() 初始化的 jmpb,若想跳轉(zhuǎn)回剛才設(shè)置的 setjmp() 處,則 longjmp() 函數(shù)的第一個(gè)參數(shù)是 setjmp() 所初始化的 jmpb 這個(gè)異常,這也說明一件事,即 jmpb 這個(gè)異常,一般需要定義為全局變量,否則,若是局部變量,當(dāng)跨函數(shù)調(diào)用時(shí)就幾乎無法使用(除非每次遇到函數(shù)調(diào)用都將 jmpb 以參數(shù)傳遞,然而明顯地,是不值得這樣做的);longjmp() 函數(shù)的第二個(gè)參數(shù)是傳給 setjmp() 的第二次返回值,這在介紹 setjmp() 函數(shù)時(shí)已經(jīng)介紹過。
下面是 setjmp() 函數(shù)與 longjmp() 函數(shù)的一個(gè)示意圖:
通過示意圖可以看出 setjmp() 函數(shù)與 longjmp() 函數(shù)的關(guān)系,如何跳轉(zhuǎn)。
4、異常處理過程
先來對(duì)比(參考)一下 C++ 的異常處理,C++ 在語言層上便添加了異常處理機(jī)制,使用 try 塊來包含那些可能出現(xiàn)錯(cuò)誤的代碼,你可以在 try 塊代碼中拋出異常,C++ 使用 throw 來拋出異常。拋出異常后,將轉(zhuǎn)到異常處理程序中執(zhí)行,C++ 使用 catch 塊來包含那些處理異常的代碼,catch 塊可以接收不同類型的異常。需要說明的是,throw 一般不在 try 塊內(nèi)的代碼中拋出異常,try 塊內(nèi)的代碼調(diào)用了別的函數(shù),如函數(shù)A,函數(shù)A 又調(diào)用了函數(shù) B,throw 可以在函數(shù)B中拋出異常,或者更深的函數(shù)調(diào)用層,無論如何,只要有異常拋出,程序?qū)⑥D(zhuǎn)到 catch 處執(zhí)行。
C中如何實(shí)現(xiàn),或者明確地說是模擬這一功能?
下面介紹的是一些簡(jiǎn)單的方法。
現(xiàn)在假設(shè) longjmp() 第二個(gè)值為1,即 setjmp() 第二次將返回1。我們使用一組簡(jiǎn)單的宏來替代 setjmp() 和 longjmp() 以便使用:
首先定義一個(gè)全局的異常:
jmp_buf Jump_Buffer;
因?yàn)?span lang=EN-US> setjmp() 第一次調(diào)用初始化后返回0,第二次返回非0,可以這樣定義一個(gè)宏使得它功能接近于 C++ 的 try。
#define try if(!setjmp(Jump_Buffer))
當(dāng) setjmp() 函數(shù)第一次0 時(shí),取非為真,則執(zhí)行 try 塊內(nèi)的代碼,如:
try
{
Test();
}
當(dāng)因?yàn)檎{(diào)用 longjmp() 拋出異常而導(dǎo)致 setjmp() 第二次返回時(shí)(程序?qū)?huì)轉(zhuǎn)到 setjmp() 函數(shù)處返回,這時(shí),這時(shí)應(yīng)該執(zhí)行的是異常處理代碼。longjmp() 使 setjmp() 函數(shù)返回非0,if(!setjmp(JumpBuffer)) 中將值取非則為假,是以,異常處理放在其后應(yīng)該使用一個(gè) else:
#define catch else
如此看起來便跟 C++ 相似了,setjmp() 函數(shù)的第二次返回導(dǎo)致 if() 中表達(dá)式值為假,剛好使 catch 塊得以執(zhí)行,如:
try
{
Test();
}
catch
{
puts("Error");
}
實(shí)現(xiàn)如 C++ 的 throw 語句,事實(shí)上以宏替換 longjmp(jmp_buf, int) 的調(diào)用:
#define throw longjmp(Jump_Buffer, 1)
下面的例程解釋如何使用這些宏:
/* 輸入一個(gè)整型數(shù),如果大于 100,則以異常拋出 */
#i nclude "stdio.h"
#i nclude "conio.h"
#i nclude "setjmp.h"
jmp_buf Jump_Buffer;
#define try if(!setjmp(Jump_Buffer))
#define catch else
#define throw longjmp(Jump_Buffer, 1)
int Test(int T);
int Test_T(int T);
int Test(int T)
{
if(T > 100)
throw;
else
puts("OK.");
return;
}
int Test_T(int T)
{
Test(T);
return;
}
int main(void)
{
int T;
try
{
puts("Input a :");
scanf("%d", &T);
T++;
Test_T(T);
}
catch
{
puts("Input Error!");
}
getch();
return 0;
}
當(dāng)遇到 throw 拋出異常,立即轉(zhuǎn)跳到 setjmp 處執(zhí)行,
屏棄了與之無相關(guān)的枝節(jié)(函數(shù)的返回及 throw 其后的代碼)
通過示意圖可以看出 setjmp() 函數(shù)與 longjmp() 函數(shù)的關(guān)系,如何跳轉(zhuǎn)。
4、異常處理過程
先來對(duì)比(參考)一下 C++ 的異常處理,C++ 在語言層上便添加了異常處理機(jī)制,使用 try 塊來包含那些可能出現(xiàn)錯(cuò)誤的代碼,你可以在 try 塊代碼中拋出異常,C++ 使用 throw 來拋出異常。拋出異常后,將轉(zhuǎn)到異常處理程序中執(zhí)行,C++ 使用 catch 塊來包含那些處理異常的代碼,catch 塊可以接收不同類型的異常。需要說明的是,throw 一般不在 try 塊內(nèi)的代碼中拋出異常,try 塊內(nèi)的代碼調(diào)用了別的函數(shù),如函數(shù)A,函數(shù)A 又調(diào)用了函數(shù) B,throw 可以在函數(shù)B中拋出異常,或者更深的函數(shù)調(diào)用層,無論如何,只要有異常拋出,程序?qū)⑥D(zhuǎn)到 catch 處執(zhí)行。
C中如何實(shí)現(xiàn),或者明確地說是模擬這一功能?
下面介紹的是一些簡(jiǎn)單的方法。
現(xiàn)在假設(shè) longjmp() 第二個(gè)值為1,即 setjmp() 第二次將返回1。我們使用一組簡(jiǎn)單的宏來替代 setjmp() 和 longjmp() 以便使用:
首先定義一個(gè)全局的異常:
jmp_buf Jump_Buffer;
因?yàn)?span lang=EN-US> setjmp() 第一次調(diào)用初始化后返回0,第二次返回非0,可以這樣定義一個(gè)宏使得它功能接近于 C++ 的 try。
#define try if(!setjmp(Jump_Buffer))
當(dāng) setjmp() 函數(shù)第一次0 時(shí),取非為真,則執(zhí)行 try 塊內(nèi)的代碼,如:
try
{
Test();
}
當(dāng)因?yàn)檎{(diào)用 longjmp() 拋出異常而導(dǎo)致 setjmp() 第二次返回時(shí)(程序?qū)?huì)轉(zhuǎn)到 setjmp() 函數(shù)處返回,這時(shí),這時(shí)應(yīng)該執(zhí)行的是異常處理代碼。longjmp() 使 setjmp() 函數(shù)返回非0,if(!setjmp(JumpBuffer)) 中將值取非則為假,是以,異常處理放在其后應(yīng)該使用一個(gè) else:
#define catch else
如此看起來便跟 C++ 相似了,setjmp() 函數(shù)的第二次返回導(dǎo)致 if() 中表達(dá)式值為假,剛好使 catch 塊得以執(zhí)行,如:
try
{
Test();
}
catch
{
puts("Error");
}
實(shí)現(xiàn)如 C++ 的 throw 語句,事實(shí)上以宏替換 longjmp(jmp_buf, int) 的調(diào)用:
#define throw longjmp(Jump_Buffer, 1)
下面的例程解釋如何使用這些宏:
/* 輸入一個(gè)整型數(shù),如果大于 100,則以異常拋出 */
#i nclude "stdio.h"
#i nclude "conio.h"
#i nclude "setjmp.h"
jmp_buf Jump_Buffer;
#define try if(!setjmp(Jump_Buffer))
#define catch else
#define throw longjmp(Jump_Buffer, 1)
int Test(int T);
int Test_T(int T);
int Test(int T)
{
if(T > 100)
throw;
else
puts("OK.");
return;
}
int Test_T(int T)
{
Test(T);
return;
}
int main(void)
{
int T;
try
{
puts("Input a :");
scanf("%d", &T);
T++;
Test_T(T);
}
catch
{
puts("Input Error!");
}
getch();
return 0;
}
當(dāng)遇到 throw 拋出異常,立即轉(zhuǎn)跳到 setjmp 處執(zhí)行,
屏棄了與之無相關(guān)的枝節(jié)(函數(shù)的返回及 throw 其后的代碼)
main [setjmp()] -> Test_T -> Test [throw]
↑ ↓
┗━━━━━━━━━━━━━━┛
當(dāng)輸入一個(gè)大于100的整數(shù),throw 導(dǎo)致異常拋出,使用 1 返回到 setjmp() 函數(shù)處,宏 try 使 if(!setjmp(Jump_Buffer)) 不成立,執(zhí)行 catch 塊,catch 塊是十分簡(jiǎn)單的 else 分支語句關(guān)鍵詞的別稱。
這一組宏完成了對(duì) setjmp() 及 longjmp() 兩個(gè)函數(shù)的封裝,使程序具備簡(jiǎn)單的異常處理功能。然而,遺憾的是這組宏不具備嵌套的能力;
當(dāng)這組宏應(yīng)用到嵌套異常,只能響應(yīng)最后一組異常宏,并且無法拋出異常類型,至少它連一個(gè)常量整型都無法拋出,這是因?yàn)?span lang=EN-US> jmp_buf 全局只能存放一個(gè) jmpb 異常結(jié)構(gòu)。
以下是對(duì)上面敘述的簡(jiǎn)單的圖示:
雖然這組宏無法嵌套使用,然而拋出一個(gè)常量整型是有可能的(甚至是一個(gè)結(jié)構(gòu)struct),更改成如下一組宏,便可拋出一個(gè)常量整型,并且可以在 catch 處以 catch() 的方式處理異常。
jmp_buf Jump_Buffer;
int T;
#define try if( !( T = setjmp(Jump_Buffer) ) )
#define catch(Val) elseif(T == Val)
/* throw 拋出的值不應(yīng)該等于0,因?yàn)檫@會(huì)導(dǎo)致無法執(zhí)行try后面的catch塊而繼續(xù)執(zhí)行形成了死循環(huán)*/
#define throw(Val) longjmp(Jump_Buffer, Val)
下面的例程演示了這組宏:
/* 輸入一個(gè)整型數(shù)值,若大于 100 以異常拋出一個(gè)常量 20
否則以異常拋出一個(gè)常量 20 */
#i nclude "stdio.h"
#i nclude "conio.h"
#i nclude "setjmp.h"
jmp_buf Jump_Buffer;
int T;
#define try if( !( T = setjmp(Jump_Buffer) ) )
#define catch(Val) else if(T == Val)
#define catch_all else /*它只能是放置在最后的一個(gè)異常處理塊 */
#define throw(Val) longjmp(Jump_Buffer, Val)
int Test(int T);
int Test_T(int T);
int Test(int T)
{
if(T > 100)
throw(20); /*只是演示,20這個(gè)常量值并無特別意義*/
throw(10); /* catch_all 塊將處理這個(gè)異常*/
return;
}
int Test_T(int T)
{
Test(T);
return;
}
int main(void)
{
int T;
try
{
puts("Input a :");
scanf("%d", &T);
T++;
Test_T(T);
}
catch(20)
{
puts("Input Error!(Code: 20)");
}
catch_all
{
puts("Unknown error!");
}
getch();
return 0;
}
正是因?yàn)?span lang=EN-US> jmp_buf 全局只能存放一個(gè) jmpb 結(jié)構(gòu),使得只有最后一組宏可以響應(yīng)異常;
這是無法嵌套異常的原因,要實(shí)現(xiàn)多重嵌套可以建立一個(gè)全局堆棧來維護(hù)一組 jmpb 結(jié)構(gòu),將在日后給出實(shí)現(xiàn),若感興趣請(qǐng)自行實(shí)現(xiàn),請(qǐng)將實(shí)現(xiàn)給我一份以作參考。這里給出的只是C異常處理的簡(jiǎn)單實(shí)現(xiàn),若要完善的異常處理,這需要更多的手段(如有部分異常需要由信號(hào)捕抓及處理)。