代码便已运行环境: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 <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)决定的。所以说,虚拟函数采用最优适合法,而异常处理采用的是最先适合法。

3.总结

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


参考文献

[1] 陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.P355-P364
[2] c++异常处理机制
[3] C++中理解“传递参数”和异常之间的差异

C++ 抛出异常与传递参数的区别相关推荐

  1. 【Qt】信号和槽对值传递参数和引用传递参数的总结

    在同一个线程中 当信号和槽都在同一个线程中时,值传递参数和引用传递参数有区别: 值传递会复制对象:(测试时,打印传递前后的地址不同) 引用传递不会复制对象:(测试时,打印传递前后的地址相同) 不在同一 ...

  2. qt槽函数如何传递多个参数_【Qt】信号和槽对值传递参数和引用传递参数的总结...

    在同一个线程中 当信号和槽都在同一个线程中时,值传递参数和引用传递参数有区别: 值传递会复制对象:(测试时,打印传递前后的地址不同) 引用传递不会复制对象:(测试时,打印传递前后的地址相同) 不在同一 ...

  3. service和doXX方法区别、Http请求头案例、获取请求的传递参数

    一.service和doXX方法区别: 1.注意:tomcat服务器首先会调用servlet的service方法,然后在service方法中再根据请求方式来分别调用对应的doXX方法. 2.例如,如果 ...

  4. 以下是一个使用 VBA 的例子,演示了 ByVal 和 ByRef 的区别,以及如何在函数中传递参数和返回值。

    以下是一个使用 VBA 的例子,演示了 ByVal 和 ByRef 的区别,以及如何在函数中传递参数和返回值: Sub Example()Dim x As Integer, y As Integerx ...

  5. JSP include 和 jsp:include 的区别以及使用include动作指令传递参数

    在javaweb中有两个include指令 一个是编译指令<% @ include file="fileName"%>,要知道JSP文件最终会被转换成Servlet执行 ...

  6. JS与PHP向函数传递可变参数的区别

    # JS 调用函数传递可变参数的方法 <script> function test() { for(var i =0;i < arguments.length; i++) { ale ...

  7. 按值传递和按引用传递的区别_c++按值、地址、引用传递参数

    在现实生活中,"地址"指的是我们居住在某条街上的某个小区的某栋某楼某室: 而在计算机中,"地址"指的是一个.一些数据在内存中储存的位置.比如我们之前讲到的指针, ...

  8. 后台获取前台传递参数为null和空字符串的区别,以及sql拼接之if判断

    1.获取到的值为null 当URL路径中没有"name"属性,此时后台使用request.getParameter("name")获取到的值为null; 2.获 ...

  9. ref和out 传递参数(C#)

    1.参数传递默认都是传递栈空间里面存储的内容 2.如果添加了ref那么传递的都是栈空间地址,而不再是栈空间里面的内容 3.如果添加了out,那么传递的也是栈空间的地址 1 //写一个方法计算一个int ...

最新文章

  1. 硅谷顶级VC发声:AI技术公司毛利实在太低,人工和算力成本太高
  2. 步进电机控制芯片_STK682/步进电机_STK682-010-E控制芯片 原创中文翻译
  3. python求最大值最小值求和_python3.2求和与最值
  4. 使用Windows自带工具校验文件MD5
  5. 视觉SLAM笔记(62) 单目稠密重建
  6. 操作系统产品密钥查看方法
  7. python-----异常处理
  8. 如何快速删除pdf中某一页?
  9. JAVA生成跳转指定页面并且附带参数的二维码
  10. php基于浏览器的linux终端模拟器,回顾最佳的9款Linux终端模拟器
  11. 从表征到行动---意向性的自然主义进路(续六)
  12. 仿淘宝实现鼠标移入图片,图片放大功能
  13. [DataAnalysis]参数假设检验和分布拟合检验
  14. 中国桑叶提取物市场投资分析及需求前景预测报告2022-2027年
  15. Win10如何重置ClearType设置,使其恢复系统默认值?
  16. 【功能业务篇】APP授权微信登录、绑定账号测试思考
  17. 东芝将在家电和半导体等部门裁员约1万人
  18. 如何实现回收站数据恢复
  19. C++常用的11种设计模式
  20. windows 剪贴板_如何在Windows 10上清除剪贴板历史记录

热门文章

  1. 多线程篇三:线程同步
  2. 嵌入式linux 项目开发(一)——HTML编程
  3. Photoshop CS5的序列号
  4. 初学者,你应当如何学习C++以及编程-转
  5. 蓝桥杯 ALGO-100 算法训练 整除问题
  6. 1065. 单身狗(25)-PAT乙级真题
  7. presentViewController:navigationController animated:YES completion:^(void)
  8. LeetCode199. Binary Tree Right Side View
  9. 蓝桥杯 ALGO-30算法训练 入学考试(01背包,动态规划)
  10. Linux修改主机名称