代码便已运行环境:VS2012+Debug+Win32
C++的异常处理机制有3部分组成:try(检查),throw(抛出),catch(捕获)。把需要检查的语句放在try模块中,检查语句发生错误,throw抛出异常,发出错误信息,由catch来捕获异常信息,并加以处理。一般throw抛出的异常要和catch所捕获的异常类型所匹配。异常处理的一般格式为:
try
{
被检查语句
throw 异常
}
catch(异常类型1)
{
进行异常处理的语句1
}
catch(异常类型2)
{
进行异常处理的语句2
}
catch(...) // 三个点则表示捕获所有类型的异常
{
进行默认异常处理的语句
}
从语法上看,C++的异常处理机制中,在catch子句中申明参数与在函数里声明参数几乎没有什么差别。例如,定义了一个名为stuff的类,那么可以有如下的函数申明。
void f1(stuff w);
void f2(stuff& w);
void f3(const stuff& w);
void f4(stuff* p);
void f5(const stuff* p);
同样地,在特定的上下文环境中,可以利用如下的catch语句来捕获异常对象:
catch(stuff w);
catch (stuff& w);
catch(const stuff& w);
catch (stuff* p);
catch (const stuff* p);
因此,初学者很容易认为用throw抛出一个异常到catch字句中与通过函数调用传递一个参数两者基本相同。它们有相同点,但存在着巨大的差异。造成二者的差异是因为调用函数时,程序的控制权最终还会返回到函数的调用处,但是当抛出一个异常时,控制权永远不会回到抛出异常的地方。相同点就是传递参数和传递异常都可以是传值、传引用或传指针。
下面考察二者的不同点。
(1)区别一:C++标准要求被作为异常抛出的对象必须被拷贝复制。
考察如下程序。
#include <iostream>
using namespace std;
class Stuff{
int n;
char c;
public:
void addr(){
cout<<this<<endl;
}
friend istream& operator>>(istream&, Stuff&);
};
istream& operator>>(istream& s, Stuff& w){
w.addr();
cin>>w.n;
cin>>w.c;
cin.get();//清空输入缓冲区残留的换行符
return s;
}
void passAndThrow(){
Stuff localStuff;
localStuff.addr();
cin>>localStuff; //传递localStuff到operator>>
throw localStuff; //抛出localStuff异常
}
int main(){
try{
passAndThrow();
}
catch(Stuff& w){
w.addr();
}
}
程序的执行结果是:
0025FA20
0025FA20
5 c
0025F950
在执行输入操作是,实参localStuff是以传引用的方式进入函数operator>>,形参变量w接收的是localStuff的地址,任何对w的操作但实际上都施加到localStuff上。在随后的抛出异常的操作中,尽管catch子句捕捉的是异常对象的引用,但是捕捉到的异常对象已经不是localStuff,而是它的一个拷贝。原因是throw语句一旦执行,函数passAndThrow()的执行也将结束,localStuff对象将被析构从而结束其生命周期。因此需要抛出localStuff的拷贝。从程序的输出结果也可以看出在catch子句中捕捉到的异常对象的地址与localStuff不同。
即使被抛出的对象不会被释放,即被抛出的异常对象是静态局部变量,甚至是全局性变量,而且还可以是堆中动态分配的异常变量,当被抛出时也会进行拷贝操作。例如,如果将passAndThrow()函数声明为静态变量static,即:
void passAndThrow(){
static Stuff localStuff;
localStuff.addr();
cin>>localStuff; //传递localStuff到operator>>
throw localStuff; //抛出localStuff异常
}
当抛出异常时仍将复制出localStuff的一个拷贝。这表示尽管通过引用来捕捉异常,也不能在catch块中修改localStuff,仅仅能修改localStuff的拷贝。C++规定对被抛出的任何类型的异常对象都要进行强制复制拷贝, 为什么这么做,我目前还不明白。
(2)区别二:因为异常对象被抛出时需要拷贝,所以抛出异常运行速度一般会比参数传递要慢。
当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应的类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。
考察如下程序。
#include <iostream>
using namespace std;
class Stuff{
int n;
char c;
public:
Stuff(){
n=c=0;
}
Stuff(Stuff&){
cout<<"Stuff's copy constructor invoked"<<endl;
cout<<this<<endl;
}
void addr(){
cout<<this<<endl;
}
};
class SpecialStuff:public Stuff{
double d;
public:
SpecialStuff(){
d=0.0;
}
SpecialStuff(SpecialStuff&){
cout<<"SpecialStuff's copy constructor invoked"<<endl;
addr();
}
};
void passAndThrow(){
SpecialStuff localStuff;
localStuff.addr();
Stuff& sf=localStuff;
cout<<&sf<<endl;
throw sf; //抛出Stuff类型的异常
}
int main(){
try{
passAndThrow();
}
catch(Stuff& w){
cout<<"catched"<<endl;
cout<<&w<<endl;
}
}
程序输出结果:
0022F814
0022F814
Stuff’s copy constructor invoked
0022F738
catched
0022F738
程序输出结果表明,sf和localStuff的地址是一样的,这体现了引用的作用。把一个SpecialStuff类型的对象当做Stuff类型的对象使用。当localStuff被抛出时,抛出的类型是Stuff类型,因此需要调用Stuff的拷贝构造函数产生对象。在catch中捕获的是异常对象的引用,所以拷贝构造函数构造的Stuff对象与在catch块中使用的对象w是同一个对象,因为他们具有相同的地址0x0022F738。
在上面的程序中,将catch字句做一个小的修改,变成:
catch(Stuff w){…}
程序的输出结果就变成:
0026FBA0
0026FBA0
Stuff’s copy constructor invoked
0026FAC0
Stuff’s copy constructor invoked
0026FC98
catched
0026FC98
可见,类Stuff的拷贝构造函数被调用了2次。这是因为localStuff通过拷贝构造函数传递给异常对象,而异常对象又通过拷贝构造函数传递给catch字句中的对象w。实际上,抛出异常时生成的异常对象是一个临时对象,它以一种程序猿不可见的方式在发挥作用。
(3)区别三:参数传递和异常传递在类型匹配的过程不同,catch字句在类型匹配时比函数调用时类型匹配的要求要更加严格。
考察如下程序。
#include <math.h>
#include <iostream>
using namespace std;
void throwint(){
int i=5;
throw i;
}
double _sqrt(double d){
return sqrt(d);
}
int main(){
int i=5;
cout<<"sqrt(5)="<<_sqrt(i)<<endl;
try{
throwint();
}
catch(double){
cout<<"catched"<<endl;
}
catch(...){
cout<<"not catched"<<endl;
}
}
程序输出:
sqrt(5)=2.23607
not catched
C++允许从int到double的隐式类型转换,所以函数调用_sqrt(i)中,i被悄悄地转变为double类型,并且其返回值也是double。一般来说,catch字句匹配异常类型时不会进行这样的转换。可见catch字句在类型匹配时比函数调用时类型匹配的要求要更加严格。
不过,在catch字句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类见的抓换。即一个用来捕获基类的catch字句可以处理派生类类型的异常。这种派生类与基类间的异常类型转换可以作用于数值、引用以及指针。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void*指针的catch字句能捕获任何类型的指针类型异常。
(4)区别四:catch字句匹配顺序总是取决于它们在程序中出现的顺序。函数匹配过程则需要按照更为复杂的匹配规则来顺序来完成。
因此,一个派生类异常可能被处理其基类异常的catch字句捕获,即使同时存在有能处理该派生类异常的catch字句与相同的try块相对应。考察如下程序。
#include <iostream>
using namespace std;
class Stuff{
int n;
char c;
public:
Stuff(){
n=c=0;
}
};
class SpecialStuff:public Stuff{
double d;
public:
SpecialStuff(){
d=0.0;
}
};
int main(){
SpecialStuff localStuff;
try{
throw localStuff; //抛出SpecialStuff类型的异常
}
catch(Stuff&){
cout<<"Stuff catched"<<endl;
}
catch(SpecialStuff&){
cout<<"SpecialStuff catched"<<endl;
}
}
程序输出:
Stuff catched
程序中被抛出的对象是SpecialStuff类型的,本应由catch(SpecialStuff&)字句捕获,但由于前面有一个catch(Stuff&),而在类型匹配时是允许在派生类和基类之间进行类型转换的,所以最终是由前面的catch子句将异常捕获。不过,这个程序在逻辑上多少存在一些问题,因为处在前面的catch字句实际上阻止了后面的catch子句捕获异常。所以,当有多个catch字句对应同一个try块时,应该把捕获派生类对象的catch字句放在前面,而把捕获基类对象的catch子句放在后面。否则,代码在逻辑上是错误的,编译器也会发出警告。
与上面这种行为相反,当调用一个虚拟函数时,被调用的函数是由发出函数调用的对象的动态类型(dynamic type)决定的。所以说,虚拟函数采用最优适合法,而异常处理采用的是最先适合法。
综上所述,把一个对象传递给函数(或一个对象调用虚拟函数)与把一个对象作为异常抛出,这之间有三个主要区别。
第一,把一个对象作为异常抛出时,总会建立该对象的副本。并且调用的拷贝构造函数是属于被抛出对象的静态类型。当通过传值方式捕获时,对象被拷贝了两次。对象作为引用参数传递给函数时,不需要进行额外的拷贝;
第二,对象作为异常被抛出与作为参数传递给函数相比,前者允许的类型转换比后者要少(前者只有两种类型转换形式);
第三,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。
[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[P355-P364]
[2]http://blog.csdn.net/hanchaoman/article/details/5914204
[3]http://dev.yesky.com/171/2602671.shtml