錯誤處理是任何語言都需要解決的問題,只有不能保證100%的正確運行,就需要有處理錯誤的機制。異常處理就是其中的一種錯誤處理方式。
1 過程活動記錄(Active Record)
C語言中每當有一個函數(shù)調(diào)用時,就會在堆棧(Stack)上準備一個被稱為AR的結(jié)構(gòu)。?
每當遇到一次函數(shù)調(diào)用的語句,C編譯器都會產(chǎn)生出匯編代碼來在堆棧上分配這個AR。例如下面的C代碼:
void?a(int?i)
{
????if(i==0){
????????i?=?1;
????}
????else
????{
????????printf("i?=?%d?n",?i);
????}
}
int?main(int?argc,?char**?argv)
{
????a(1);
}123456789101112131415123456789101112131415
2 通過setjmp和longjmp操縱AR,完成任意跳轉(zhuǎn)
那么如何來操縱AR呢,一個可能的方法是,根據(jù)局部變量的地址進行推算,例如對于上面的a函數(shù),執(zhí)行a函數(shù)時的當前AR地址就是參數(shù)i的地址偏移8個字節(jié),也就是 ((char*)&i) - 8。然而,不同的C編譯器,以及不同的硬件平臺都會產(chǎn)生不同的AR結(jié)構(gòu)布局,甚至在一些平臺上,AR根本不會存放到Stack中。所以這種方式操縱AR是不通用的。
為此,c語言通過庫函數(shù)的方式提供了操縱AR的統(tǒng)一方法,那就是setjmp和longjmp函數(shù)。
int?setjmp(jmp_buf?jb); void?longjmp(jmp_buf?jb,?int?r);
1212
setjmp用于保存當前AR到jb變量中;?
而longjmp用于設(shè)置當前AR為jb,并跳轉(zhuǎn)到調(diào)用setjmp();之后的第一個語句處。其結(jié)果就相當于回到了setjmp()剛執(zhí)行完畢,只是偷偷的修改了setjmp的返回值。
setjmp()第一次調(diào)用時總是返回0,而通過longjmp(jb,r)跳轉(zhuǎn)后其返回值總是被修改為r,并且r不能為0。這樣程序中就很容易根據(jù)setjmp()的返回值來判斷是否是longjmp()導致了跳轉(zhuǎn)才執(zhí)行到此。
setjmp/longjmp主要從嵌套的函數(shù)調(diào)用中跳出來。
#include#includejmp_buf?jb;
void?a();
void?b();
void?c();
int?main()
{
????if(setjmp(jb)==0){
????????a();
????}
????printf("after?a();?n");
????return?0;
}
void?a()
{
????b();
????printf("a()?is?calledn");
}
void?b()
{
????c();
????printf("b()?is?calledn");
}
void?c()
{
????printf("c()?is?calledn");
????longjmp(jb,?1);
}12345678910111213141516171819202122232425262728293031321234567891011121314151617181920212223242526272829303132
在c()中可以直接跳轉(zhuǎn)到main()中,實際上longjmp不限制跳轉(zhuǎn)的目的地,可以跳轉(zhuǎn)到任意位置并恢復當時的堆棧環(huán)境(堆棧平衡)。
3 C語言中實現(xiàn)異常處理
異常處理是錯誤處理的一種方式,C語言中更常用的錯誤處理方式是檢測函數(shù)返回值。
#includeint?f1()
{
????if(1/*正確執(zhí)行*/)?{?return?1;?}
????else?{?return?-1;?}
}
int?f2()
{
????if(0/*正確執(zhí)行*/)?{?return?1;?}
????else?{?return?-1;?}
}
int?main()
{
????if(f1()<0){
????????printf("錯誤處理1n");
????????exit(1);
????}
????if(f2()<0){
????????printf("錯誤處理2n");
????????exit(2);
????}
????return?0;
}123456789101112131415161718192021222324252627123456789101112131415161718192021222324252627
上面代碼顯示了常見的C語言錯誤處理方式。嚴謹?shù)能浖_發(fā)中,必須檢測每一次函數(shù)調(diào)用可能出現(xiàn)的錯誤,并做相應的處理。造成的后果就是冗長繁瑣的代碼。為了統(tǒng)一處理錯誤,C++,C#,Java等現(xiàn)代語言引入了異常處理機制。同樣功能的C++代碼大概如下:
#includeclass?Ex1{
};
class?Ex2{
};
void?f1()
{
????printf("進入f1()n");
????if(0/*正確執(zhí)行*/){?}
????else?{
????????throw?Ex1();
????}
????printf("退出f1()n");
}
void?f2()
{
????printf("進入f2()n");
????if(1/*正確執(zhí)行*/)?{??}
????else?{
????????throw?Ex2();
????}
????printf("退出f2()n");
}
int?main()
{
????try{
????????f1();
????????f2();
????}catch(Ex1?&ex){
????????printf("處理錯誤1n");
????????exit(1);
????}
????catch(Ex2?&ex){
????????printf("處理錯誤2n");
????????exit(2);
????}
????return?0;
}123456789101112131415161718192021222324252627282930313233343536373839404142123456789101112131415161718192021222324252627282930313233343536373839404142
程序輸出:
進入f1() 處理錯誤1
1212
可見,異常處理讓代碼看起來更加整潔,邏輯代碼在一起,錯誤處理代碼在一起。throw后面的語句不再執(zhí)行,執(zhí)行流直接跳轉(zhuǎn)到最近的try對應的catch塊。
可以推測,
throw要負責兩件事情:(1)完成跳轉(zhuǎn);(2)恢復堆棧AR;try則負責保存當前AR
可見這與setjmp/longjmp基本相當。于是可以在C中近似寫成。
#include#include#includejmp_buf?jb;
void?f1()
{
????printf("進入f1()n");
????if(0/*正確執(zhí)行*/){?}
????else?{
????????longjmp(jb,1);
????}
????printf("退出f1()n");
}
void?f2()
{
????printf("進入f2()n");
????if(1/*正確執(zhí)行*/)?{??}
????else?{
????????longjmp(jb,?2);
????}
????printf("退出f2()n");
}
int?main()
{
????int?r?=?setjmp(jb);
????if(r==0){
????????f1();
????????f2();
????}else?if(r==1){
????????printf("處理錯誤1n");
????????exit(1);
????}else?if(r==2){
????????printf("處理錯誤2n");
????????exit(2);
????}
????return?0;
}12345678910111213141516171819202122232425262728293031323334353637383940411234567891011121314151617181920212223242526272829303132333435363738394041
當然完整的異常處理遠比這里的代碼要復雜,需要考慮異常的嵌套等,這里僅僅給出最簡單的思路。
4 不要在C++中使用setjmp和longjmp
C++為異常處理提供了直接支持。除非極特殊需要,不要再重新實現(xiàn)自己的異常機制,尤其需要說明的是,簡單的調(diào)用setjmp/longjmp有可能帶來問題。如
#include#include#includeclass?MyClass
{
public:
????MyClass(){?printf("MyClass::MyClass()n");}
????~MyClass(){?printf("MyClass::~MyClass()n");}
};
jmp_buf?jb;
void?f1()
{
????MyClass?obj;
????printf("進入f1()n");
????if(0/*正確執(zhí)行*/){?}
????else?{
????????longjmp(jb,1);
????}
????printf("退出f1()n");
}
void?f2()
{
????printf("進入f2()n");
????if(1/*正確執(zhí)行*/)?{??}
????else?{
????????longjmp(jb,?2);
????}
????printf("退出f2()n");
}
int?main()
{
????int?r?=?setjmp(jb);
????if(r==0){
????????f1();
????????f2();
????}else?if(r==1){
????????printf("處理錯誤1n");
????????exit(1);
????}else?if(r==2){
????????printf("處理錯誤2n");
????????exit(2);
????}
????return?0;
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
g++編譯,程序輸出:
MyClass::MyClass() 進入f1() 處理錯誤1
123123
vc++編譯,程序輸出:
MyClass::MyClass() 進入f1() MyClass::~MyClass() 處理錯誤1
12341234
longjmp()跳轉(zhuǎn)前局部對象可能并不會析構(gòu)(g++),也可能析構(gòu)(VC++),C++標準對此并無明確要求。這種依賴于具體編譯器版本的代碼是應該避免的。
而C++本身的throw關(guān)鍵字,卻能嚴格保證局部對象構(gòu)造和析構(gòu)的成對調(diào)用。
5 辯證看待異常處理
為實現(xiàn)異常處理,C++編譯器為此必須做更多的工作,也必然導致在AR中直接或間接地存放更多的信息,并產(chǎn)生操作這些信息的匯編代碼,最終必然導致運行效率的降低。
另一方面,已經(jīng)存在大量沒有嚴格使用異常處理C++函數(shù)庫和類庫,兼容的C庫更是沒有異常的概念,歷史的包袱讓C++很難完全采用異常處理。在這個方面,Java和C#從頭開始,重要的庫都實現(xiàn)了標準的異常處理規(guī)范,完全采用異常機制切實可行。
有趣的是C++11在標準中刪除了異常規(guī)范,而且添加了 noexcept關(guān)鍵字來聲明一個函數(shù)不會拋出異常,可見異常并不是那么受歡迎。
C++編譯器也會提供一個禁用異常的選項。?
然而,C++的STL廣泛使用異常,所以實際上使用了STL的C++程序是不可能禁用異常的,要是沒有了STL,C++又有什么優(yōu)勢了呢?C++在不斷的矛盾沖突中向前發(fā)展者。





