从语法上看,在函数里声明参数与在 catch 子句中声明参数几乎没有什么差别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class 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) ...

让我们先从相同点谈起。你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。

产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是 当你抛出一个异常时,控制权永远不会回到抛出异常的地方。

有这样一个函数,参数类型是 Widget,并抛出一个 Widget 类型的异常:

// 一个函数,从流中读值到 Widget 中
istream operator>>(istream& s, Widget& w);
void passAndThrowWidget()
{
  	Widget localWidget;
	cin >> localWidget; //传递 localWidget 到 operator>>
	throw localWidget; // 抛出 localWidget 异常
}

当传递 localWidget 到函数 operator»里,不用进行拷贝操作,而是把 operator»内 的引用类型变量 w 指向 localWidget,任何对 w 的操作实际上都施加到 localWidget 上。

不论通过传值捕获异常还是通过引用捕获(不能通过 指针捕获这个异常,因为类型不匹配)都将进行 lcalWidget 的拷贝操作,也就说传递到 catch 子句中的是 localWidget 的拷贝。对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行 速度比参数传递要慢。

必须这么做,因为当 localWidget 离开了生存空间后,其析构函数将被调用。如果把 localWidget 本身(而不是它的拷贝)传递给 catch 子句,这个 子句接收到的只是一个被析构了的 Widget,一个 Widget 的“尸体”。这是无法使用的。因此 C++规范要求被做为异常抛出的对象必须被复制。即使被抛出的对象不会被释放,也会进行拷贝操作。

例如passAndThrowWidget 函数声明 localWidget 为静态变量(static),

void passAndThrowWidget()
{
	static Widget localWidget;
	cin >> localWidget;
	throw localWidget;
}

当抛出异常时仍将复制出 localWidget 的一个拷贝。

即使通过引用来捕获异常,也不能在 catch 块中修改 localWidget;仅仅能修改 localWidget 的拷贝。

当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。比如以下这经过少许修改的 passAndThrowWidget:

class Widget { ... };
class SpecialWidget: public Widget { ... }; void 	passAndThrowWidget()
{
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget; // rw 引用 SpecialWidget
throw rw; //它抛出一个类型为 Widget // 的异常
}

这里抛出的异常对象是 Widget,即使 rw 引用的是一个 SpecialWidget。因为 rw 的静态类型(statictype)是 Widget,而不是 SpecialWidget。你的编译器根本没有注意到 rw 引用的是一个 SpecialWidget。编译器所注意的是 rw 的静态类型(static type)。这种行为可能与你所期 待的不一样,但是这与在其他情况下 C++中拷贝构造函数的行为是一致的。

异常是其它对象的拷贝,这个事实影响到你如何在 catch 块中再抛出一个异常。比如下 面这两个 catch 块,乍一看好像一样:

catch (Widget& w)
{
...
throw; 
}

catch (Widget& w)
{
... 
throw w;
}

这两个 catch 块的差别在于第一个 catch 块中重新抛出的是当前捕获的异常,而第二个 catch 块中重新抛出的是当前捕获异常的一个新的拷贝。

第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。 特别是如果这个异常开始就是做为 SpecialWidget 类型抛出的,那么第一个块中传递出去的还是 SpecialWidget 异常,即使 w 的静态类型(static type)是 Widget。这是因为重新抛 出异常时没有进行拷贝操作。

第二个 catch 块重新抛出的是新异常,类型总是 Widget,因为 w 的静态类型(static type)是 Widget。

一般来说,你应该用 throw 来重新抛出当前的 异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。

让我们测试一下下面这三种用来捕获 Widget 异常的 catch 子句,异常是做为passAndThrowWidgetp 抛出的:

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

一个被异常抛出的临时对象可以通过普通的引用捕获;它不需要通过指向 const 对象的引用捕获。在函数调用中不允许转递一个临时对象到一个非 const 引用类型的参数里,但是在异常中却被允许。

让我们先不管这个差异,回到异常对象拷贝的测试上来。我们知道当用传值的方式传递 函数的参数,我们制造了被传递对象的一个拷贝,并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的。当我 们这样声明一个 catch 子句时:

catch (Widget w) ... // 通过传值捕获 

会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临 时对象拷贝进 w 中。

同样,当我们通过引用捕获异常时,

catch (Widget& w) ... // 通过引用捕获
catch (const Widget& w) ... //也通过引用捕获

这仍旧会建立一个被抛出对象的拷贝:拷贝同样是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。

我们还没有讨论通过指针抛出异常的情况。不过,通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。

但你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。 Catch 子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。

对象从函数的调用处传递到函数参数里与从异常抛出点传递到 catch 子句里所采用的 方法不同,这只是参数传递与异常传递的区别的一个方面;第二个差异是在函数调用者或抛 出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。比如在标准数学库(the standard math library)中 sqrt 函数:

double sqrt(double); // from <cmath> or <math.h> 

我们能这样计算一个整数的平方根,如下所示:

int i;
double sqrtOfi = sqrt(i); 

毫无疑问,C++允许进行从int到double的隐式类型转换,所以在sqrt的调用中,i被悄悄地转变为 double 类型,并且其返回值也是 double。一般来说,catch 子句匹配异常类型时不会进行这样的转换。见下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void f(int value)
{
  try {
    if (someFunction()) {
      throw value;
    ...
} }
  catch (double d) {
    ...
}
... }

在 try 块中抛出的 int 异常不会被处理 double 异常的 catch 子句捕获。该子句只能捕 获类型真真正正为 double 的异常,不进行类型转换。因此如果要想捕获 int 异常,必须使 用带有 int 或 int&参数的 catch 子句。

不过在 catch 子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间 的转换。一个用来捕获基类的 catch 子句也可以处理派生类类型的异常。

例如在标准 C++库 (STL)定义的异常类层次中的诊断部分

这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以 及指针上:

1
2
3
4
5
catch (runtime_error) ...
catch (runtime_error&) ... 
catch (const runtime_error&) ...
catch (runtime_error*) ... 
catch (const runtime_error*) ...

第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有 const void* 指针的 catch 子句能捕获任何类型的指针类型异常:

catch (const void*) ... //捕获任何指针类型异常

传递参数和传递异常间最后一点差别是 catch 子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的 catch 子句捕获,即使同时存在 有能直接处理该派生类异常的 catch 子句,与相同的 try 块相对应。例如:

1
2
3
4
5
6
7
8
9
try {
... 
}
catch(logic_error& ex) {
 ...
}
catch(invalid_argument& ex) {
 ...
}

当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里。你可以这样说虚拟函数采用最优适合法, 而异常处理采用的是最先适合法。如果一个处理派生类异常的 catch 子句位于处理基类异常 的 catch 子句后面,编译器会发出警告。

综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。

  1. 异常对象在传递时总被进行拷贝;当通过传值方式捕获 时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝。
  2. 对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。
  3. catch 子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的 catch 将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。