分享一下在C程序設(shè)計當(dāng)中對異常的處理。主要是介紹一下goto和longjmp函數(shù)的使用。
在寫程序的時候,有些地方很容易出錯,當(dāng)然這種出錯不是說那種你寫錯了,而是說比如硬件的初始化失敗了,或者資源暫時不可用等等導(dǎo)致函數(shù)返回異常。這種錯是難以避免的,而且通常是非致命的,只要多嘗試幾次可能就可以了。比如之前我們寫過網(wǎng)絡(luò)編程,要建立網(wǎng)絡(luò)通信,我們需要調(diào)用socket,bind,listen等等一系列函數(shù),每個函數(shù)都有可能會出錯。
但是你的程序怎么知道該怎么處理呢?程序出錯了顯然是不能繼續(xù)往下執(zhí)行的,但是立即終止也不合適,因?yàn)檫@種錯是非致命的,那么我們應(yīng)該怎么去設(shè)計一個比較健壯的程序呢?今天介紹的可以當(dāng)做是一種思路。
一、使用goto
說到goto,可能很多人的第一反應(yīng)是不要用,但是問他為什么他可能講不出來,因?yàn)槭莿e人告訴他的。goto真的不能用嗎?當(dāng)然不是,最有力的證明就是Linux內(nèi)核里面就有大量的goto語句。實(shí)際上,只要用的適當(dāng),還是非常好用的,當(dāng)然我并不是說程序里面goto滿天飛。
下面舉例說明goto的應(yīng)用場景:
有時候我們完成一件事情要分為很多個步驟,每個步驟里面還可能占用一些資源,然而這些步驟很容易出錯,如果其中某個步驟出錯了,就不能繼續(xù)下一個步驟,也不能立即終止程序,因?yàn)檫@樣會使資源得不到釋放。那么使用goto就可以調(diào)出程序并且對資源進(jìn)行回收。
來看一段代碼:
#include
#include
char *p1=NULL,*p2=NULL,*p3=NULL;
int step1(void);
int step2(void);
int step3(void);
int main(int argc,char* argv[])
{
if(step1()<0)
{
goto error1;
}
if(step2()<0)
{
goto error2;
}
if(step3()<0)
{
goto error3;
}
error3:
printf("釋放步驟3的資源\\n");
free(p3);
error2:
printf("釋放步驟2的資源\\n");
free(p2);
error1:
printf("釋放步驟1的資源\\n");
free(p1);
exit(0);
}
int step1(void)
{
p1=(char*)malloc(10);
return 0;
}
int step2(void)
{
p2=(char*)malloc(10);
return 0;
}
int step3(void)
{
p3=(char*)malloc(10);
return -1;
}
在這段代碼里面,假設(shè)完成一件事情一共有三個步驟,每個步驟里面都維護(hù)了一個指針變量(資源),假設(shè)步驟一和步驟二都是正常的,步驟三出了問題,返回一個錯誤的值,如果我們接收到步驟三的錯誤返回值之后立即終止程序,那么步驟一和步驟二里申請的資源就得不到釋放,比如這里的指針會造成內(nèi)存泄漏,顯然不是我們希望看到的。
但是使用上面的這種結(jié)構(gòu),如果在步驟二出錯了,它會跳轉(zhuǎn)到error2這里先釋放步驟2申請的資源,再釋放步驟一 的資源,最后退出,其他的地方出錯也是類似處理。上面是一種代碼框架,實(shí)際寫代碼應(yīng)該根據(jù)實(shí)際情況來處理異常。
我們來看一下效果:
以上就是goto在多個步驟容易出錯時的一種處理。這里順便提一下goto的另外一種應(yīng)用場景,就是用來跳出多層循環(huán)。我們知道跳出循環(huán)一般使用break和continue,但是這個只能調(diào)出當(dāng)前循環(huán),不能跳出多層循環(huán),有時候在多層循環(huán)里面,一旦條件滿足,我們就不需要再執(zhí)行后面的循環(huán)了,使用goto可以解決這個問題。
我們來看一下代碼:
#include
int main(void)
{
for(int i=0;i<2;i++)
{
for(int j=0;j<2;j++)
{
for(int k=0;k<2;k++)
{
if(k==1)
{
goto lable;
}
lable2: printf("i=%d,j=%d,k=%d\\n",i,j,k);
}
}
}
lable:
printf("after goto \\n");
// goto lable2;
}
在這里有三層循環(huán)嵌套,一旦條件滿足,就通過goto跳出整個循環(huán)體,執(zhí)行后面的代碼。如果使用break ,就非常麻煩。
代碼的執(zhí)行結(jié)果是:
第一次k=0,正常打印,第二次,k=1,滿足條件,跳出循環(huán),執(zhí)行后面的語句,打印出after goto.
當(dāng)然,問題也快出來了,剛剛是上面跳到了下面,如果我們再從下面跳上去會怎么樣?我們打開最后一行的注釋,重新編譯執(zhí)行,會發(fā)現(xiàn)打印出幾百上千行的內(nèi)容:
代碼看起來好像不復(fù)雜,就是先跳下去,然后又跳回原來的后面,怎么會打印這么多東西呢?這就是使用goto不當(dāng)帶來的害處。這種交叉式地跳來跳去會使得程序結(jié)構(gòu)非常混亂,混亂到我也懶得去分析。
二、使用longjmp
剛剛講了goto的異常處理,但是goto有一個局限性,就是goto只能在一個函數(shù)內(nèi)進(jìn)行跳轉(zhuǎn),不能跨越函數(shù)。
如果一個函數(shù)里嵌套了多個函數(shù)調(diào)用,而里層的函數(shù)出了錯,希望跳轉(zhuǎn)到上一層或上幾層的函數(shù),該怎么辦?顯然,goto是做不到的。這時可以使用longjmp函數(shù)。longjmp函數(shù)和setjmp函數(shù)配合使用。
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
先在程序容易出錯的地方使用setjmp,定義一個入口,等到后面代碼真的出錯之后使用longjmp跳轉(zhuǎn)到setjmp處。setjmp直接調(diào)用返回0,若從longjmp返回,則為非0.
舉個例子:
#include
#include
jmp_buf jmpbuffer;
int fun1(void);
int fun2(void);
int fun3(void);
int main(int argc,char* argv[])
{
printf("這里是主函數(shù)\\n");
if(setjmp(jmpbuffer)!=0)
{
printf("Error\\n");
}
fun1(); //假設(shè)fun1是一個容易出錯的函數(shù),出錯后將返回上一步,然后再重新執(zhí)行。
printf("這里是主函數(shù)調(diào)用fun1之后\\n");
return 0;
}
int fun1(void)
{
printf("這里是fun1\\n");
fun2();
}
int fun2(void)
{
printf("這里是fun2\\n");
fun3();
}
int fun3(void)
{
static int i=0;
printf("這里是fun3\\n");
if(i++==0)
{
longjmp(jmpbuffer,1); //跳轉(zhuǎn)回main函數(shù)
}
return -1;
}
在這里,主函數(shù)調(diào)用了fun1函數(shù),而fun1調(diào)用fun2,fun2又調(diào)用fun3.這種多層嵌套里面,每一層都可能出錯。如果我們希望里面任何一層出錯了,就返回main函數(shù),那么用longjmp就可以實(shí)現(xiàn)。對上面程序進(jìn)行解釋:
當(dāng)?shù)谝淮螆?zhí)行setjmp時,由于是直接調(diào)用,所以返回0,接著調(diào)用我們的功能函數(shù)fun1,假設(shè)fun3里面出錯了,那么就會通過longjmp跳轉(zhuǎn)到setjmp處,同時攜帶一個返回值1,那么這時就會執(zhí)行if語句進(jìn)行錯誤處理,接著再執(zhí)行fun1,也許此時就全部正常了,一直執(zhí)行到最后。(這是很正常的現(xiàn)象,正如開頭說的,像硬件初始化,申請資源等都可能不是一次成功的,需要重復(fù)多次)。
而且在多個地方都可以使用longjmp,攜帶不同的返回值,這樣根據(jù)setjmp的返回值也很容易確定問題出在哪里。
來看一下效果:
使用longjmp還有一個問題我們可能也需要關(guān)注一下,就是當(dāng)使用longjmp返回的時候,函數(shù)里的那些變量還能保持原來的值嗎?我們可以做一個實(shí)驗(yàn)來驗(yàn)證這一點(diǎn):
#include
#include
#include
static void f1(int,int,int,int);
static void f2(void);
static jmp_buf jmpbuffer;
static int global;
int main(int argc,char* argv[])
{
int autoval;
register int regival;
volatile int volaval;
static int staval;
global=1;autoval=2;regival=3;volaval=4;staval=5;
if(setjmp(jmpbuffer)!=0)
{
printf("after longjmp:\\n");
printf("global=%d,autoval=%d,regival=%d,volaval=%d,staval=%d\\n", \\
global,autoval,regival,volaval,staval);
exit(0);
}
global=10;autoval=20;regival=30;volaval=40;staval=50;
f1(autoval,regival,volaval,staval);
exit(0);
}
static void f1(int a,int b,int c,int d)
{
printf("in f1():\\n");
printf("global=%d,autoval=%d,regival=%d,volaval=%d,staval=%d\\n", \\
global,a,b,c,d);
f2();
}
static void f2(void)
{
longjmp(jmpbuffer,1);
}
這里我們定義了很多種不同的變量,先對變量賦一個初值,然后改變變量的值,接著調(diào)用f1,在f1里打印各變量的值,f1再調(diào)用f2,f2使用longjmp跳轉(zhuǎn)回main函數(shù),那么這時各變量的值如何?是剛開始賦的初值,還是后面改變后的值呢?
我們編譯執(zhí)行一下:
可以發(fā)現(xiàn)使用register聲明的變量保持的是初值,而其他變量都是改變后的值。
如果編譯時進(jìn)行優(yōu)化,結(jié)果又如何?
可以發(fā)現(xiàn)除了剛剛的register聲明的變量,普通局部變量(自動變量)也沒有更新,而是保持了初值,這通常不是我們希望的,我們肯定是希望得到最新的值,這也是因?yàn)榫幾g優(yōu)化帶來的問題。所以如果希望避免這個問題,可以加上volatile來修飾。
以上就是今天要分享的內(nèi)容,主要是在C程序中,由多個步驟可能引發(fā)的錯誤,或者是多層嵌套里面可能出現(xiàn)的錯誤進(jìn)行處理,還要注意資源的回收等問題。附帶講了亂用goto帶來的弊端,以及在函數(shù)間跳轉(zhuǎn)與返回時變量的值的改變,程序優(yōu)化帶來的影響等。
-
函數(shù)
+關(guān)注
關(guān)注
3文章
4341瀏覽量
62806 -
代碼
+關(guān)注
關(guān)注
30文章
4808瀏覽量
68812 -
程序設(shè)計
+關(guān)注
關(guān)注
3文章
261瀏覽量
30419 -
C程序
+關(guān)注
關(guān)注
4文章
255瀏覽量
36074
發(fā)布評論請先 登錄
相關(guān)推薦
評論