C++ 抛出异常与传递参数的区别
发布日期:2021-06-29 19:19:38 浏览次数:4 分类:技术文章

本文共 4712 字,大约阅读时间需要 15 分钟。

代码便已运行环境:VS2017+Debug+Win32


1.C++ 异常处理基本格式

C++的异常处理机制有3部分组成:try(检查),throw(抛出),catch(捕获)。把需要检查的语句放在try模块中,检查语句发生错误,throw抛出异常,发出错误信息,由catch来捕获异常信息,并加以处理。一般throw抛出的异常要和catch所捕获的异常类型所匹配。异常处理的一般格式为:

try{	被检查语句   throw 异常}catch(异常类型1){	进行异常处理的语句1}catch(异常类型2){	进行异常处理的语句2}catch(...)    //三个点则表示捕获所有类型的异常{  	进行默认异常处理的语句}

2. 抛出异常与传递参数的区别

从语法上看,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 
using namespace std;class Stuff{ int n; char c;public: void addr() { cout<
<
>(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(); }}

程序的执行结果是:

0025FA200025FA205 c0025F950

在执行输入操作是,实参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 
using namespace std;class Stuff{ int n; char c;public: Stuff() { n=c=0; } Stuff(Stuff&) { cout<<"Stuff's copy constructor invoked"<

程序输出结果:

0022F8140022F814Stuff's copy constructor invoked0022F738catched0022F738

程序输出结果表明,sf和localStuff的地址是一样的,这体现了引用的作用。把一个SpecialStuff类型的对象当做Stuff类型的对象使用。当localStuff被抛出时,抛出的类型是Stuff类型,因此需要调用Stuff的拷贝构造函数产生对象。在catch中捕获的是异常对象的引用,所以拷贝构造函数构造的Stuff对象与在catch块中使用的对象w是同一个对象,因为他们具有相同的地址0x0022F738。

在上面的程序中,将catch子句做一个小的修改,变成:

catch(Stuff w){…}

程序的输出结果就变成:

0026FBA00026FBA0Stuff's copy constructor invoked0026FAC0Stuff's copy constructor invoked0026FC98catched0026FC98

可见,类Stuff的拷贝构造函数被调用了2次。这是因为localStuff通过拷贝构造函数传递给异常对象,而异常对象又通过拷贝构造函数传递给catch子句中的对象w。实际上,抛出异常时生成的异常对象是一个临时对象,它以一种程序猿不可见的方式在发挥作用。

(3)区别三:参数传递和异常传递的类型匹配过程不同,catch子句在类型匹配时比函数调用时类型匹配的要求要更加严格。考察如下程序。

#include 
#include
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)<

程序输出:

sqrt(5)=2.23607not 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 
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"<

程序输出:Stuff catched。

程序中被抛出的对象是SpecialStuff类型的,本应由catch(SpecialStuff&)子句捕获,但由于前面有一个catch(Stuff&),而在类型匹配时是允许在派生类和基类之间进行类型转换的,所以最终是由前面的catch子句将异常捕获。不过,这个程序在逻辑上多少存在一些问题,因为处在前面的catch子句实际上阻止了后面的catch子句捕获异常。所以,当有多个catch子句对应同一个try块时,应该把捕获派生类对象的catch子句放在前面,而把捕获基类对象的catch子句放在后面。否则,代码在逻辑上是错误的,编译器也会发出警告。

与上面这种行为相反,当调用一个虚拟函数时,被调用的函数是由发出函数调用的对象的动态类型(dynamic type)决定的。所以说,虚拟函数采用最优适合法,而异常处理采用的是最先适合法。

3.总结

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

第一,把一个对象作为异常抛出时,总会建立该对象的副本。并且调用的拷贝构造函数是属于被抛出对象的静态类型。当通过传值方式捕获时,对象被拷贝了两次。对象作为引用参数传递给函数时,不需要进行额外的拷贝;
第二,对象作为异常被抛出与作为参数传递给函数相比,前者允许的类型转换比后者要少(前者只有两种类型转换形式);
第三,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。


参考文献

[1] 陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.P355-P364

[2]
[3]

转载地址:https://dablelv.blog.csdn.net/article/details/50095643 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:C++ 抛出和接收异常的顺序
下一篇:C++ 为什么要引入异常处理机制

发表评论

最新留言

网站不错 人气很旺了 加油
[***.192.178.218]2024年04月06日 10时10分25秒