相同点:函数参数和异常的传递方式有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子句以其“出现源代码的顺序”被编译器检验比对,其中第一个匹配成功后者便执行。而当我们以某对象调用一个虚函数,被选中执行的是那个“与对象类型最佳吻合”的函数,不论是不是源代码所列的第一个。