析构函数不要抛出异常。

构造函数可以抛出异常,但是要谨慎。

原因下面这篇文章讲的不错,转载如下:

http://jarfield.iteye.com/blog/811703

写Java代码的时候,遇到错误总是喜欢抛出异常,简单实用。最近开始写C++代码,发现异常没那么简单,使用须谨慎。

翻阅了《Effective C++》 《More Effective C++》《Inside The C++ Object Model》的相关章节,大概弄明白了一些东东,总结在本文。

本文不是总结普适的C++异常机制,还没有这个内力哈! 主要是结合构造函数和析构函数,来总结异常对他俩的影响。构造函数和析构函数本来就很折磨脑筋,再叠加上异常机制,确实比较复杂。

异常与析构函数

本节内容较少,因此先说。构造函数放到下一节讨论。

绝对不要将异常抛出析构函数

这一条在《Effective C++》 《More Effective C++》中均被作为独立章节讲解,可见其重要性。

有一点不要误解:析构函数的代码当然可以throw异常,只是这个异常不要被抛出析构函数之外。如果在析构函数中catch住异常,并且不再抛出,这就不会带来问题。

至于原因,有两点。我们先看第一点。

异常被抛出析构函数之外,往往意味着析构函数的工作没有做完。如果析构函数需要释放一些资源,异常可能导致资源泄露,使得程序处于一个不安全的状态。

如下面的伪代码所示,异常导致p不能free,从而造成内存泄露。

  1. class A
  2. {
  3. public:
  4. ~A()
  5. {
  6. throw exception;
  7. free(p);
  8. }
  9. };

OK,这个问题好办,我好好写代码,确保析构函数释放所有的资源之后,才抛出异常。这还不行吗?

  1. class A
  2. {
  3. public:
  4. ~A()
  5. {
  6. free(p);
  7. throw exception;
  8. }
  9. };

嗯,确实不行。我们来看第二个原因。

如果两个异常同时存在:第一个异常还没有被catch,第二个异常又被抛出,这会导致C++会调用terminate函数,把程序结束掉!

这简直是灾难,远比资源泄漏要严重。

那么,什么时候会同时出现两个异常呢?看下面的代码。

  1. void f()
  2. {
  3. A a;   // 没错,就是前面的class A
  4. throw exception;
  5. }

f()抛出异常后,会进行stack-unwinding。在这个过程中,会析构所有的active local object。所谓active local object,就是已经构造完成的局部对象,例如上面的对象a。

调用a的析构函数时,(第一个)异常还没有被catch。可是a的析构函数也抛出了(第二个)异常。这时,两个异常同时存在了。程序会毫不留情地结束!

这个理由足够充分了:再也不要让异常逃离你的析构函数!

异常与构造函数

构造函数本来就是一件难以琢磨的东东,背后做了很多事情:成员对象的构造、基类成分的构造、虚表指针的设置等。这些事情本来就很纠结了,再让构造函数抛出异常,会出现怎样的悲剧呢?

有一点比较安慰:异常即使被抛出构造函数之外,也不会造成程序结束。那么,是否存在资源泄漏的问题呢?不可一概而论,我们分情况分析。

对象自身的内存如何释放

对象有可能在栈上,也可能在堆上,我们分两种情况讨论。

  1. // 对象在栈上
  2. f()
  3. {
  4. A a;
  5. }
  6. // 对象在堆上
  7. f()
  8. {
  9. A * a = new A();
  10. }

如果对象是在栈上,那么函数退栈自然会释放a占用的空间,无需多虑。

如果对象是在堆上,我们还得两种情况讨论:

  1. 如果是new运算符抛出的异常,那么堆空间还没有分配成功,也就无需释放
  2. 如果是构造函数抛出的异常,堆空间已经分配成功,那么编译器会负责释放堆空间(Inside The C++ Object Model, p301)

可见,对象本身的内存,是不会泄露的。

成员对象和基类成分怎么办

成员对象和基类成分的内存,会随着对象自身内存的释放而被一起释放,没什么问题。

但是,有一点需要谨记:如果一个对象的构造函数抛出异常,那么该对象的析构函数不会被调用。

原因很简单:如果对象没有被构造完整,析构函数中的某些代码可能会有风险。为了避免这类意外问题,编译器拒绝生成调用析构函数的代码。

那么,成员对象的基类成员对象的析构函数,会被调用吗?如果不会调用,则可能出现资源泄漏。答案是,会被调用。见下面的代码。

  1. class B : class C
  2. {
  3. A a;
  4. A * pa;
  5. public:
  6. B()
  7. {
  8. pa = new A();
  9. }
  10. ~B()
  11. {
  12. delete pa;
  13. }
  14. };

如果B的构造函数抛出异常,编译器保证:成员对象a的析构函数、基类C的析构函数会被调用(Inside The C++ Object Model, p301)。

成员指针怎么办

注意上述代码中的pa,它指向一块堆空间,由于B的析构函数不会被调用了,内存就会出现泄漏。

这还真是一个问题,编译器也不能帮我们做更多事情,只能由程序员自己负责释放内存。

我们可能要这样写代码

  1. class B : class C
  2. {
  3. A a;
  4. A * pa;
  5. public:
  6. B()
  7. {
  8. pa = new A();
  9. try {
  10. throw exception;
  11. } catch(...)
  12. {
  13. delete pa; //确保释放pa
  14. throw;
  15. }
  16. }
  17. ~B()
  18. {
  19. delete pa;
  20. }
  21. };

这样的代码难看很多,有一种建议的做法就是:用智能指针包装pa。智能指针作为B的成员对象,其析构函数是可以被自动调用的,进而释放pa。

析构函数如何被自动调用

上面提到:

  1. 普通函数抛出异常时,所有active local object的析构函数都会被调用
  2. 构造函数抛出异常时,所有成员对象以及基类成分的析构函数都会被调用

那么,这是怎么实现的呢?

我们以第一种情况为例,分析实现细节。看下面的代码:

  1. f()
  2. {
  3. A a1;
  4. if (...) {  // 某些条件下,抛出异常
  5. throw exception;
  6. }
  7. A a2;
  8. throw exception; // 总会抛出异常
  9. }

如果L5抛出异常,那么对象a1会被析构。如果L8抛出异常,那么对象a1 a2都要被析构。编译器是怎么知道,什么时候该析构哪些对象的呢?

支持异常机制的编译器,会做一些”簿记“工作,将需要被析构的对象登记在特定的数据结构中。编译器将上述代码分成不同的区段,每个区段中需要被析构的对象,都不相同。

例如,上述代码中,L3 L4~L7 L8就是三个不同的区段:

  1. 如果L3抛出异常,那么没有对象需要析构
  2. 如果L4~L7抛出异常,那么a1需要被析构
  3. 如果L8抛出异常,那么a1和a2都要析构

编译器通过分析代码,簿记这些区段以及需要析构的object list。运行时,根据异常抛出时所在的区段,查找上述的数据结构,就可以知道哪些对象需要被析构。

构造函数抛出异常时,成员对象及基类成分被析构的原理,是类似的。在C++运行时看来,构造函数只是普通的函数而已。

总结

C++的异常机制,给编译器和运行时均带来了一定的复杂度和代价。上述的”簿记“工作,只是冰上一角。

关于异常的使用,也有很多坑。怎么throw 怎么catch,都是有讲究的。有空下次再做总结。

C++异常与析构函数及构造函数的更多相关文章

  1. EC读书笔记系列之4:条款8 别让异常逃离析构函数

    条款8 别让异常逃离析构函数 记住: ★析构函数绝对不要吐出异常.若一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序. ★若客户需对某个操作函数运行期间 ...

  2. Effective C++_笔记_条款08_别让异常逃离析构函数

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码 ...

  3. Effective C++ 条款08:别让异常逃离析构函数

    1.别让异常逃离析构函数的原因 <Effective C++>第三版中条款08建议不要在析构函数中抛出异常,原因是C++异常机制不能同时处理两个或两个以上的异常.多个异常同时存在的情况下, ...

  4. php 析构函数,构造函数

    php 析构函数,构造函数   <?php /** * 测试使用的PHP操作类 * Date: 2017/7/13 * Time: 14:22 */class Test{ /** 姓名 */ p ...

  5. 《Effective C++》——条款08:别让异常逃离析构函数

    考虑如下代码: class Widget{ public: ... ~Widget(){...}//假设这个可能吐出一个异常 }; void doSomething() { std::vector&l ...

  6. Effective C++ 条款八 别让异常逃离析构函数

    class DBConn //这个class用来管理DBConnction对象 { public:   //自己设计一个新的DBConn接口 方法3 void close() { db.close() ...

  7. c++之别让异常逃离析构函数

    关于 本文代码演示环境: VS2017. 代码写的够不规范,目的是为了缩短文章篇幅. 本文主要是为了加深印象,写了好多次的代码,还是忘记了这茬.... 之前上传到github的代码会慢慢改过来. 本文 ...

  8. C++异常以及异常与析构函数

    1. 抛出异常 1.1 抛出异常(也称为抛弃异常)即检测是否产生异常,在C++中,其采用throw语句来实现,如果检测到产生异常,则抛出异常. 该语句的格式为: throw 表达式; 如果在try语句 ...

  9. EC笔记,第二部分:8.别让异常逃离析构函数

    1.为何析构函数不应该抛出异常?    有两种情况:    1).假设析构函数中有众多语句,而第一条语句抛出异常(或者其他语句),那么抛出异常以后的语句就得不到执行.而通常我们在析构函数中写的是清理资 ...

随机推荐

  1. VS2013 EF6连接MySql

    1.安装mysql server下载地址 http://cdn.mysql.com/Downloads/MySQL-5.6/mysql-5.6.21-winx64.zip 2.安装MySql的VS插件 ...

  2. HDU 1257(最小拦截系统)

    Description 某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不 能超过前一发的高度.某天,雷达 ...

  3. 10.2 Hibernate持久层

    点击项目右键->MyEclipse->Add Hibernate Capabilities 打开MyEclipse Hibernate Perspective(MyEclipse Hibe ...

  4. 2018亚洲区预选赛北京赛站网络赛 D.80 Days 尺取

    题面 题意:你带着K元要去n个城市,这n个城市是环形的,你可以选择任意一个起点,然后顺时针走,对于每个城市,到达时可以获得a元,但是从这里离开又需要花费b元,问你能否找到一个起点(输出花钱最少的那个) ...

  5. ROS-launch文件标签解读

    前言:ROS提供了一个同时启动节点管理器(master)和多个节点的途径,即使用启动文件(launch file).事实上,在ROS功能包中,启动文件的使用是非常普遍的.任何包含两个或两个以上节点的系 ...

  6. .NET中使用反射访问属性方法

    .net所编写的程序集包含两个重要部分:IL(中间语言代码) 和metadata(元数据).我们编写的代码中不是有很多很多的类吗,类有很多很多的成员,在编译代码的时候,元数据表就根据代码把类的所有信息 ...

  7. WinForm进程 线程

    进程主要调用另一程序,线程 分配工作. 一.进程: 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动.它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体.Process 类,用来操 ...

  8. [原创]一道基本ACM试题的启示——多个测试用例的输入问题。

    Problem Description 输入两点坐标(X1,Y1),(X2,Y2),计算并输出两点间的距离. Input 输入数据有多组,每组占一行,由4个实数组成,分别表示x1,y1,x2,y2,数 ...

  9. Dependency Injection in ASP.NET MVC

    原文引自http://www.dotnetcurry.com/ShowArticle.aspx?ID=786 1.传统三层结构,相邻两层之间交互: 2.如果使用EntityFramework则View ...

  10. Linux 安装MySQL5.7.18

    https://dev.mysql.com/downloads/mysql/Linux-Generic md5sum mysql-5.7.18-linux-glibc2.5-x86_64.tar.gz ...