More Effective C++ 条款12:了解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”之间的差异

相同点:函数参数和异常的传递方式有3种:传值、传引用、传指针。

不同点,试看以下函数,不但传递一个widget作为参数,也抛出一个Widget exception:

//此函数从一个stream中读取一个Widget
istream operator>>(istream& s, Widget& w);
void passAndThrowWidget()
{
    Widget localWidget;
    cin >> localWidget;   //将localWidget传给operator>>
    throw localWidget;    //将localWidget抛出成为一个exception
}

当localWidget引用方式传递到operator>>函数中,并没有发生异常。对w所做的任何更改,其实施加于localWidget上。这和抛出异常的情况不同,不论被捕捉的异常时以值、或引用方式传递,都不会发生localWidget的复制行为,而交到catch子句手上的正是那个副本。因为在此情况下一旦控制权离开passAndThrowWidget,localWidget便离开了其生存空间,于是localWidget析构函数被调用。如果此时以localWidget本身传递给一个catch子句的,此子句收到的将是一个被析构的Widget,一个已经仙逝的Widget。这便是C++特别要声明的,一个对象被抛出作为异常时,总是发生复制行为。

即使将函数内变量声明为static,复制行为还是会发生:

void passAndThrowWidget()
{
    static Widget localWidget; //static 会存在直到程序结束
    cin >> localWidget; 
    throw localWidget; 
}

"异常对象必定会造成复制行为"这一事实也解释了“传递参数”和“抛出异常”之间的另一个不同:后者常常比前者慢。

当对象被复制当做一个异常,复制行为是由对象的拷贝构造执行的。这个拷贝构造相应于该对象的“静态类型”而非“动态类型”。例如:

class Widget {...};
class SpecialWidget : public Widget
{  ... };
void passAndThrowWidget()
{
    SpecialWidget  localWidget; 
    ...
    Widget &rw = localWidget;  //rw代表一个SpecialWidget  
    throw rw; //抛出一个类型为Widget 的异常
}

rw虽然代表一个SpecialWidget,但编译器不关心,它只关心的是rw的静态类型。复制动作永远是以对象的静态类型为本。

异常对象是其他对象的副本这个事实,会对你如何在catch语句内传播异常带来冲击,考虑下面两个代码:

catch(Widget& w)
{
    ...
    throw;   //重新抛出此exception,使它继续传播
}

catch(Widget& w)
{
    ...
    throw w;  //传播被捕捉的异常的一个副本
}

前者重新抛出当前的异常,后者抛出的是当前异常的副本。更明确的说如果最初抛出的异常的类型是SpecialWidget,第一语句块会传播一个SpecialWidget异常——甚至虽然w的静态类型是Widget.这是因为当次异常被重新抛出时,并没有发生复制行为。第二catch语句块咋抛出一个新的异常,其类型总是Widget,因为那是w的静态类型。一般而言,你必须使用以下语句

throw;

才能重新抛出当前异常,期间让你没有机会改变异常的类型,此外还比较有效率,因为不需要产生新的异常对象。

catch(Widget w);  //值
catch(Widget &w);  //引用,在异常情况中,可将临时变量传递给参数。在函数调用中则不行
catch(const Widget &w);  //const引用

以值传递,会发生两个副本的构造代价。其中一个构造用于任何异常都会产生的临时对象身上,另一个构造用于将临时对象复制到w。

当我们以引用方式捕获一个异常,产生一次副本的构造代价。主要的构造用于任何异常都会产生的临时对象身上。

try语句中抛出的int异常绝不会被用来捕捉double异常的catch子句捕捉到。后者只能捕捉类型确确实实为double的异常,期间不会发生类型转换行为。

异常与catch子句发生匹配的过程中,仅有两种转换可以发生,第一种是继承架构中的类转化。一个处理基类异常而编写的catch子句,可以处理类型为子类的异常。这个规则可适用于传值、传引用和传指针三种。

第二个允许发生的转换是一个从“有型指针”转为“无型指针”,所以一个针对const void*指针而设计的catch子句,可捕获任何类型指针的异常。

传递参数和传播异常的最后一个不同时,catch子句总是依出现顺序做匹配尝试,因此,当try语句块中分别有针对基类而设计和针对子类而设计的catch子句,一个子类异常仍有可能被针对父类而设计的catch子句处理掉。例如:

try{
    ...
}
catch(logic_error& ex){   //捕捉所有的logic_error异常,甚至包括子类对象
    ...
}
catch(invalid_argument& ex)  //invalid_argument继承于logic_error.此语句块绝不会执行起来,都被上述句子捕获
{
    ...
}

虚函数采用最佳吻合策略,而异常属于最先吻合策略。绝对不要将针对父类而设计的catch子句放在针对子类而设计的catch子句之前。上述代码应改为:

try{
    ...
}
catch(invalid_argument& ex){   //处理invalid_argument异常
}
catch(logic_error& ex) //处理其他所有的logic_error
{
    ...
}

总结:

1.异常对象总是会被复制,如果通过传值方式捕获,它们甚至被复制两次,至于传递给函数参数的对象则不一定得复制

2.被抛出成为异常的对象,其被允许的类型转换动作,比“被传递到函数”的对象少。

3.catch子句以其“出现源代码的顺序”被编译器检验比对,其中第一个匹配成功后者便执行。而当我们以某对象调用一个虚函数,被选中执行的是那个“与对象类型最佳吻合”的函数,不论是不是源代码所列的第一个。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值