zoukankan      html  css  js  c++  java
  • C++异常与析构函数及构造函数

    析构函数不要抛出异常。

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

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

    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,从而造成内存泄露。

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

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

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

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

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

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

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

    Java代码  收藏代码
    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的析构函数也抛出了(第二个)异常。这时,两个异常同时存在了。程序会毫不留情地结束!

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

    异常与构造函数

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

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

    对象自身的内存如何释放

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

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

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

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

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

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

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

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

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

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

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

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

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

    成员指针怎么办

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

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

    我们可能要这样写代码

    Cpp代码  收藏代码
    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.   
    18.     ~B()  
    19.     {  
    20.         delete pa;  
    21.     }  
    22. };  

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

    析构函数如何被自动调用

    上面提到:

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

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

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

    Java代码  收藏代码
    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,都是有讲究的。有空下次再做总结。

  • 相关阅读:
    logstash 字段引用
    Filter Conditions 过滤条件
    Not found org.springframework.http.converter.json.MappingJacksonHttpMessageConve
    rsyslog Properties
    rsyslog 模板
    rsyslog 基本结构
    awk RS ORS
    elasticsearch分布式特点
    spring事物配置,声明式事务管理和基于@Transactional注解的使用
    myBatis:事务管理
  • 原文地址:https://www.cnblogs.com/charlesblc/p/6367596.html
Copyright © 2011-2022 走看看