函数参数和exception的传递方式有三种:by value,by reference,by pointer。然而视你所传递的是参数或exception,发生的事情完全不同。
原因:当你调用一个函数,控制权最终会回到调用端(除非函数失败以至于无法返回),但当你抛出一个exception,控制权不会再回到抛出端。
有这样的一个函数,参数类型是Widget,并抛出一个Widget类型的异常:
//一个函数,从流中读值到Widget中
istream operator >>(istream& is,Widget& w);
void passAndThrowWidget()
{
Widget localWidget;
cin >> localWidget;
throw localWidget;
}
当传递localWidget到函数operator>>里,不用进行拷贝操,而是把operator>>内的引用类型变量w指向了localWidget,任何对w的操作实际都实施到localWidget本身上。这与抛出localWidget异常有很大不同。不论通过传递值捕获异常还是通过 引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行localWidget的拷贝操作,也就是说传递到catch子句中的localWidget是拷贝。必须这么做,因为当localWidget离开生存空间后,其析构函数将被调用。如果localWidget本身传递给catch子句,这个子句接收到的时候被析构了的Widget,一个Widget的“尸体”。这是无法使用的。因此C++规定要求被作为异常抛出的对象必须被复制。
即时被抛出的对象不会被释放,也会进行拷贝操作。例如passAndThrowWidget函数生命localWidget为静态量(static):
void passAndThrowWidget()
{
static Widget localWidget;
cin >> localWidget;
throw localWidget;
}
当抛出异常仍将复制出localWidget的一个拷贝。这表示即使通过引用来捕获异常,也不能在catch中修改localWidget;仅仅能修改localWidget的拷贝。参数传递和抛出异常第二个差异:抛出异常运行速度比参数传递慢。
当异常对象被拷贝时,拷贝操作由拷贝构造函数完成。该拷贝构造函数是静态类型的所对应的拷贝构造函数,而不是动态类型对应的拷贝构造函数。例如:
class Widget{};
class SpecialWidget:public Widget{}
void passAndThrowWidget()
{
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget; //rw引用了localSpecialWidget,
thread rw; //它抛出的异常是Widget
}
这里抛出的异常是Widget,即使rw引用了SpecialWidget,因为rw的静态类型是Widget。
异常类型其对象的拷贝,这影响你如何在catch块中再抛出异常,比如下面的两个catch块:
catch(Widget& w)
{
...
throw; //捕获异常重新抛出
}
catch(Widget& w)
{
...
throw w; //捕获异常传递的是拷贝
}
第一个catch重新抛出的当前异常,无论它是什么类型,如果w一开始是SpecialWidget,传递的还是SpecialWidget。
第二个catch重新抛出异常的拷贝,类型总是Widget。
异常生成的拷贝是一个临时对象。
如下有三种捕获Widget异常的的catch子句,异常是作为passAndThrowWidget抛出的:
catch(Widget w);
catch(Widget& w);
catch(const Widget w);
当第一个语句时候,会建立两个被抛出对象的拷贝,一个是所有异常的都必须建立的临时对象,第二个是把临时对象拷贝w中。(两次拷贝)
第二、第三语句的时候,只会建立临时对象。。
当抛出一个异常时,系统构造的被抛出对象的拷贝数比相同的对象作为参数传递给函数的构造的的拷贝数要多一次。
函数调用或抛出异常或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。
比如标准数学库的sqrt函数:
double sqrt(double);
我们能这样计算一个整数的平方根:
int i ;
...
double sqrOfi = sqrt(i);
C++允许把int到double隐式转换,所以i被悄悄变为double,并且其返回值也是double,一般来说catch子句匹配异常类型时不会进行这样的转换:
void f(int value)
{
try{
if(someFunction())
throw value; //抛出的是int
}
catch(double d) //只能处理double的异常
{
...
}
...
}
上述只能捕获double类型异常,而抛出的是int类型的异常,因此要捕获int类型的必须使用int类型或者int&类型参数的catch子句。
不过在catch子句进行异常匹配可以进行两种转换。第一种就是继承类和基类之类的转换,一个用来捕获基类的catch子句也可以用来处理派生类类型的异常。
这种派生类和基类间的异常类型转换可以用于数值、引用以及指针上面。
第二种允许从一个类型化指针转换成无类型指针,所以带有const void*的catch子句可以捕获任何类型的指针类型异常:
catch(const void*); //捕获任何指针类型的异常
传递参数和传递异常最后一个区别点:catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的catch子句捕获,即使同时存在有可能直接处理该派生类异常的catch子句,与相对应的try。例如:
try{
...
}
catch(logic_error& ex) //这个讲捕获所有的logic_error异常
{
...
}
catch(invalid_argument& ex) //这个块永远捕获被执行,invalid_argument
{ //是logic_error子类
}
与上面这种行为相反的,当你调用一个虚函数,被调用的函数位于与发生函数调用的对象的动态类型最相近的类里。即:虚函数采用最优法,而异常处理采用最先法。
所以上述代码应该先捕获invalid_argument,再捕获logic_error。
综上所述,把一个对象传递给函数或一个对象调用虚函数与一个对象作为异常抛出主要有三点区别:
- 异常对象在传递时总进行拷贝,当通过传值方式捕获异常时,异常对象被拷贝了两次。对象作为参数传递给函数不一定需要被拷贝。
- 对象作为异常被抛出与参数传递给函数相比,前者类型转换比后者少(只有两种转换)。
- catch子句在进行异常类型匹配的顺序是它们在源码中出现的顺序,第一个类型匹配成功的catch被执行。当一个对象调用一个虚函数,被选择的函数位于与对象类型匹配最佳的类里,即使该类不在源代码的最前头。