//...
//...
//...
//...
//...
//...
//...
//...
//...
程序中申请了资源没有释放不一定是程序的逻辑错误,有可能是出现了某种异常。不过对于内存泄漏如果不是一个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()。
剩下的就不废话了。