异常的抛出与函数传递参数
从语法角度看,传递参数和捕获异常其简直一摸一样:1
2
3
4
5
6
7
8
9
10
11
12
13class 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 (Widget& w) ...
catch (const Widget& w) ...
catch (Widget *pw) ...
catch (const Widget *pw) ...
它们确实有相同点,但是也存在巨大差异。
- 相同点在于:传递过程都是可以传值、传递引用、传递指针。
- 不同点在于:调用函数时,程序控制权终将返回调用处,但当你抛出异常时,控制权将永远回不到抛出异常的地方。
异常与拷贝
以如下函数举例:1
2
3
4
5
6istream operator>>(istream& s, Widget& w);
void passAndThrowWidget(){
Widget localWidget;
cin >> localWidget;//传递参数
throw localWidget;//抛出异常
}
当我们传递参数时,其实是把w绑定到localwidget,任何施加于w的操作都施加于它。但throw则不同,无论通过传值还是引用,异常都会被拷贝,原因很简单,localwidget一旦离开了生存空间后析构函数就会被调用。如果我们把localwidget本身传递出去,catch只能接受到一个析构了的widget,因此异常抛出的对象必须被复制。
static异常
即使对象是static,不会被析构,抛出异常的时候也会进行复制:1
2
3
4
5void passAndThrowWidget(){
static Widget localWidget;
cin >> localWidget;
throw localWidget; // 拷贝操作
}
这表明哪怕是使用引用捕获异常,我们永远也无法通过catch来修改localwidget本身,仅仅只能修改对象的拷贝。这也证明了抛出异常与参数传递的第二个差异:抛出异常速度比参数传递要慢。
静态类型
当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型所对应类的拷贝构造函数,而不是对象的动态类型对应类的拷贝构造函数。1
2
3
4
5
6
7
8class Widget { ... };
class SpecialWidget: public Widget { ... };
void passAndThrowWidget(){
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget;
throw rw;//抛出一个类型为Widget的异常
}
catch中的异常
异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。1
2
3
4
5
6
7
8catch (Widget& w){// 捕获 Widget 异常
... // 处理异常
throw; // 重新抛出异常,让它继续传递
}
catch (Widget& w){// 捕获 Widget 异常
... // 处理异常
throw w; // 传递被捕获异常的拷贝
}
这两个catch块中,第一个catch块重新抛出的是当前捕获的异常,第二个抛出的是当前捕获的异常的一个拷贝。
第一个块中抛出的类型不需要静态动态类型,因为它没有进行拷贝操作。
第二个块中重新抛出的是新异常,其类型必然是widget.
一般来说,推荐使用throw,这样不会改变异常的类型,也不用生成额外的异常临时变量了。(临时变量详见More Effective C++ 19)
异常的捕获方式
异常一般有传值、传引用、传指向const的引用三种:1
2
3catch (Widget w) ... // 通过传值捕获异常
catch (Widget& w) ... // 通过传递引用捕获异常
catch (const Widget& w) ... //通过传递指向const的引用捕获异常
通过比对我们又发现了一大差异,我们已经知道抛出的拷贝其实是一个临时变量,而在传递参数时临时变量无法被普通引用绑定,只能通过const reference。
不同捕获方式之间的差异
传值
当我们声明这样的一个catch子句时:1
catch (Widget w) ...//传值捕获
该表达式会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,另一个是把临时对象拷贝进w中。
引用
当我们声明使用引用捕获时:1
2catch (Widget& w) ... // 通过引用捕获
catch (const Widget& w) ... //也通过引用捕获
只会建立一个拷贝,该拷贝是一个临时对象,并且被引用绑定,但我们使用引用传递参数时则不会发生拷贝。
指针
对于指针而言,传递参数与传递异常差不多,都是一个指针的拷贝被传递,只不过要记得用指针传递异常时异常必须是全局的或者在堆中,否则catch子句只能通过指针去往一个已经被析构的地方。
异常与类型转换
隐式转换
异常传递中不允许出现隐式转换,但传递参数时可以。
异常传递中允许发生的转换
第一种是派生类与基类间的转换。(is-a)
另一种就是允许从一个类型化指针转变为无类型指针,所以参数为const void*的catch语句能捕获任何类型的指针类型异常。1
catch(const void*) ...
catch语句匹配
传递函数与传递异常的差别还在于catch语句匹配不按最合适的,而是总是按照先后顺序,因此可能会发生派生类异常永远总是处理不到的情形:1
2
3
4
5
6
7
8
9try {
...
}
catch (logic_error& ex) {//base
...
}
catch (invalid_argument& ex) {//derived
...//永远不会被执行
}
虚函数采用的是最优匹配法,而异常处理采用的是最先适合法。但如果一个派生类的catch在一个基类的catch后面,通常编译器会发出警告。
总结
一个对象传递给函数,一个对象调用虚函,把一个对象做为异常抛出,它们之间有三个主要区别:
- 异常在传递时总被拷贝,传值时被拷贝了两次
- 异常的类型转换很少(2种)
- 异常采用最先匹配法