异常(Exceptions)
条款 9:利用destructors避免泄漏资源
假设存在如下的类声明
class ALA{ //abstract base class
public:
virtual void proccessAdoption() = 0;
...
};
class Puppy: public ALA{
public:
virtual void processAdoption();
...
};
class Kitte: public ALA{
public:
virtual void processAdopiton();
...
};
ALA* readALA(istream& s); // 从输入流读取单个对象
void processAdoptions(istream& dataSource){
while(dataSources){ // 不断读入数据
ALA *pa = readALA(dataSource); // 取出一个
pa->processAdoption(); // 调用对应类的处理函数
delete pa; // 删除readALA返回的对象
}
}
如果pa->processAdoption()抛出了异常,那么这个异常就会传播到processAdoptions的调用端,这就意味这pa不会被删除,就出现了资源泄漏的情况了;
void processAdoptions(istream& dataSource){
while(dataSource){
ALA *pa = readALA(dataSource);
try{
pa->processAdoption();
}
catch(...){ // 捕捉所有的异常
delete pa; // 当异常被抛出时,回收内存,避免资源泄漏
throw; // 将异常传播传播给调用端
}
delete pa; //正常情况下,避免资源泄漏
}
}
但是这导致代码维护和撰写起来十分麻烦,所以,解决办法是,以一个“类似指针的对象”(智能指针)取代指针pa,当类似指针的对象被(自动)销毁时,可以令其析构函数调用delete;
template<class T>
class auto_ptr{
public:
auto_ptr(T *p = 0): ptr(p) { }
~auto_ptr(){ delete ptr; }
private:
T *ptr;
};
void processAdopotions(istream& dataSource){
while(dataSource){
auto_ptr<ALA> pa(readALA(dataSource));
pa->processAdoption();
} //每次循环结束,pa会自动调用析构函数
}
条款 10:在构造函数内阻止资源泄漏
假设存在以下类声明:
class Image{ //图像类
public:
Image(const string& imageDataFileName);
...
};
class AudioClip{ //音频类
public:
AudioClip(const string& audioDataFileName);
...;
}
class PhoneNumber{...};
class BookEntry{ //通讯录类
public:
BookEntry(const string& name,
const string& address = "",
const string& imageFileName = "",
const string& audioClipFileName = "");
~BookEntry();
void addPhoneNumber(const PhoneNumber& number);
...
private:
string theName; //个人姓名
string theAddress; //个人地址
list<PhoneNumber> thePhones; //个人电话号码
Image *theImage; //个人照片
AudioClip *theAudioClip; //个人录音
};
类的构造函数和析构函数定义如下:
BookEntry:: BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
theName(name), theAddress(address),
theImage(0), theAudioClip(0){
if(ImageFileName != ""){ //如果有图像文件
theImage = new Image(imageFileName); //new存在抛出异常的可能
}
if(audioClipFileName != ""){ //如果有录音文件
theAduioClip = new AudioClip(audioClipFileName);//new存在抛出异常的可能
}
}
BookEntry::~BookEntry(){
delete theImage;
delete theAudioClip;
}
C++只会析构已完成构造的对象,对象只有在其构造函数完成才算是完全构造妥当,而在异常在构造过程中被抛出时,析构函数就不会被调用;
void testBookEntryClass(){
BookEntry *pb = 0;
try{
pb = new BookEntry("name", "address");
...
}catch(...){ //捕捉所有异常
delete pb; //异常抛出情况下,回收pb所指向的资源(BookEntry所分配的image对象或AudioClip对象仍然泄露了,因为构造过程未完成,pb将为null指针,delete此指针并没有任何意义)
throw;
}
delete pb; //正确情况下,回收pb所指向的资源
}
这种情况下的资源泄漏是因为构造过程中出现异常并不会调用析构函数对已构造的对象进行析构,所以智能指针也不会使得这种情况好转,所以解决方案就是在构造函数出现异常的时候不等抛出就先对异常进行处理;
class BookEntry{
public:
...
private:
...
void cleanup();
};
void BookEntry:: cleanup(){ //清理函数
delete theImage;
delete theAudioClip;
}
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(0), theAudioClip(0){
try{
...
}catch(...){
cleanup(); //构造过程中出现异常,对资源进行释放
throw; //传播异常
}
}
BookEntry:: ~BookEntry(){
cleanup(); //释放资源
}
但是如果类成员是常量指针,情况又会有所不同
class BookEntry{
public:
...
private:
string theName;
string theAddress;
Image * const theImage; //常量指针
AudioClip * const theAudioClip; //常量指针
};
BookEntry:: BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName): 0), //在成员初始化列表完成初始化,完成后不可更改
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0) //在初始化时进行判断,但是这个位置有抛出异常的可能
{ }
常量指针不可赋值,所以在初始化完成之后便不可更改了,所以,在C++的成员初始化列表中直接完成初始化动作,但是这个地方存在抛出异常的风险(new分配堆内存),而且,因为初始化列表在函数执行之前就发生了,所以函数内的异常捕捉并不能捕捉到初始化列表的异常;
解决办法一 : 使用私有成员函数(private member funtions)取代语句,在函数里捕获异常,但是缺点是处理的动作散布在数个函数中,造成维护的困难;
class BookEntry{
public:
...
private:
...
Image * initImage(const string& imageFileName); //初始化Image对象函数
AudioClip * initAudioClip(const string& audioClipFileName);//初始化AudioClip对象函数
};
BookEntry:: BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName):
theName(name), theAddress(address),
theImage(initImage(imageFileName)), theAudioClip(initAudioClip(audioClipFileName)){ }
Image * BookEntry::initImage(const string& imageFileName){ //theImage先被初始化,所以即使初始化失败也无须处理资源泄问题
if(imageFileName != "")
return new Image(imageFileName);
else
return 0;
}
AudioClip * BookEntry(const string& audioClipFileName){ //因为,theAudioClip第二个初始化,所以如果有异常抛出,必须确定将theImage资源释放掉
try{ //捕获异常
if(audioClipFileName != "")
return new AudioClip(audioClipFileName);
else
return 0;
}catch(...){
delete theImage;
throw;
}
}
解决办法二 : 将theImage 和 theAudioClip所指对象视为资源,交给局部对象来管理(条款9),当theAudioClip初始化期间有异常抛出,theImage已经是完整构造好的对象,所以它会自动销毁;
class Entry{
public:
...
private:
...
const auto_ptr<Image> theImage; //const auto_ptr对象
const auto_prt<Image> theAudioClip; //const auto_ptr对象
};
BookEntry:: BookEmntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName):
theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName) : 0 )
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0){ }
条款 11:禁止异常流出析构函数之外
两种情况下析构函数会被调用:
情况一:
在对象在正常状态下被销毁,也就是离开它的生存空间(scope)或是被明确地删除(delete 指针);
情况二:
当对象被异常处理机制(异常传播过程的栈展开机制)销毁;
参考:http://www.programlife.net/cpp-exception-stack-unwinding.html
当析构函数被调用时,可能(也可能不)有一个异常正在作用之中,如果控制器基于异常的因素离开了析构函数,那么此时就是同时处于两个异常的状态,C++就会调用terminate函数(将程序结束掉,会立刻执行,不等局部对象被销毁);
class Session{
public:
Session();
~Session();
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
};
Session:: ~Session(){ //正常情况下,如果logDestruction抛出异常,会传播到析构函数的调用端,但是如果析构函数是因为异常而调用的,程序就会终止
logDestruction(this);
}
当然使用try/catch 阻止 logDestruction抛出的异常传出Session析构函数之外,但是catch语句中的内容也是存在着抛出异常的可能性,这就导致问题回到了原点,所以,取代的办法是:阻止异常抛出析构函数之外(捕捉,但是不做任何处理)
Session:: ~Session(){
try{
logDestruction(this);
}catch(...){ // 这里仍然存在抛出异常的可能性
cerr<<...<<".\n";
}
//catch(...){ } // 捕获异常,但是不做任何动作
}
如果异常从析构函数中抛出,而且没有当场捕获,那么析构函数就会执行不全,影响到原本该完成的动作,可以通过重新安排执行顺序来解决这个问题,但是这不是最好的办法;
Session:: ~Session(){ //异常抛出时,数据库事务就不会被执行
logCreation(this); //可能抛出异常
startTransaction(); //开始一个数据库事务
}
Session:: ~Session(){ //调换了执行顺序,可以顺利执行
startTransaction(); //开始一个数据库事务
logCreation(this); //可能抛出异常
}
条款 12:了解“抛出一个异常”与 “传递一个参数”或“调用一个虚函数”之间的差异
存在以下声明:
class Widget{...}; //某个类
void f1(Widget w); //接受各种类型的函数
void f2(Widget& w);
void f3(const Widget& w);
void f4(Widget *pw);
void f5(const Widget *pw);
catch (Widget w) ... //catch 各种类型的语句
catch (Widget& w) ...
catch (const Widget& w) ...
catch (Widget *w) ...
catch (const Widget *w) ...
相同点:
函数参数和异常传递的方式有三种:传值(by value),传引用(by reference),传指针(by pointer);
以指针方式抛出异常和传指针,两者都是传递指针的副本,但是,抛出异常时,不要抛出一个指向局部变量的指针;
不同点:
1.调用函数时,控制权会回到调用端(除非函数失败),抛出异常时,控制权再也不会回到抛出端;
2.无论被捕获的异常是以传值或传引用方式传递,都会发生复制行为,即是交到catch语句手上的是那个副本,因为一旦控制权离开了原本的作用域之后,就是离开生存空间(scope)之后,于是析构函数就会被调用,所以在这之前,一个对象被抛出作为异常时,总是会发生复制(无论是否会被析构),而且复制的对象是一个临时对象;
void passAndThrowWidget(){
static Widget localWidget; //静态对象,会存在直到程序结束
throw localWidget; //依然存在复制的动作
}
当对象被复制当作一个异常时,复制行为是由对象的拷贝构造函数执行的,拷贝构造函数相对应的是对象的“静态类型”而非“动态类型”(复制动作总是以对象的静态类型为本,但是也可以以动态类型为本,进行复制,见条款25);
class Widget{...}; //基类
class SpecialWidget{...}; //派生类
void passAndThrowWidget(){
SpecialWidget sw;
Widget& rw = sw; //rw是一个SpecialWidget
throw rw; //抛出类型为Widget的异常
}
在捕获子句(catch)抛出异常的方式也有区别,会影响到对象是否复制;
catch(Widget& w){
...
throw w; //传播当前异常的副本,存在复制的动作
}
catch(Widget& w){
...
throw; //传播当前异常,不会引发复制动作
}
捕获的变量的方式不同同样会影响是否发生复制的动作,如果是传值的方式进行捕获,那么会存在两次复制的成本(一次是抛出时复制到临时对象,一次是临时对象复制到参数),而传引用只有一次(复制到临时对象);
catch(Widget w) ... //传值,会引发复制动作
catch(Widget& w) ... //传引用,不会引发复制动作,如果这里是函数调用是不被允许的(函数调用不允许将一个临时对象绑定到non-const引用上),不过,对于异常捕获来说是合法的
catch(const Widget& w) ... //传引用
3.“调用端或抛出端”被搬移到“参数或捕获子句”时,类型匹配(type match)规则有所不同,在函数调用时,允许隐式类型转换,但是在异常捕获子句中是不允许隐式类型转换的,允许两种转换:第一种,在继承体系内的转换,一个针对基类编写的捕获语句可以处理类型为派生类的异常,第二种,可以接受“有型”指针转换为“无型”指针,而且,在虚函数的调用时,采用的是最佳匹配(best fit),调用的是“调用者(某个对象)的动态类型”中的函数,而在异常捕获子句中,采用的是最先匹配(first fit),哪个捕获子句最先可以捕获就在哪里进行处理;
double sqrt(double);
int i;
double sqrtOfi = sqrt(i); //函数调用中int 隐式转换为 double
class BaseClass{...};
class DerivedClass:public BaseClass{ ... };
void f(int value){
try{
if(...){
throw value;//抛出int类型的异常
}
}
catch(double d){ //捕捉double类型的异常,然而这并不会被调用
...
}
catch(BaseClass bc){ //可以接受派生类的异常
...
}
catch(const void*){ //可以捕获任何类型的指针异常(有型可以转换为无型,而且non-const对象可以转换为const对象)
...
}
}
条款 13:以传引用方式捕捉异常
1.以指针传递异常:
问题一:如果忘记声明的的静态对象的话,那么就会造成指针指向一个不存在的对象;
问题二: catch子句的部分不知道是否应该删除获得的指针,如果是方法一(栈内存)那么不必删除,如果是方法二(堆内存),不删除就会有资源泄漏的问题;
calss exception{ ... }; //异常类
void Function(){ //方法一:使用静态类型
static exception ex; //静态类型,保证离开作用域后,指针指向的对象仍然存在
...
throw &ex;
...
}
void Function(){ //方法二:使用堆对象
...
throw new exception; //在堆内存中生产对象
...
}
void doSomething(){
...
try{
Function();
}catch(exception *ex){ ... } //捕获异常,但是并不肯定Function中编写异常的方式
}
2.以传值来传递异常
这种情况下,会导致两次的复制成本(条款12),而且还会导致有切割(slicing)问题,因为派生类异常对象被视为基类异常对象,将失去他的派生类数据成员,当虚函数在其上面被调用时,会解析为基类的虚函数;
class exception{ //基类
public:
virtual const char * what() throw(); //虚函数
...
};
class runtime_error: public exception{ .. }; //派生类
class Validation_error: public runtime_error{ //派生类
public:
virtual const * char what() throw(); //派生类虚函数
...
};
void Funciton(){
...
if( a validation test fails){
throw Validation_error(); //抛出一个派生类异常
}
...
}
void doSomething(){
try{
Function();
}catch(exception ex){ //捕捉继承体系里所有的异常(传值)
cerr << ex.what(); //调用的是exception::what(),而非Validation_error::what()
...
}catch(exception& ex){ //捕捉继承体系里所有异常(传引用)
cerr << ex.what(); //调用Validation_error::what(),因为没有切割的问题
..
}
}
所以使用catch by reference,可以免除传指针(对象删除问题)和传值(切割)的问题;
条款 14:明智运用exception specifications
编译器允许函数调用一个可能违反exception specifications的函数(要兼容之前没有exception specifications的旧代码,向前兼容),如果在运行期,函数抛出了一个并未列于其exception specifications的异常,那么程序就会异常终止(会出现未定义行为);
extern void f1(); //可以抛出任何东西
void f2() throw(int); //抛出一个int异常
void f2() throw(int){
...
f1(); //合法,甚至能抛出int以外的异常,这和声明的exception specifications不一样
...
}
避免未定义行为,如果A函数内调用了B函数,而B函数没有exception specifications,那么A函数也不要设定exception specifications,在注册回调函数时,很容易会疏忽了,可以在回调函数指针声明加上exception specifications来避免这个情况(加入编译器检查,但是并不是每个编译器都支持这个功能),或者写成宏;
typedef void(*CallBackPtr)(int eventXLocation, int eventYLocation, void *dataToPassBack); //函数指针简写
class CallBack{
public:
CallBack(CallBackPtr, fPtr, void *dataToPassBack):func(fPtr), data(dataToPassBack){ } //构造函数
void makeCallBack(int eventXLocation,int eventYLocation) const throw();
private:
CallBackPtr func; //callback发生时所要调用的函数
void *data; //传给callback函数的数据
};
void CallBack::makeCallBack(int eventXlocation, int eventYLocaton)const throw(){ //func有可能抛出违反makeCallBack的异常
func(eventXLocation, eventYlocation, data);
}
处理“系统”可能抛出的异常,常见的是bad_alloc,一般由内存分配失败时,operator new 和 operator []抛出的(条款8),当无法“阻止非预期异常发生”时,可以使用不同类型的异常取代非预期的异常;
方法一:用某个类代替非预期的异常
class Unexpectdeexception{}; //非预期异常类型
void convertUnexpected(){ //抛出非预期异常的函数
throw UnexpectedException();
}
set_unexpected(convertUnexpected); //设置convertUnexpected取代默认的unexpected函数(默认为terminate)
方法二:将非预期的异常转换为一个已知的类型(非预期函数的替代者重新抛出当前的(current)exception,这个异常会被标准类型bad_exception取代之);
void convertUnexpected(){
throw; //重新抛出当前的异常
}
set_unexpected(convertUnexpected); //设置convertUnexpected取代默认的unexpected函数
条款 15:了解异常处理的成本
异常是C++的一部分,只要程序的任一部分运用了异常,整个程序就必须支持它,否则不可能在运行时期提供正确的异常处理行为,所以,如果程序不使用异常的时候,可以通过编译器执行优化(不需要异常机制可以去除异常支持);
异常处理机制带来的成本来自try语句块(代码膨胀,执行速度下降)和抛出一个异常(速度上比正常函数返回慢),所以,在在必要的位置才使用异常,非必要的时候,不要使用异常机制,可以用分析工具分析程序(profiler)(条款16),以决定“对异常的支持”是否一个影响的因素;