增強錯誤恢復能力是提高代碼健壯性的最有力途徑之一
之所以平時編寫代碼的時候不願意去寫錯誤處理,主要是由於這項工作及其無聊並可能導致代碼膨脹,導致的結果就是本來就比較復雜的程序變得更加復雜。當然了,前面的緣由主要是針對C語言的,原因就在於C語言的‘緊耦合’性,必須在接近函數調用的地方使用錯誤處理,當然會增加復雜性了。
1.傳統的錯誤處理(主要是針對C語言的方法)
1)函數中返回錯誤信息,或者設置一個全局的錯誤狀態。導致的問題就和前面說到的一樣,代碼數量的爆炸,而且,從一個錯誤的函數中返回的東西本身也沒什麼意義。
2)使用鮮為人知的信號處理。由函數signal()和函數raise()。當然了,這樣的話耦合度還是相當的高。
3)使用標准庫中非局部跳轉函數:setjump()和longjump(), 使用setjump()可以保存程序中已知的一個無錯誤狀態,一旦發生錯誤,可以使用longjump()返回到該狀態
下面的代碼演示了setjump()和longjump()的使用方法(用C++描述)
[cpp]
<SPAN style="FONT-FAMILY: SimSun; FONT-SIZE: 14px">/*
對函數setjmp(),如果直接調用,便會將當前處理器相關的信息保存到jmp_buf中並返回0
但如果使用同一個jmp_buf調用longjmp(),則函數就會返回到setjmp剛剛返回的地方
這次的返回值是longjmp的第二個參數
與goto語句的差別是,使用longjmp()可以返回任何預先確定的位置
*/
#include <iostream>
#include <csetjmp>
using namespace std;
class Rainbow
{
public:
Rainbow(){cout<<"Rainbow()"<<endl;}
~Rainbow(){cout<<"~Rainbow()"<<endl;}
};
jmp_buf kansas;
void oz()
{
Rainbow rb;
for(int i=0;i<3;i++)
cout<<"there's no place like home"<<endl;
longjmp(kansas,47);
}
int main()
{
if(setjmp(kansas)==0)
{
cout<<"toenado,witch,munchkins..."<<endl;
oz();
}
else
{
cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;
}
return 0;
}</SPAN>
/*
對函數setjmp(),如果直接調用,便會將當前處理器相關的信息保存到jmp_buf中並返回0
但如果使用同一個jmp_buf調用longjmp(),則函數就會返回到setjmp剛剛返回的地方
這次的返回值是longjmp的第二個參數
與goto語句的差別是,使用longjmp()可以返回任何預先確定的位置
*/
#include <iostream>
#include <csetjmp>
using namespace std;
class Rainbow
{
public:
Rainbow(){cout<<"Rainbow()"<<endl;}
~Rainbow(){cout<<"~Rainbow()"<<endl;}
};
jmp_buf kansas;
void oz()
{
Rainbow rb;
for(int i=0;i<3;i++)
cout<<"there's no place like home"<<endl;
longjmp(kansas,47);
}
int main()
{
if(setjmp(kansas)==0)
{
cout<<"toenado,witch,munchkins..."<<endl;
oz();
}
else
{
cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;
}
return 0;
}程序的運行結果如下:
可以看到,程序並沒有調用類的析構函數,而這樣本身就是異常現象(C++定義的),所以,這些函數不適合C++。
2.拋出異常
當代碼出現異常的時候,可以創建一個包含錯誤信息的對象並拋出當前語境,如下:
[cpp]
<SPAN style="FONT-SIZE: 14px">#include <iostream>
using namespace std;
class MyError
{
const char* const data;
public:
MyError(const char* const msg=0):data(msg){}
};
void f()
{
throw MyError("Something bad happen");
}
/*
當然了。這裡沒有使用try,程序會報錯
*/
int main()
{
f();
return 0;
}</SPAN>
#include <iostream>
using namespace std;
class MyError
{
const char* const data;
public:
MyError(const char* const msg=0):data(msg){}
};
void f()
{
throw MyError("Something bad happen");
}
/*
當然了。這裡沒有使用try,程序會報錯
*/
int main()
{
f();
return 0;
}throw首先會創建程序所拋出對象的一個拷貝,包含throw表達式的函數返回了這個對象,異常發生之前所創建的局部對象被銷毀,這種被稱為“棧反解”。而程序員需要為每一種不同的異常拋出不同的對象。
3.捕獲異常
就像前面所說的,如果一個函數通過throw出了一個對象,那麼函數就會返回這個錯誤對象並退出。如果不想退出這個函數,,那麼就可以設置一個try塊。這個塊被稱作try的原因是程序需要在這裡嘗試調用各種函數。
當然,被拋出的異常會在某個地方被終止,這個地方就是異常處理器(catch)。
異常處理器緊跟在try之後,一旦某個異常被拋出,異常處理機制就會依次尋找參數類型與異常類型相匹配的異常處理器。找到後就會進入catch語句,於是系統就認為這個異常已經處理了。
下面通過對前面的setjump()和longjump()進行修改得到的程序:
[cpp]
#include <iostream>
using namespace std;
class Rainbow
{
public:
Rainbow(){cout<<"Rainbow()"<<endl;}
~Rainbow(){cout<<"~Rainbow()"<<endl;}
};
void oz()
{
Rainbow rb;
for(int i=0;i<3;i++)
cout<<"there's no place like home"<<endl;
throw 47;
}
int main()
{
try{
cout<<"toenado,witch,munchkins..."<<endl;
oz();
}catch(int){
cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;
}
return 0;
}
#include <iostream>
using namespace std;
class Rainbow
{
public:
Rainbow(){cout<<"Rainbow()"<<endl;}
~Rainbow(){cout<<"~Rainbow()"<<endl;}
};
void oz()
{
Rainbow rb;
for(int i=0;i<3;i++)
cout<<"there's no place like home"<<endl;
throw 47;
}
int main()
{
try{
cout<<"toenado,witch,munchkins..."<<endl;
oz();
}catch(int){
cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;
}
return 0;
}
程序的運行結果:
當執行throw語句時,程序的控制流程開始回溯,直到找到帶有int參數的catch為止。程序在這裡繼續恢復執行。當然了,當程序從oz()中返回時,是會調用析構函數的。
在異常處理中有兩個基本的模型:終止於恢復
終止:無論拋出了什麼異常,程序都無法挽救,不需要返回發生異常的地方。
恢復:自動重新執行發生錯誤的代碼。在C++中,必須顯示的將程序的執行流程轉移到錯誤發生的地方,通常是重新調用發生錯誤的函數,例如把try放到while循環中。
4.異常匹配
一個異常並不與其處理器完全相關,一個對象或者是指向派生類對象的引用都能與基類處理器匹配。最好是通過引用而不是通過值來匹配異常(防止再次拷貝)。如果一個指針被拋出,將使用通常的標准指針轉換來匹配異常,但不會把一種異常類型自動轉換為另一種異常類型:
[cpp]
<SPAN style="FONT-SIZE: 14px">#include <iostream>
using namespace std;
class Except1{};
class Except2
{
public:
Except2(const Except1&){}
};
void f(){throw Except1();}
/*這裡的拋出的異常不會做隱式轉換*/
int main()
{
try{
f();
}catch(Except2&){
cout<<"inside catch(Except2)"<<endl;
}catch(Except1&){
cout<<"inside catch(Except1)"<<endl;
}
return 0;
}</SPAN>
#include <iostream>
using namespace std;
class Except1{};
class Except2
{
public:
Except2(const Except1&){}
};
void f(){throw Except1();}
/*這裡的拋出的異常不會做隱式轉換*/
int main()
{
try{
f();
}catch(Except2&){
cout<<"inside catch(Except2)"<<endl;
}catch(Except1&){
cout<<"inside catch(Except1)"<<endl;
}
return 0;
}
下面的例子顯示了基類的異常處理器怎樣捕獲派生類異常:
[cpp]
<SPAN style="FONT-SIZE: 14px">#include <iostream>
using namespace std;
class X
{
public:
class Trouble{};
class Small:public Trouble{};
class Big:public Trouble{};
void f(){throw Big();}
};
/*
程序的結果就是捕獲了第一個異常處理,因為第一個catch處理完了所有異常,所以其他catch不會繼續處理
*/
int main()
{
X x;
try{
x.f();
}catch(X::Trouble&){
cout<<"catch Trouble"<<endl;
}catch(X::Small&){
cout<<"catch Small"<<endl;
}catch(X::Big&){
cout<<"catch Big"<<endl;
}
return 0;
}</SPAN>
#include <iostream>
using namespace std;
class X
{
public:
class Trouble{};
class Small:public Trouble{};
class Big:public Trouble{};
void f(){throw Big();}
};
/*
程序的結果就是捕獲了第一個異常處理,因為第一個catch處理完了所有異常,所以其他catch不會繼續處理
*/
int main()
{
X x;
try{
x.f();
}catch(X::Trouble&){
cout<<"catch Trouble"<<endl;
}catch(X::Small&){
cout<<"catch Small"<<endl;
}catch(X::Big&){
cout<<"catch Big"<<endl;
}
return 0;
}
一般來說,先捕獲派生類的異常,最後捕獲的是基類異常。
捕獲所有異常:catch(...)可以捕獲所有的異常。
重新拋出異常:需要釋放某些資源時,例如網絡連接或堆上的內存需要釋放時,通常希望重新拋出一個異常(捕獲異常之後,釋放資源,然後重新拋出異常)
catch(...){
//釋放一些資源
throw;
}
不捕獲異常:無法匹配異常的話,異常就會傳遞到更高一層,直到能夠處理這個異常。
1.terminate()函數
當沒有任何一個層次的異常處理器能夠處理異常時,這個函數就會調用。terminate()函數會調用abort()使函數終止,此時,函數不會調用正常的終止函數,析構函數不會執行。
2.set_terminate()函數
可以設置自己的terminate()函數
[cpp]
#include <iostream>
#include <exception>
#include <stdlib.h>
using namespace std;
void terminator()
{
cout<<"I'll be back!"<<endl;
exit(0);
}
/*set_terminate返回被替換的指向terminate()函數的指針
第一次調用時,返回的是指向原terminate函數的指針*/
void (*old_terminate)()=set_terminate(terminator);
class Botch
{
public:
class Fruit{};
void f(){
cout<<"Botch::f()"<<endl;
throw Fruit();
}
~Botch(){throw 'c';}
};
/*
程序在處理一個異常的時候會釋放在棧上分配的對象,這時,析構函數被調用,這時候產生了第二個異常
正是這個第二個以下航導致了terminate的調用
*/
int main()
{
try{
Botch b;
b.f();
}catch(...){
cout<<"inside catch(...)"<<endl;
}
return 0;
}
#include <iostream>
#include <exception>
#include <stdlib.h>
using namespace std;
void terminator()
{
cout<<"I'll be back!"<<endl;
exit(0);
}
/*set_terminate返回被替換的指向terminate()函數的指針
第一次調用時,返回的是指向原terminate函數的指針*/
void (*old_terminate)()=set_terminate(terminator);
class Botch
{
public:
class Fruit{};
void f(){
cout<<"Botch::f()"<<endl;
throw Fruit();
}
~Botch(){throw 'c';}
};
/*
程序在處理一個異常的時候會釋放在棧上分配的對象,這時,析構函數被調用,這時候產生了第二個異常
正是這個第二個以下航導致了terminate的調用
*/
int main()
{
try{
Botch b;
b.f();
}catch(...){
cout<<"inside catch(...)"<<endl;
}
return 0;
}
一般來說,不要在析構函數中拋出異常。
5.清理
C++的異常處理可以使得程序從正常的處理流程跳轉到異常處理流程,此時,構造函數建立起來的所有對象,析構函數一定會被調用。
下面的例子展示了當構造函數沒有正常結束是不會調用相關聯的析構函數。
[cpp]
<SPAN style="FONT-SIZE: 14px">#include <iostream>
using namespace std;
class Trace
{
static int counter;
int objid;
public:
Trace(){
objid=counter++;
cout<<"construction Trace #"<<objid<<endl;
if(objid==3)
throw 3;
}
~Trace(){
cout<<"destruction Trace #"<<objid<<endl;
}
};
int main()
{
try{
Trace n1;
Trace Array[5];
Trace n2;
}catch(int i){
cout<<"caught "<<i<<endl;
}
return 0;
}</SPAN>
#include <iostream>
using namespace std;
class Trace
{
static int counter;
int objid;
public:
Trace(){
objid=counter++;
cout<<"construction Trace #"<<objid<<endl;
if(objid==3)
throw 3;
}
~Trace(){
cout<<"destruction Trace #"<<objid<<endl;
}
};
int main()
{
try{
Trace n1;
Trace Array[5];
Trace n2;
}catch(int i){
cout<<"caught "<<i<<endl;
}
return 0;
}
如果一個對象的構造函數則執行時發生異常,那麼這個對象的析構函數就不會被調用,因此,如果在構造函數中分配了資源卻產生異常,析構函數是不能釋放這些資源的。例如常說的“懸掛”指針。
下面是一個例子:
[cpp]
<SPAN style="FONT-SIZE: 14px">#include <iostream>
#include <cstddef>
using namespace std;
class Cat
{
public:
Cat(){cout<<"Cat()"<<endl;}
~Cat(){cout<<"~Cat()"<<endl;}
};
/*這些語句用來模擬內存不足的情況,可以不用鳥他
但可以看到這裡的new中拋出了一個異常*/
class Dog
{
public:
void* operator new(size_t sz){
cout<<"allocating a Dog"<<endl;
throw 47;
}
void operator delete(void* p){
cout<<"deallocating a Dog"<<endl;
::operator delete(p);
}
};
class UseResources
{
Cat* bp;
Dog* op;
public:
UseResources(int count=1){
cout<<"UseResources()"<<endl;
bp=new Cat[count];
op=new Dog;
}
~UseResources(){
cout<<"~UseResources()"<<endl;
delete [] bp;
delete op;
}
};
int main()
{
try{
UseResources ur(3);
}catch(int){
cout<<"inside handler"<<endl;
}
return 0;
}</SPAN>
#include <iostream>
#include <cstddef>
using namespace std;
class Cat
{
public:
Cat(){cout<<"Cat()"<<endl;}
~Cat(){cout<<"~Cat()"<<endl;}
};
/*這些語句用來模擬內存不足的情況,可以不用鳥他
但可以看到這裡的new中拋出了一個異常*/
class Dog
{
public:
void* operator new(size_t sz){
cout<<"allocating a Dog"<<endl;
throw 47;
}
void operator delete(void* p){
cout<<"deallocating a Dog"<<endl;
::operator delete(p);
}
};
class UseResources
{
Cat* bp;
Dog* op;
public:
UseResources(int count=1){
cout<<"UseResources()"<<endl;
bp=new Cat[count];
op=new Dog;
}
~UseResources(){
cout<<"~UseResources()"<<endl;
delete [] bp;
delete op;
}
};
int main()
{
try{
UseResources ur(3);
}catch(int){
cout<<"inside handler"<<endl;
}
return 0;
}
Resources的析構函數沒有被調用,這是因為在構造函數的時候拋出了異常,這樣,創建的Cat對象也無法被析構。
為了防止資源洩露,需要用以下方法防止不成熟的資源分配方式:
1、在構造函數中捕獲異常,用於釋放資源
2、在構造函數中分配資源,在析構函數中釋放資源
這樣使得資源的每一次分配都具有原子性,稱為資源獲得式初始化,使得對象對資源的控制的時間與對象的生命周期相等,下面對上述例子作一些修改:
[cpp]
<SPAN style="FONT-SIZE: 14px">#include <iostream>
#include <cstddef>
using namespace std;
template<class T,int sz=1>
class PWrap
{
T* ptr;
public:
class RangeeError{};
PWrap(){
ptr=new T[sz];
cout<<"Pwrap constractor"<<endl;
}
~PWrap(){
delete[] ptr;
cout<<"PWrap deconstracor"<<endl;
}
T& operator[](int i) throw(RangeeError){
if(i>=0&&i<sz)
return ptr[i];
throw RangeeError();
}
};
class Cat
{
public:
Cat(){cout<<"Cat()"<<endl;}
~Cat(){cout<<"~Cat()"<<endl;}
void g(){}
};
class Dog
{
public:
void* operator new[](size_t sz){
cout<<"allocating a Dog"<<endl;
throw 47;
}
void operator delete[](void* p){
cout<<"deallocating a Dog"<<endl;
::operator delete(p);
}
};
class UseResources
{
PWrap<Cat,3> cats;
PWrap<Dog> dog;
public:
UseResources(){
cout<<"UseResources()"<<endl;
}
~UseResources(){
cout<<"~UseResources()"<<endl;
}
void f(){cats[1].g();}
};
int main()
{
try{
UseResources ur;
}catch(int){
cout<<"inside handler"<<endl;
}catch(...){
cout<<"inside catch"<<endl;
}
return 0;
}</SPAN>