增强错误恢复能力是提高代码健壮性的最有力途径之一
之所以平时编写代码的时候不愿意去写错误处理,主要是由于这项工作及其无聊并可能导致代码膨胀,导致的结果就是本来就比较复杂的程序变得更加复杂。当然了,前面的缘由主要是针对C语言的,原因就在于C语言的‘紧耦合’性,必须在接近函数调用的地方使用错误处理,当然会增加复杂性了。
1.传统的错误处理(主要是针对C语言的方法)
1)函数中返回错误信息,或者设置一个全局的错误状态。导致的问题就和前面说到的一样,代码数量的爆炸,而且,从一个错误的函数中返回的东西本身也没什么意义。
2)使用鲜为人知的信号处理。由函数signal()和函数raise()。当然了,这样的话耦合度还是相当的高。
3)使用标准库中非局部跳转函数:setjump()和longjump(), 使用setjump()可以保存程序中已知的一个无错误状态,一旦发生错误,可以使用longjump()返回到该状态
下面的代码演示了setjump()和longjump()的使用方法(用C++描述)
/*
对函数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.抛出异常
当代码出现异常的时候,可以创建一个包含错误信息的对象并抛出当前语境,如下:
#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()进行修改得到的程序:
#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.异常匹配
一个异常并不与其处理器完全相关,一个对象或者是指向派生类对象的引用都能与基类处理器匹配。最好是通过引用而不是通过值来匹配异常(防止再次拷贝)。如果一个指针被抛出,将使用通常的标准指针转换来匹配异常,但不会把一种异常类型自动转换为另一种异常类型:
#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;
}
下面的例子显示了基类的异常处理器怎样捕获派生类异常:
#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()函数
#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++的异常处理可以使得程序从正常的处理流程跳转到异常处理流程,此时,构造函数建立起来的所有对象,析构函数一定会被调用。
下面的例子展示了当构造函数没有正常结束是不会调用相关联的析构函数。
#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;
}
如果一个对象的构造函数则执行时发生异常,那么这个对象的析构函数就不会被调用,因此,如果在构造函数中分配了资源却产生异常,析构函数是不能释放这些资源的。例如常说的“悬挂”指针。
下面是一个例子:
#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、在构造函数中分配资源,在析构函数中释放资源
这样使得资源的每一次分配都具有原子性,称为资源获得式初始化,使得对象对资源的控制的时间与对象的生命周期相等,下面对上述例子作一些修改:
#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;
}
这是运行结果:
使用这种方法与第一种的不同之处:使得每个指针都被嵌入到对象之中,这些对象的构造函数最先被调用,并且如果他们之中任何一个构造函数在抛出异常之前完成,那么这些对象的析构函数也会在栈反解的时候被调用。
程序中,operator[]使用了一个称作RangeeError的嵌套类,如果参数越界,那么就创建一个RangeeError的类型对象。
auto_ptr:
由于在C++中动态内存的分配非常频繁,所以C++提供了一个RALL封装类,用于指向分配的对内存:
#include <iostream>
#include <memory>
#include <cstddef>
using namespace std;
class TraceHeap
{
int i;
public:
static void* operator new(size_t siz){
void* p=::operator new(siz);
cout<<"Allocating TraceHeap object on the heap at address "<<p<<endl;
return p;
}
static void operator delete(void* p){
cout<<"Deleting TraceHeap object at address "<<p<<endl;
::operator delete(p);
}
TraceHeap(int i):i(i){}
int getVal() const {return i;}
};
int main()
{
auto_ptr<TraceHeap> pMyObject(new TraceHeap(5));
cout<<pMyObject->getVal()<<endl;
}
程序的运行结果为:
函数级的try块:
由于构造函数能够抛出异常,为了处理在对象的成员或者其基类子类被抛出的异常,可以把这些子对象的初始化放到try中:
#include <iostream>
using namespace std;
class Base
{
int i;
public:
class BaseExcept{};
Base(int i):i(i){throw BaseExcept();}
};
class Dirived:public Base
{
public:
class DirivedExcept{
const char* msg;
public:
DirivedExcept(const char* msg):msg(msg){}
const char* what() const{return msg;}
};
Dirived(int j) try : Base(j){
cout<<"this won't print"<<endl;
}catch(BaseExcept&){
throw DirivedExcept("Base subobject threw");
}
};
int main()
{
try{
Dirived d(3);
}catch(Dirived::DirivedExcept& d){
cout<<d.what()<<endl;
}
}