C語(yǔ)言的那些小秘密之異常處理
很多讀者在此之前可能根本沒(méi)有使用或者聽(tīng)說(shuō)過(guò)C語(yǔ)言的異常處理,印象中都是C++或者java才有的東西,C語(yǔ)言怎么會(huì )有異常處理呢?當然估計在大學(xué)出于一般的性的學(xué)習考試之類(lèi)的話(huà)老師幾乎是不會(huì )提及C語(yǔ)言的異常處理的,那么到底什么是異常處理?C語(yǔ)言中又該如何來(lái)實(shí)現異常處理呢?那么我們今天就講解一種典型的實(shí)現C語(yǔ)言異常處理的方法,以setjmp()函數和longjmp()函數實(shí)現的異常處理,我盡可能的把它們是怎樣實(shí)現異常處理方法講解清楚,希望接下來(lái)的內容對你有所幫助,讓你學(xué)到一些新的東西。
本文引用地址:http://dyxdggzs.com/article/276910.htm首先我們來(lái)了解下異常處理,異常是一個(gè)在程序執行期間發(fā)生的事件,它中斷正在執行的程序的正常的指令流,而我們的異常處理功能提供了處理程序運行時(shí)出現的任何意外或異常情況的方法。
接下來(lái)我們先看看setjmp()函數和longjmp()函數實(shí)現C語(yǔ)言異常處理。
setjmp()函數原型:
int ( jmp_buf env );
如果我們打開(kāi)源代碼會(huì )發(fā)現在setjmp()函數中涉及到很多的寄存器的操作,如Ebp、Ebx、Edi、Esi、Esp、 Eip等等,在此就不一一例舉了,我們無(wú)非是想向讀者說(shuō)明一個(gè)問(wèn)題,那就是在調用setjmp()函數的過(guò)程中保存程序的當前運行時(shí)的堆棧環(huán)境,保存這些堆棧環(huán)境有什么用呢?接下來(lái)我們看看longjmp()函數。
longjmp()函數原型:
void longjmp( jmp_buf env, int value );
剛剛上面的函數功能是保存程序執行時(shí)候的堆棧環(huán)境,我們發(fā)現在longjmp()函數里也有一個(gè)jmp_buf類(lèi)型的env變量,這其實(shí)是為了保證接下來(lái)調用longjmp時(shí),會(huì )根據這個(gè)曾經(jīng)保存的變量來(lái)恢復先前的環(huán)境,并且當前的程序控制流,會(huì )因此而返回到最初調用setjmp()函數時(shí)的程序執行點(diǎn)。此時(shí),在接下來(lái)的控制流的例程中,所能訪(fǎng)問(wèn)的所有的變量,包含了longjmp函數調用時(shí)所擁有的變量。我們就這樣說(shuō)讀者可能就得有點(diǎn)抽象了,那我們還是來(lái)看看一段代碼后再來(lái)分析吧,在此特地給出了一個(gè)簡(jiǎn)單的代碼,由易到難的來(lái)分析。
[cpp] view plaincopy#include
#include
jmp_buf buf;
void error_code(void)
{
longjmp(buf,1);
}
int main()
{
double a,b;
printf("請輸入被除數:");
scanf("%lf",&a);
printf("請輸入除數:");
if(setjmp(buf)==0)
{
scanf("%lf",&b);
if(0==b)
error_code();
printf("相除的結果為:%fn",a/b);
}
else
printf("出現錯誤除數為0n");
return 0;
}
運行結果為:

[cpp] view plaincopy請輸入被除數:12
請輸入除數:0
出現錯誤除數為0
Press any key to continue
看了上面的運行結果,現在我們接著(zhù)上面的講,在一開(kāi)始的部分我們并沒(méi)有具體的交代setjmp()函數和longjmp()函數的返回值和參數的具體含義。兩個(gè)函數中的env變量保存的是調用setjmp()函數的時(shí)候當前運行程序的堆棧信息,而longjmp()函數的調用就是根據在調用setjmp()函數的時(shí)候的堆棧信息返回到最初調用setjmp()函數的地方,而其中的第二個(gè)參數就是此刻setjmp()函數的返回值,但是值得注意的就是調用longjmp()函數之后setjmp函數返回的值必須是非零值,如果longjmp傳送的value參數值為0,那么實(shí)際上setjmp返回的值是1。一開(kāi)始我們調用setjmp()函數的時(shí)候,它的返回值為0,之后再調用longjmp()函數的時(shí)候,通過(guò)設定longjmp()函數的第二個(gè)參數來(lái)設定它的返回值。
現在我們來(lái)分析上邊的代碼,在main()函數中,我們最初調用setjmp()函數的時(shí)候,把當前的環(huán)境信息保存在了buf中,函數返回0,然后往下運行,我們輸入0。通過(guò)if語(yǔ)句發(fā)現b的值為0那么就調用error_code()函數來(lái)進(jìn)行處理,在該函數中我們使用了longjmp()函數,其使用方式為longjmp(buf,1);,通過(guò)上面的講解,我們知道第一個(gè)參數的作用是用來(lái)得到最初調用setjmp()函數是的環(huán)境信息,以便在使用longjmp()函數的時(shí)候能夠正確的返回到setjmp()函數最初的調用處,而后面的參數表示的返回到setjmp()函數的時(shí)候的返回值。我們在此返回1,所以執行else部分的語(yǔ)句。
分析完了上面的代碼,讀者應該都知道了兩個(gè)函數的使用方法,值得注意的地方就是我們在setjmp與longjmp結合使用時(shí),它們必須有嚴格的先后執行順序,先調用setjmp函數,之后再調用longjmp函數,以恢復到先前被保存的“程序執行點(diǎn)”。否則,假如在setjmp調用之前,執行longjmp函數,將導致程序的執行流變的不可猜測,很輕易導致程序崩潰而退出。為了加深讀者的對于兩個(gè)函數參數的使用,我們看看下面的代碼:
[cpp] view plaincopy#include
#include
#include
#include
jmp_buf buf;
void func1()
{
longjmp(buf,1);
}
void func2()
{
longjmp(buf,2);
}
void func3()
{
longjmp(buf,3);
}
int main( void )
{
int value;
char str[50];
value = setjmp( buf );
if( value == 0 )
{
func1();
}
switch( value )
{
case 1:
strcpy( str, "func1 return value" );
break;
case 2:
strcpy( str, "func2 return value" );
break;
case 3:
strcpy( str, "func3 return value" );
break;
default:
strcpy( str, "Other error value" );
break;
}
printf("%s:%dn",str,value);
if(1==value)
{
func2();
}
if(2==value)
{
func3();
}
return 0;
}
運行結果為:

[cpp] view plaincopyfunc1 return value:1
func2 return value:2
func3 return value:3
Press any key to continue
看看運行結果,我們分析下代碼,在每個(gè)函數中我們調用longjmp()函數,通過(guò)設置第二個(gè)參數為不同的值來(lái)改變setjmp()函數的返回值,然后我們通過(guò)判斷value值來(lái)打印出是那個(gè)函數的返回值,我們在此例舉這個(gè)簡(jiǎn)單的代碼是要大家加深對于這兩個(gè)函數的參數的使用情況。如果我們在上面的代碼中稍作修改,在setjmp()函數的調用之前調用longjmp()函數,我們發(fā)現此時(shí)沒(méi)有任何的輸出,程序直接崩潰掉退出了。
接下來(lái)我們來(lái)看看一個(gè)函數的使用,如果對于這個(gè)函數不理解的讀者,可以多看幾次我給出的模擬該函數的實(shí)現代碼。
頭文件: #include
功能:設置某一信號的對應動(dòng)作
函數原型:void (*signal(int signum,void(* handler)(int)))(int);
注意:第一個(gè)參數signum指明了所要處理的信號類(lèi)型,它可以取除了SIGKILL和SIGSTOP外的任何一種信號。
如果讀者是第一場(chǎng)接觸上面的函數的話(huà)可能有些不知道該如何著(zhù)手,一時(shí)間有些難以理解,不知道到底是什么意思。別急,我們現在來(lái)逐一分析它到底是什么意思,我們在講解之前再來(lái)看看它的另外一種表示方法。
typedef void(*sig_t) ( int );
sig_t signal(int signum,sig_t handler);
把上面的函數原型拆分為了如上兩行代碼,現在我們分析下上面的兩行代碼。
第一行代碼定義了一個(gè)函數指針(注:如果有對函數指針知識點(diǎn)不熟悉的讀者可以去閱讀我之前寫(xiě)的那篇文章《C語(yǔ)言的那些小秘密之函數指針》),其類(lèi)型為含有一個(gè)int型參數,無(wú)返回值;
第二行代碼中,signal函數的返回值是一個(gè)函數指針,與第一行我們定義的類(lèi)型相同,第二個(gè)參數也為一個(gè)函數指針,其實(shí)signal的返回值就是第二個(gè)函數指針指向的函數地址。這樣說(shuō)可能有不少讀者都有些懵的感覺(jué),還是老方法,代碼最有說(shuō)服力,我們還是為讀者模擬下signal的實(shí)現方式,呈現出一段代碼來(lái)分析下。
[cpp] view plaincopy#include
#include
typedef void (*pfun) ();
pfun signal_call(int a,pfun fdsa);
pfun signal_call(int a,pfun fdsa)
{
return fdsa;
}
void func()
{
printf("hello world!!!n");
}
int main()
{
pfun p = func;
signal_call(1,p)();
return 0;
}
運行結果為:

[cpp] view plaincopyhello world!!!
Press any key to continue
現在我們來(lái)分析下上面的代碼,我們采用上面的定義形式實(shí)現了如下兩行代碼:
typedef void (*pfun) ();
pfun signal_call(int a,pfun fdsa);
在接下來(lái)的main()函數中我們定義了一個(gè)函數指針p,使其指向了 func()函數,接下來(lái)我們使用了一句 signal_call(1,p)();代碼,實(shí)現了func函數調用,那么這到底是怎么實(shí)現的呢?那么我們來(lái)分析下,前面的signal_call(1,p)返回的是一個(gè)函數指針,在代碼中我們發(fā)現其實(shí)返回的就是p,所以signal_call(1,p)();就可以變形為p(),看到這種形式我們這就可以很清楚的看出,它調用的就是我們代碼中的func()函數了?,F在讀者明白了signal()函數的實(shí)現方法,接下來(lái)我們來(lái)看看一段使用signal捕捉除數為0時(shí)候的異常代碼。
[cpp] view plaincopy#include
#include
#include
#include
#include
#include
jmp_buf buf;
int err;
void handler( int num )
{
err = num;
printf( "發(fā)生浮點(diǎn)計算異常n");
longjmp( buf, 1);
}
int main( void )
{
double a, b;
char str[20];
int ret;
_control87( 0, _MCW_EM );
if( signal( SIGFPE, handler ) == SIG_ERR )
{
printf("綁定失敗n" );
abort();
}
ret = setjmp( buf );
if(0 == ret )
{
printf("請輸入被除數:");
scanf("%lf",&a);
printf("請輸入除數:");
scanf("%lf",&b);
printf( "a / b = %4.3gn", a/b);
printf("發(fā)生異常時(shí)候不會(huì )被執行的語(yǔ)句n");
}
return 0;
}
沒(méi)有發(fā)生異常時(shí)候的運行結果:

[cpp] view plaincopy請輸入被除數:123
請輸入除數:3
a / b = 41
發(fā)生異常時(shí)候不會(huì )被執行的語(yǔ)句
Press any key to continue
發(fā)生異常時(shí)候的運行結果:

[cpp] view plaincopy請輸入被除數:12
請輸入除數:0
發(fā)生浮點(diǎn)計算異常
Press any key to continue
現在來(lái)分析下上面的運行結果,先看看_control87( 0, _MCW_EM );這句,可能很多讀者對于這代碼比較陌生,它的功能是開(kāi)啟所有的浮點(diǎn)計算異常,通常情況下浮點(diǎn)計算異常是被屏蔽掉的,我們?yōu)榱四軌蚴沟媒酉聛?lái)的signal能夠捕捉到浮點(diǎn)計算異常,所以要將其開(kāi)啟。在往下看我們通過(guò)signal( SIGFPE, handler )來(lái)綁定了一個(gè)浮點(diǎn)計算異常處理函數,如果發(fā)生異常時(shí),那么就調用handler()函數來(lái)處理。接下來(lái)通過(guò)ret = setjmp( buf );保存程序運行的環(huán)境信息,以便接下來(lái)的調用longjmp()函數能夠根據這個(gè)保存的信息返回該程序先前setjmp()函數的執行點(diǎn)。同時(shí)我們對比兩次運行的結果發(fā)現如果發(fā)現異常的時(shí)候接下來(lái)的打印語(yǔ)句“printf("發(fā)生異常時(shí)候不會(huì )被執行的語(yǔ)句n");”是不會(huì )被執行的,直接跳轉到我們綁定的handler()函數執行了,當然我們在此僅僅是例舉一些簡(jiǎn)單的代碼教會(huì )讀者學(xué)會(huì )使用setjmp()函數和longjmp()函數來(lái)實(shí)現異常處理,讀者完全可以在此基礎上編寫(xiě)出復雜的異常處理。
到這兒C語(yǔ)言的異常處理部分就結束了,由于本人水平有限,博客中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時(shí)也歡迎讀者共同探討相關(guān)的內容,如果樂(lè )意交流的話(huà)請留下你寶貴的意見(jiàn)。
c語(yǔ)言相關(guān)文章:c語(yǔ)言教程
c++相關(guān)文章:c++教程
評論