Chinaunix首页 | 论坛 | 博客
  • 博客访问: 257869
  • 博文数量: 42
  • 博客积分: 2415
  • 博客等级: 大尉
  • 技术积分: 590
  • 用 户 组: 普通用户
  • 注册时间: 2006-01-13 14:14
文章分类

全部博文(42)

文章存档

2018年(1)

2017年(8)

2015年(3)

2012年(4)

2011年(11)

2010年(1)

2009年(5)

2008年(9)

我的朋友

分类: C/C++

2009-02-19 02:16:32

异常的基本思想就是当一个函数发现自己不能处理的错误是就throw异常,由其直接或者间接的调用者catch这个异常。
当一个函数检测到自己不能处理的错误时,传统的处理方法有以下几种:
1.终止程序:实际上在很多情况下出现错误时是不能简单的终止的,尤其是对于库来说,出现错误应该由调用者来决定怎样处理。
2.返回一个代表错误的值:如果每一种错误都由一个值来表示的话,调用者需要检查每一种可能,这是一个负担。
3.返回一个合法的值并让程序处于非法状态:例子就是C库函数,出现错误时一般返回-1,并置errno为代表某种错误的值,对于这样一个全局变量有可能出现不一致的情况,不过这个时候更多的应该是程序员自己的责任。
4.发现错误时调用一个函数来处理:已经类似于异常处理了,其实信号句柄就是这样的函数。
异常类一般来说处理成类层次:
Class matherr{};
Class overflow:public matherr{};
Class underflow:public matherr{};
Class zerodivide:public matherr{};
Void f()
{
Try{
//...
}
Catch(overflow){/*...*/}
Catch(matherr){/*...*/}
}
这样当出现未知错误是也总能找到合适的catch块,这就类似于switch语句中的default了。
如果不处理成类层次,那么就很容易由于忘记添加相应的catch块,或者根本就不允许添加。
既然异常被处理成类层次,那么就可以有这样的处理:
Class matherr{
Virtual void debug_print(){cerr<<"math err";}
};
Calss Int_overflow:public matherr{
Const char *op;
Int a1,a2;
Public:
Int_overflow(const char* p,int a,int b){op=p;a1=a;a2=b;}
Virtual void debug_print() const{cerr<
};
不过如果像这样:
Void f()
{
Try{
g();
}
Catch(matherr m){
}
}
如果抛出的是Int_overflow,当进入catch块时,Int_overflow的信息就被丢失了,这时候需要用指针或者引用:
Int add(int x,int y)
{
If((x>0&&y>0&&x>INT_MAX-y)||(x<0&&y<0&&x
Throw Int_overflow("+",x,y);
Retrun x+y;
}
Void f()
{
Try{
Int i1=add(1,2);
Int i2=add(INT_MAX,-2);
Int i3=add(INT_MAX,2);
}
Catch(matherr& m){
M.debug_print();
}
}
如果不是用引用来捕获m,就会调用matherr::debug_print()了。
异常除了是树结构,还可以合成:
Class Netfile_err:public Network_err,public File_system_err{/*...*/};
Void f()
{
Try{
//...
}
Catch(Network_err& e){
//...
}
}
Void g()
{
Try{
//...
}
Catch(File_system_err& e){
//...
}
}
对于下面的代码:
Void f()
{
Try{
Throw E();
}
Catch(H)
{
//什么时候抛出E会进入这里?
}
}
有四种情况:
1.H与E是同一个类型;
2.H是E的unambiguous public base;
3.H和E是指针,并且其所指向的对象具备1或者2
4.H是引用,并且H所指的对象具备1或者2
此外我们还可以加上const做修饰,加不加都没有太多的影响。
理论上说当抛出一个异常时就会复制,一个句柄捕获到一个异常时有可能已经复制多次了,因此不能抛出无法复制的异常,比方说把复制拷贝函数放到private当中,同时要保证内存足够,至少要保证new可以抛出内存不足的异常bad_alloc。
在异常句柄中还可以再次抛出这个异常:
Void  h()
{
Try{
//...
}
Catch(matherr){
If(can_handle_it_completely){
//...
}else{
//...
Throw;
}
}
}
可以看到重新抛出时throw不需要操作数。如果试图重新抛出时并没有可以抛出的异常,就会调用terminate()。重新抛出的异常是原始的异常而不仅仅是部分,比如上面如果抛出的是Int_overflow,重新抛出是还是Int_overflow而不是Matherr。
Catch(...)表示捕获所有的异常,通常在这个块中需要重新抛出异常:
Void M()
{
Try{
//...
}
Catch(...){
//...
Throw;
}
}
通常这是为了保持莫些不变量。
因为异常是按照顺序逐个匹配的,因此对于有类层次的异常来说,顺序就很重要了,比如:
Void g()
{
Try{
}
Catch(...){
}
Catch(std::exception& e){
}
Catch(std::bad_cast){
}
}
Exception和bad_cast将不被考虑,因为catch(...)已经捕获了所有的异常。如果把catch(...)去掉,bad_cast也永远不会被考虑,因为bad_cast是exception的派生类。所以必须合理安排catch的顺序。
 
程序中申请了资源没有释放不一定是程序的逻辑错误,有可能是出现了某种异常。不过对于内存泄漏如果不是一个daemon程序并不是个很大的问题。
对于申请资源有这么一个技术:resource acquisition is initialization。比如可以把FILE*封装成这样:
Class File_ptr{
FILE* p;
Public:
File_ptr(const char* n,const char *a){ p=fopen(n,a);}
File_prt(FILE *pp){p=pp;}
//...
~File_ptr(){if(p)  fclose(p);}
Operator FILE*(){return p;} 
};
Void use_file(const char *f)
{
File_ptr f(fn,"r");
//use f
}
这样当use_file函数返回时,f就自动析构并且释放了相应的文件指针。
再看一个申请内存的例子:
Class Y{
Int *p;
Void init();
Public:
Y(int s){p=new[s];init();}
~Y(){delete [] p;}
};
如果init()抛出异常,由于这时候对象还未完全构造,析构函数是不会被调用的,从而造成所分配的空间没有释放也就是所谓的内存泄漏。
这种情况可以这样处理:
Class Z{
Vector p;
Void init();
Public:
Z(int s):p(s){init();}
};
因为vector是可以被自动销毁的,所以即便init()抛出异常也不会有内存泄漏出现。
标准库提供了一个模板类auto_ptr,使用上跟一个普通指针是类似的,并且在超出作用范围时自动销毁:
Void f(point p1,point p2,auto_ptr pc,shape *pb)
{
Auto_ptrp(new rectangle(p1,p2)); //注意这里的模板参数是shape,而不是shape*
Auto_ptr pbox(pb);
P->rotate(45);
If(in_a_mess) throw mess();
}
需要注意的是auto_ptr的拷贝语义的特殊性:当一个auto_ptr被复制后,这个auto_ptr将不再指向任何地方。并且因为拷贝是会修改auto_ptr,一个const auto_ptr将不能被拷贝。
比如:
Void g(circle *pc)
{
Auto_ptr p2(pc);  //now p2 is resposible for deletion
Auto_ptr p3(p2);  //now p3 is resposible for deletion(and p2 isn't);
P2->m=7; //错误
}
不止一个auto_ptr指向同一个对象是未定义的,最可能的情况就是这个对象被删除两次。
因为这样的拷贝语义,auto_ptr不适合用到标准容器以及某些标准算法中。
Auto_ptr不是smart pointer。
对于:
Void f(arena& a,X* buffer)
{
X* p1=new X;
X* p2=new X[10];
X* p3=new(buffer[10]) X; //把X放到buffer中,buffer销毁的时候X也销毁了
X* p4=new(buffer[11]) X[10];
X* p5=new(a) X; //把X放到arena中,需要用a中销毁
X* p6=new(a) X[10];
}
如果X的构造函数抛出异常,p1,p2的new()操作是不会出现内存泄漏的。但是'对于使用placement syntax的new()来说就不一定。对于P3、p4,buffer是可以自动销毁的,因此p3、p4都不会有内存泄漏。而对于p5、p6,如果arena提供了delete方法就不会泄漏,否则p5、p6就无从销毁。因此new()和delete()必须配对。
具体的看下构造函数抛出异常的例子:
Class Vector{
Public:
Class Size{};
Enum {max=32000};
Vector(int sz)
{
If(sz<0||sz>max) throw Size();
//...
}
//...
};
Vector *f(int i)
{
Try{
Vector* p=new Vector(i);
//...
Return p;
}
Catch(Vector::Size){
//...
}
}
要捕获一个成员初始化时抛出的异常,可以这样:
Class X{
Vector v;
Public:
X(int);
};
X::X(int s)
Try
:v(s)
{
}
Catch(Vector::Size){
}
对于拷贝构造函数,如果抛出异常,必须在此之前释放已经获取的资源。
对于拷贝赋值,抛出异常前必须保证两个操作数都处于合法状态。
从异常处理的角度看,一个析构函数会在两种情况下调用:
1.正常调用:离开某一作用范围,或者delete,等等
2.异常处理中调用:栈回滚(指寻找合适的异常处理句柄)的时候,由于异常处理使得程序流程离开一个作用范围,这个范围中包含的对象的析构函数被调用。
对于后面一种情况,如果析构函数抛出异常,则会被认为是异常处理失败并调用std::terminate(),因为不好判断是否可以忽略在处理一个异常时引发的另外一个异常。在这种情况下通过抛出异常来离开一个析构函数是不符合标准库要求的。
总的来说,如果要处理析构函数抛出的异常,可以这样:
X::~X()
try
{
f(); //might throw
}
Catch(...)
{
//...
}
如果一个异常抛出而尚未被捕获,则标准库函数uncaught_exception()返回true。因为一个对象被正常销毁是不存在异常的,而在栈回滚时被销毁是有异常存在的并且此时异常尚未被捕获,因此可以通过这个函数来判断一个对象是由于什么原因被销毁。
抛出异常可以作为一种退出程序流程的手段:
Void fnd(Tree* p,const string& s)
{
If(s==p->str) throw p;
If(p->left) fnd(p->left,s);
If(p->right) fnd(p->right,s);
}
Tree *find(Tree *p,const string&s)
{
Try{
fnd(p,s);
}
Catch(Tree* q){
Return q;
}
Return 0;
}
显然,这是一种相当高效率的退出递归的办法。
可以在函数声明时指定该函数可以抛出的异常的集合:
Void f(int a) throw (x2,x3);
表明这个函数只会抛出x2,x3及其派生的异常。从效果看:
Void f() throw (x2,x3)
{
}
等同于:
Void f()
Try
{
}
Catch(x2){}
Catch(x3){}
Catch(...){
Std::unexpected();  //通常就是std::terminate()
}
实际上这种句法即所谓的exception specifications是一个对调用者的保证,因此更多的使用在接口定义中。
比较:
Int f();  //can throw any exception
Int f() throw(); //no exception thrown
 
如果一个函数的某一个声明使用了exception specification,那么这个函数的每一个声明包括定义都要有同样的exception specification。
编译器并不需要跨编译单元对这种一致性做检查,并且最好不做,这样可以避免添加一个异常导致必须重新编译所有涉及的代码,有时候这个代价是相当大的而且有时候也不是所有的代码都是可以获得的。
对于虚函数只能被有同样或者更为有限的exception specification的函数覆盖:
Class B{
Public:
Virtual void f();
Virtual void g() throw(X,Y);
Virtual void h() throw(X);
};
Class D{
Public:
Virtual void f() throw(X);
Virtual void g() throw(X);
Virtual void h() throw(X,Y);  //error
};
B::f()可以抛出所有的异常,而D::f()只抛出X类的异常;
B::g()可以抛出X,Y类的异常,D::g()只抛出X类的异常;
B::h()抛出X类的异常,D::h()要抛出X,Y类的异常,这是错误的;
所以这里的更为有限指的是抛出的异常只能是更少而不能更多。否则如果一个派生类抛出一个基类函数没有公布的异常,调用者不一定能够捕获得到这个异常。
对于指针:
Void f() throw(X);
Void (*pf1)() throw(X,Y)=&f;
Void (*pf2)() throw()=&f; //error
Pf1抛出X,Y类,f只抛出X类,把f赋给pf1,可以看作f覆盖了pf1。
而Pf2没有异常抛出,把f赋给pf2就违背了前述原则。
一个很极端的例子:
Void g();
Void (*pf3)() throw(X)=&g;  //error
exception specification不能在typedef中使用:
Typedef (*PF)() throw(X);  //error
有一种技术叫做用户异常映射,有一种技术叫做恢复异常类型,如果一个异常没有被捕获,就会调用terminate(),缺省情况下terminate()会调用abort()。
剩下的就不废话了。
 
end.
阅读(1147) | 评论(0) | 转发(0) |
0

上一篇:关于模板(template)

下一篇:class hierarchies

给主人留下些什么吧!~~