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,从而造成内存泄露。
- class A
- {
- public:
- ~A()
- {
- throw exception;
- free(p);
- }
- };
OK,这个问题好办,我好好写代码,确保析构函数释放所有的资源之后,才抛出异常。这还不行吗?
- class A
- {
- public:
- ~A()
- {
- free(p);
- throw exception;
- }
- };
嗯,确实不行。我们来看第二个原因。
如果两个异常同时存在:第一个异常还没有被catch,第二个异常又被抛出,这会导致C++会调用terminate函数,把程序结束掉!
这简直是灾难,远比资源泄漏要严重。
那么,什么时候会同时出现两个异常呢?看下面的代码。
- void f()
- {
- A a; // 没错,就是前面的class A
- throw exception;
- }
f()抛出异常后,会进行stack-unwinding。在这个过程中,会析构所有的active local object。所谓active local object,就是已经构造完成的局部对象,例如上面的对象a。
调用a的析构函数时,(第一个)异常还没有被catch。可是a的析构函数也抛出了(第二个)异常。这时,两个异常同时存在了。程序会毫不留情地结束!
这个理由足够充分了:再也不要让异常逃离你的析构函数!
异常与构造函数
构造函数本来就是一件难以琢磨的东东,背后做了很多事情:成员对象的构造、基类成分的构造、虚表指针的设置等。这些事情本来就很纠结了,再让构造函数抛出异常,会出现怎样的悲剧呢?
有一点比较安慰:异常即使被抛出构造函数之外,也不会造成程序结束。那么,是否存在资源泄漏的问题呢?不可一概而论,我们分情况分析。
对象自身的内存如何释放
对象有可能在栈上,也可能在堆上,我们分两种情况讨论。
- // 对象在栈上
- f()
- {
- A a;
- }
- // 对象在堆上
- f()
- {
- A * a = new A();
- }
如果对象是在栈上,那么函数退栈自然会释放a占用的空间,无需多虑。
如果对象是在堆上,我们还得两种情况讨论:
- 如果是new运算符抛出的异常,那么堆空间还没有分配成功,也就无需释放
- 如果是构造函数抛出的异常,堆空间已经分配成功,那么编译器会负责释放堆空间(Inside The C++ Object Model, p301)
可见,对象本身的内存,是不会泄露的。
成员对象和基类成分怎么办
成员对象和基类成分的内存,会随着对象自身内存的释放而被一起释放,没什么问题。
但是,有一点需要谨记:如果一个对象的构造函数抛出异常,那么该对象的析构函数不会被调用。
原因很简单:如果对象没有被构造完整,析构函数中的某些代码可能会有风险。为了避免这类意外问题,编译器拒绝生成调用析构函数的代码。
那么,成员对象的基类成员对象的析构函数,会被调用吗?如果不会调用,则可能出现资源泄漏。答案是,会被调用。见下面的代码。
- class B : class C
- {
- A a;
- A * pa;
- public:
- B()
- {
- pa = new A();
- }
- ~B()
- {
- delete pa;
- }
- };
如果B的构造函数抛出异常,编译器保证:成员对象a的析构函数、基类C的析构函数会被调用(Inside The C++ Object Model, p301)。
成员指针怎么办
注意上述代码中的pa,它指向一块堆空间,由于B的析构函数不会被调用了,内存就会出现泄漏。
这还真是一个问题,编译器也不能帮我们做更多事情,只能由程序员自己负责释放内存。
我们可能要这样写代码
- class B : class C
- {
- A a;
- A * pa;
- public:
- B()
- {
- pa = new A();
- try {
- throw exception;
- } catch(...)
- {
- delete pa; //确保释放pa
- throw;
- }
- }
- ~B()
- {
- delete pa;
- }
- };
这样的代码难看很多,有一种建议的做法就是:用智能指针包装pa。智能指针作为B的成员对象,其析构函数是可以被自动调用的,进而释放pa。
析构函数如何被自动调用
上面提到:
- 普通函数抛出异常时,所有active local object的析构函数都会被调用
- 构造函数抛出异常时,所有成员对象以及基类成分的析构函数都会被调用
那么,这是怎么实现的呢?
我们以第一种情况为例,分析实现细节。看下面的代码:
- f()
- {
- A a1;
- if (...) { // 某些条件下,抛出异常
- throw exception;
- }
- A a2;
- throw exception; // 总会抛出异常
- }
如果L5抛出异常,那么对象a1会被析构。如果L8抛出异常,那么对象a1 a2都要被析构。编译器是怎么知道,什么时候该析构哪些对象的呢?
支持异常机制的编译器,会做一些”簿记“工作,将需要被析构的对象登记在特定的数据结构中。编译器将上述代码分成不同的区段,每个区段中需要被析构的对象,都不相同。
例如,上述代码中,L3 L4~L7 L8就是三个不同的区段:
- 如果L3抛出异常,那么没有对象需要析构
- 如果L4~L7抛出异常,那么a1需要被析构
- 如果L8抛出异常,那么a1和a2都要析构
编译器通过分析代码,簿记这些区段以及需要析构的object list。运行时,根据异常抛出时所在的区段,查找上述的数据结构,就可以知道哪些对象需要被析构。
构造函数抛出异常时,成员对象及基类成分被析构的原理,是类似的。在C++运行时看来,构造函数只是普通的函数而已。
总结
C++的异常机制,给编译器和运行时均带来了一定的复杂度和代价。上述的”簿记“工作,只是冰上一角。
关于异常的使用,也有很多坑。怎么throw 怎么catch,都是有讲究的。有空下次再做总结。
C++异常与析构函数及构造函数的更多相关文章
- EC读书笔记系列之4:条款8 别让异常逃离析构函数
条款8 别让异常逃离析构函数 记住: ★析构函数绝对不要吐出异常.若一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序. ★若客户需对某个操作函数运行期间 ...
- Effective C++_笔记_条款08_别让异常逃离析构函数
(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码 ...
- Effective C++ 条款08:别让异常逃离析构函数
1.别让异常逃离析构函数的原因 <Effective C++>第三版中条款08建议不要在析构函数中抛出异常,原因是C++异常机制不能同时处理两个或两个以上的异常.多个异常同时存在的情况下, ...
- php 析构函数,构造函数
php 析构函数,构造函数 <?php /** * 测试使用的PHP操作类 * Date: 2017/7/13 * Time: 14:22 */class Test{ /** 姓名 */ p ...
- 《Effective C++》——条款08:别让异常逃离析构函数
考虑如下代码: class Widget{ public: ... ~Widget(){...}//假设这个可能吐出一个异常 }; void doSomething() { std::vector&l ...
- Effective C++ 条款八 别让异常逃离析构函数
class DBConn //这个class用来管理DBConnction对象 { public: //自己设计一个新的DBConn接口 方法3 void close() { db.close() ...
- c++之别让异常逃离析构函数
关于 本文代码演示环境: VS2017. 代码写的够不规范,目的是为了缩短文章篇幅. 本文主要是为了加深印象,写了好多次的代码,还是忘记了这茬.... 之前上传到github的代码会慢慢改过来. 本文 ...
- C++异常以及异常与析构函数
1. 抛出异常 1.1 抛出异常(也称为抛弃异常)即检测是否产生异常,在C++中,其采用throw语句来实现,如果检测到产生异常,则抛出异常. 该语句的格式为: throw 表达式; 如果在try语句 ...
- EC笔记,第二部分:8.别让异常逃离析构函数
1.为何析构函数不应该抛出异常? 有两种情况: 1).假设析构函数中有众多语句,而第一条语句抛出异常(或者其他语句),那么抛出异常以后的语句就得不到执行.而通常我们在析构函数中写的是清理资 ...
随机推荐
- 安卓离线SDK Windows版 资源包下载地址全集
1.Tools https://dl-ssl.google.com/android/repository/platform-tools_r19.0.1-windows.zip https://d ...
- 框架-Java:Spring MVC
ylbtech-框架-Java:Spring MVC Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面.Spring 框架提供了构建 We ...
- Unsupported major.minor version 52.0 (unable to load class XXX
java项目构建从高版本JDK改为低版本JDK报错.这是再次编译时使用的JDK版本比你原来编译的版本低所导致的. 转自:http://blog.csdn.net/zixiao217 maven项目在服 ...
- String不可变性
今天分析一下String,String有很多实用的特性,比如说“不可变性”,是工程师精心设计的艺术品.用final就是拒绝继承,防止内部属性或方法被破坏. 一,什么是不可变? String不可变很简单 ...
- hiho一下第76周《Suzhou Adventure》
我是菜鸡,我是菜鸡,我是菜鸡....重要的事说三遍 算是第一次做树形dp的题吧,不太难.. 园林构成一棵树,root为1,Hi从root出发,有k个园林必须玩,每个园林游玩后会得到权值w[i],最多玩 ...
- 2017ACM/ICPC亚洲区沈阳站 C Hdu-6219 Empty Convex Polygons 计算几何 最大空凸包
题面 题意:给你一堆点,求一个最大面积的空凸包,里面没有点. 题解:红书板子,照抄完事,因为题目给的都是整点,所以最后答案一定是.5或者.0结尾,不用对答案多做处理 #include<bits/ ...
- shp系列(三)——利用C++进行DBF文件的读(打开)
1.DBF文件要点 DBF文件又叫属性文件,也叫dBASE文件,文件后缀是.dbf,实际上ArcGIS打开后的属性表就是DBF的信息.DBF文件遵循以下几个条件: 每个要素在表中必须要包含一个与之相对 ...
- Hadoop MapReduce编程 API入门系列之wordcount版本1(五)
这个很简单哈,编程的版本很多种. 代码版本1 package zhouls.bigdata.myMapReduce.wordcount5; import java.io.IOException; im ...
- jQuery Validate前端验证
我们经常看到如下效果,那么它是如何实现的呢?看下面: 废话少说,直接上代码,大家直接Copy就能看到上面的效果啦. <html> <head> <title>验证内 ...
- Python matplotlib库
安装日期:2017.9.7 版本不太清楚,为啥嘞? 从python2到python3,还有在学的tensorflow,版本一更新就会有之前的代码不能用了.学习的时候用别人的代码各种出错,查了半天发现那 ...