写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++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证对象的...
基类dormitory,其有DormiNum和静态变量DormiMaxNum,构造函数,析构函数,一般函数GetDormiNum(), GetCountMan(), SetDormiNum()及一个纯虚函数display();2. dormitory的派生类room类,内有公有成员变量RN,构造函数、...
因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用。同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行结束,并释放已经构造的成员...
1、标准C++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证...
如果Object Pascal的类在构造函数中抛出异常,编译器会自动调用类的析构函数(由于析构函数不允许被重载,可以保证只有唯一一个析构函数,因此编译器不会迷惑于多个析构函数之中)。析构函数中一般会析构成员对象,...
1. 为局部对象调用析构函数 2. 析构函数应该从不抛出异常 3. 异常与构造函数 4. 未捕获的异常将会终止程序
第十二章 构造函数与析构函数 第十三章 面向对象程序设计 第十四章 堆与拷贝构造函数 第十五章 静态成员与友员 第十六章 继承 第十七章 多重继承 第十八章 运算符重载 第十九章 I/O流 第二十章 ...
本课程为计算机及应用(独立本科段)的一门专业课。课程内容有:面向对象的基本概念和要素,包括类、对象、...类,包括封装、数据成员、函数成员、可见性控制 、构造函数与析构函数、继承、多态性等;模板;异常处理等。
C++整型、字符型、浮点型、声明、定义、typedef、运算符、表达式、左值、选择语句、循环语句、指针、数组、函数和标识符的作用域、类基础、类作用域及相关运算符、构造函数、复制构造函数、析构函数、名称空间、类中...
14.9析构函数和虚拟析构函数 14.10小结 14.11练习 第15章 模板和包容器类 15.1包容器和循环子 15.2模板综述 15.2.1C方法 15.2.2Smalltalk方法 15.2.3模板方法 15.3模板的语法 15.3.1非内联函数定义 15.3.2栈模板...
只是由于时间关系,可能只要求实现构造函数、析构函数、拷贝构造函数等关键部分。 String的实现涉及很多C++的基础知识、内存控制及异常处理等问题,仔细研究起来非常复杂,本文主要做一个简单的总结和归纳。 ...
Item M9 析构函数与资源泄露 Item M10 构造函数与资源泄露 Item M12 异常 Item M17 懒惰计算法 Item M18 热心计算法 Item M24 虚表 没有完全吃透 Item M26 限制类个数 Item M27 对象在/不在堆上 Item M29 引用计数 ...
其中,包括构造函数、析构函数、显示函数等。 (2)输入/输出:对流提取和流插入运算符进行重载。 (3)计算功能:可进行分数的加、减、乘和除法运算。 (4)化简功能:将分数化简为最简分数。 (5)异常处理功能:...
在 PHP4 中,当函数与对象同名时,这个函数将成为该对象的构造函数,并且在 PHP4 中没有析构函数的概念。 在 PHP5 中,构造函数被统一命名为 __construct,并且引入了析构函数的概念,被统一命名为 __destruct...
Exp2:(习题16.30 构造函数、析构函数和异常处理)1. Description of the Problem(英文)Write a program il
·条款十一:禁止异常信息(exceptions)传递到析构函数外 ·条款十二:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异 ·条款十三:通过引用(reference)捕获异常 ·条款十四...
目录 介绍 基本语法 变量与数据类型 输入和输出 运算符 控制流程 条件语句 循环语句 ...构造函数与析构函数 成员函数与访问控制 继承与多态 异常处理 标准库简介 输入输出流 字符串处理 容器与算法
构造函数与析构函数 访问修饰符:public, private, protected 进阶特性 指针与引用 动态内存分配 STL库介绍:vector, map, set等 异常处理 C++的实际应用 C++在游戏开发中的应用 C++在系统编程中的应用 C++与性能...
10.4.析构函数和delete运算符 10.4..1 默认析构函数 10.4.2 调用构造函数进行类型转换 10.5 浅层复制构造函数 10.6 深层复制构造函数 第11章 运算符重载 11.1 运算符重载 11.2 在成员函数中实现自加 11.3 重载前置...
11.13章 析构函数 12.13章 深复制、浅复制 13.13章 管理指针成员 14.14章 重载操作符的定义 15.14章 重载输入输出操作符 16.14章 重载算术操作符 17.14章 重载关系操作符(一) 18.14章 重载关系操作符...