这篇博客中我们主要讲述将对象出阿迪给函数或者对对象调用虚拟函数以及将一个对象作为异常抛出,之间的三个主要的区别:
1)异常对象那个在传递时总被进行拷贝:通过传值方式捕获时候拷贝了两次;
2)对象作为异常被抛出与作为参数传递给函数相比,前者类型转换比后者类型转换要少一些;
3)catch子句在进行匹配的时候是有顺序的,因此我们需要将子类异常放在上面,基类异常出现在下面。
综述:
我们传递参数或者异常的方式有三种:①传值;②传引用;③传指针。但是传递参数和传递异常时候,系统需要完成的过程却完全不同,主要原因在于:调用函数时,程序的控制权最终还会返回到函数的调用出;但当抛出异常时候,程序控制权永远不会返回到抛出异常的地方。
一、传递异常和传递参数的区别:
1)传递异常与传递参数的引用版本和值传递版本:
istream& operator(istream& s,Widget& w);
void passAndThrowWidget(){
Widget localWidget;
cin>>localWidget;
throw localWidget;
}
当传递localWidget到函数operator>>中,不需进行拷贝操作,只是将Widget&应用到localWidget而已,没有进行拷贝;现在我们来观察throw localWidget语句,不论后面catch语句中的参数是值捕获还是引用捕获,即catch(Widget&)或者catch(Widget w),这都将进行一个localWidget的拷贝操作,即传递到catch语句中的参数是localWidget的拷贝,而且必须这样做,因为如果我们不进行拷贝,localWidget离开其生存空间后,进行析构,如果将localWidget本身传递给了catch子句,子句只会接收到被析构的Widget,显然无法使用,所以C++规范强制要求作为异常抛出的对象必须被复制!!!即使被抛出的对象不会被释放,也要进行拷贝操作。见下例:
void passAndThrowWidget(){
static Widget localWidget;
cin>>localWidget;
throw localWidget;//仍然进行拷贝
}
由于拷贝动作,所以我们可以理解参数传递与抛出异常的另一个差异为:抛出异常运行速度比参数传递要慢。
异常被拷贝时,拷贝动作由拷贝构造函数完成,注意拷贝动作函数针对对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型对应的拷贝构造函数,见下例:
class Widget{...};
class SpecialWidget:public Widget{...}
void passAndThrowWidget(){
SpecialWidget localSpecialWidget;
...
Widget& rw=localSpecialWidget;
throw rw;//这里虽然rw引用的是localSpecialWidget,但是抛出的异常类型仍旧为Widget,因为编译器只会注意到rw的静态类型
}
catch块中重新抛出异常的两种方式:
1)throw
catch(Widget& w){
...
throw;
}
2)throw w;
catch(Widget& w){
...
throw w;
}
这两种异常重新抛出有什么差异呢?
第一种异常就是当前异常,无论它是什么类型,特别是如果这个异常开始就是作为SpecialWidget类型抛出的,抛出仍旧为SpecialWidget类型,即使w的静态类型仍旧是Widget类型,这是因为“throw;”时候没有进行拷贝动作。
第二种异常是新异常,类型总是Widget,因为w的静态类型是Widget。
这两者之间效率显然“throw;”效率更高,因为不用进行拷贝动作,而且抛出的不是静态类型。
在函数调用中不允许传递一个临时对象到一个非const引用类型的参数里,但是异常中就可以。
第一个catch语句将生成两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,另一个是将临时对象拷贝进w中,注意是两个;如果我们使用引用捕获异常的时候,如底下方法二和三中传递异常时候,也会对被抛出的对象进行一个拷贝,拷贝是一个临时对象,然后对拷贝进行引用,而传递参数引用时后不会生成临时对象,所以就没有拷贝的动作发生。
catch(Widget w) ...
catch(Widget& w) ...
catch(const Widget& w) ...
2)传递异常与传递参数的指针版本区别:
下面我们讨论一下通过指针抛出异常的情况:与传递异常和传递参数的引用版本和值传递版本不同,通过指针抛出异常与通过指针传递参数是相同的,即都是一个指针的拷贝被传递,但是抛出的指针不能是一个指向局部变量的指针,因为异常离开局部变量生存空间时,局部变量已经被释放。
3)函数调用者或者抛出异常者与异常捕获者之间的类型匹配过程不同:
传递参数
#include <cmath>
double sqrt(double);
int i;
double sqrtOfi=sqrt(i);//OK
传递异常:
void f(int value){
try{
if(someFunction()){
throw value;
...
}
}catch(double d){//NON-MATCH,只处理double类型的异常
...
}
...
}
PS:
使用catch块进行异常匹配的时候可以进行两种类型转换。
一种是继承类与基类间的转换:
//catch errors of type runtime_error,range_error or overflow_error
catch(runtime_error) ...
catch(runtime_error&) ...
catch(const runtime_error&) ...
//catch errors of type runtime_error*,range_error* or overflow_error*
catch(runtime_error*) ...
catch(const runtime_error*) ...
另一种是允许一个类型化指针转变成无类型指针,所以带有const void*指针的catch子句能够捕获任何类型的异常:
catch(const void*) ... //捕获任何指针类型异常
二、拷贝异常时候的顺序问题:
假设logic_error是invalid_arguement的父类,下面代码将发生什么问题:
try{
...
}catch(logic_error& ex){
...
}catch(invalid_arguement& ex){
...
}
没错,即使抛出的异常是invalid_arguement,也会被覆盖掉,因为基类异常在前面呀,异常匹配是有顺序的,哪个异常在前就先匹配哪个,所以会发生覆盖问题,因此我们需要调整顺序:
try{
...
}catch(invalid_arguement& ex){
...
}catch(logic_error& ex){
...
}