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).假设析构函数中有众多语句,而第一条语句抛出异常(或者其他语句),那么抛出异常以后的语句就得不到执行.而通常我们在析构函数中写的是清理资 ...
随机推荐
- Linux用户和组的管理
一.概述: 1.Linux下的三类用户: (1).超级用户(root)具有操作系统的一切权限UID值均为0 (2).普通用户具有操作系统有限的权限UID值500~6000 (3).伪用户是为了方便系统 ...
- luogu3865 【模板】 ST表
题目大意:给出一段序列,每次查询一段区间,求区间最大值. ST表:设原序列为A,定义F[i][k]为A[i][2k-1]的最大值.有递归式:F[i][k]=max(F[i][k-1], F[i+2k- ...
- 轻快的vim(一):移动
断断续续的使用VIM也一年了,会的始终都是那么几个命令,效率极低 前几个星期把Windows换成了Linux Mint,基本上也稳定了下来 就今晚,我已经下定决心开始新的VIM之旅,顺便写一系列的笔记 ...
- php手机号码验证正则表达式
移动:134.135.136.137.138.139.150.151.152.157.158.159.182.183.184.187.188.178(4G).147(上网卡): 联通:130.131. ...
- 记录,javascript中对象的属性名是字符串,却可以不用引号
问题描述:今日看书,里面介绍js的对象的属性名是包括空字符串在内的所以字符串 问题来了,我们平时定义的对象如下,是没有引号""or’'的 var someone = { f ...
- 下载jdk12版本没有jre问题处理
以往下载jdk1.6版本直接运行会生成jdk,jre两个文件,但今天下载jdk12运行后,只有jdk目录文件,并没有jre后来在网上查找后通过命令行方式手动生成jre 1.下载jdk12 网址:htt ...
- 测试数据准备中用到到csv写文件知识点
对于大数据测试中,有时需要自己去准备一些数据,用csvreader来写一个比较大的文件就比较方便,下面我就直接贴示例代码了: package com.acxm.amysu.test;import co ...
- C# net winform wpf 发送post数据和xml到网页
由于项目需要发送数据到网页 这里用aspx做测试 采用post以及get发送数据,页面进行数据 首先这个东西很简单很简单,基本上学过的都会,但是原谅一直搞cs几乎不搞bs的猿类吧.三四年没接触bs. ...
- 关于出现Failed to instantiate SLF4J LoggerFactory问题原因,解决办法
在创建spring boot 文档进行配置的时候,因为使用spring boot 父级依赖的版本 <artifactId>spring-boot-starter-parent</ar ...
- WPF 解决拼接屏全屏的问题
需求: 8块1920*1080屏幕拼接 橙色4个框每个框(1920*1080)拼接成一个1920*1080 红色4个框每个框(1920*1080)拼接成一个1920*1080 橙色和红色作为displ ...