9 More Effective C++—条款12(异常的原理细节)

1 异常与函数调用

抛出异常与传递一个参数、调用一个虚函数有许多类似点:

1,某个类对象被接受
2,被接受的类对象可以选择不同的接收端,从而实现多态。
3,可以通过by-value, by-reference, by-pointer三种方式来传递类对象。

但是,实际上调用函数传递参数,与try中抛出异常,并被catch捕捉异常时完全不同的。

2 被抛出的对象总是一个副本

每当抛出exception时候,exception总会被复制。如下面的代码

// 示例1:抛出的异常为局部变量
void passThrowWidget() {
	Widget widget;
	doSomething(widget);
	
	// 抛出的对象是widget的一个副本
	// 当前作用域的widget在离开本函数时已经被销毁
	throw widget; 
}

// 示例2:抛出的异常为”静态局部变量“
void passThrowWidget() {
	static Widget widget;
	doSomething(widget);
	// 尽管本函数内的widget不会被销毁,但是抛出的widget依然是一个副本
	throw widget;
}

如上面的两个例子,无论原对象以什么形式定义,抛出的对象总是一个副本。这样做保证了,catch捕获的对象总能存在,否则可能导致捕获的异常对象已经被销毁。

3 传递抛出

被抛出的异常对象会调用其”复制构造函数”,复制构造函数以“静态类型”为模板创建。若基类引用指向派生类对象,则异常将会调用基类复制构造函数被创建。如下面代码所示。

class Widget;
class ChildWidget : public Widget {
}
void passThrowWidget() {
	ChildWidget child;
	Widget &widget = child;
	throw widget;  // 调用Widget的复制构造函数进行复制,而不是ChildWidget
}

基于上面原因,下面两种方式的异常抛出会带来不同的结果。第一种方式不会复制异常w,而是直接继续抛出;而第二种方式,会将w复制一遍,然后将新复制的异常抛出。第二种方式会带来两个问题:

1,效率降低
2,传递抛出的对象可能是基类对象,不符合原有目的。

// 方式1
catch (Widget w) {
	throw;
}
// 方式2
catch (Widget w) {
	throw w;
}
// 重新抛出的可能是基类对象,不符合原有目的。
catch (Widget w) {
	WidgetBase &base = w;
	throw base;
}

4 catch效率

被抛出的异常对象是一个临时变量,我们不能对传入函数中的临时变量进行修改,因此,接受临时变量的函数参数只能有下面第1、3种形式。

但是传入catch中的异常可以有如下三种形式,即异常捕获允许修改传入的临时变量。

1,catch (Widget w)
2,catch (Widget &w)
3,catch (const Widget &w)

如果采用第1中方式catch异常,则抛出异常将会被复制两次。第一次是在抛出时,第二次catch时。因此,高效做法是采用引用的方式(第2、3种方式)catch异常。

5 异常的指针

指针也可以当作异常被接受,与上面复制类道理相同,抛出异常时,指针将会被复制。由于离开作用域后,局部变量会被销毁,因此不能抛出一个局部变量的指针。如下面代码。

void func() {
	Widget error;
	static Widget right;
	throw &error; // 错误,离开作用域后就会被销毁
	throw &right;  // 正确,离开作用域后不会被销毁,复制指向right的指针
}
catch (Widget *e) { // 接受复制过来的指针
	...
}

6 类型匹配

对于虚函数的多态,或者函数的重载,如果有多个函数可供选择,函数会选择最适合的一个。如果实在找不到适合的,也会进行类型转换。具体如下面所述

1,虚函数采用继承体系的类型匹配。当前类对象拥有被调用函数,则使用。如果没有,则使用基类的被调用函数。
2,使用函数重载,找到参数类型最匹配的函数。若找不到,则进行类型转换,如int转换成double类型。

但是,catch与函数调用不同。主要有:

1,catch不会进行基本类型转换,如int转换成double类型。其类型转换仅局限于继承体系之间的转换。如子类可以转换成基类。
2,所有有型指针都是五型指针的子类。即任何指针都可以转换成void*。
3,catch传入参数类型遵循优先原则,而非最佳原则。当基类的catch比子类的catch出现早时,则基类catch捕获异常。

上面条款对应如下

class BaseClass {
}
class DerivedClass : public BaseClass {
}
void func() {
	static DerivedClass derived;
	throw &derived;
}
catch (void *e) { //优先捕获,因此void*是所有类型的基类指针
}
catch (BaseClass *e) {
} 
catch (DerivedClass *e) {
}
	

猜你喜欢

转载自blog.csdn.net/zhizifengxiang/article/details/83305741
今日推荐