C++的异常机制使得程序付出某些代价:资源泄漏的可能性增加了;写出具有你希望的行为的构造函数与析构函数变得更加困难;执行程序和库程序尺寸增加了,同时运行速度降低了等等。

但是为什么使用异常呢?C程序使用错误代码(Error code)来判断异常状态,这种做法的问题是:异常可能被忽略,如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。

C程序能够仅通过setjmp和longjmp来完成与异常处理相似的功能。但是当longjmp在C++中使用时,它存在一些缺陷,当异常栈展开时不能对局部对象调用析构函数。所以setjmp和longjmp不能够替换异常处理。如果你需要一个方法,能够通知不可被忽略的异常状态,并且搜索栈空间(searching the stack)以便找到异常处理代码时,还需要确保局部对象的析构函数必须被调用,这时你就需要使用C++的异常处理。

09:使用析构函数防止资源泄漏

以对象管理资源,可以在对象的析构函数中释放资源。因为不论控制流如何离开区块,不管是正常离开,还是因为异常,对象都会被销毁,其析构函数也就自然被调用。因此,这种做法也就保证了异常发生时不会发生资源泄漏。这也就是所谓的RAII(资源获取即初始化),因为几乎总是在获取资源后,同一语句内以它初始化某个管理对象。

10:在构造函数内防止内存泄漏

下面的代码:

BookEntry::BookEntry(const string& name, const string& address,
const string& imageFileName, Const string& audioClipFileName)
: theName(name), theAddress(address), theImage(), theAudioClip()
{
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}

上面的代码看似正常,但是当调用theAudioClip = new AudioClip(audioClipFileName)发生异常时,theImage所指向的对象该由谁来删除呢?

析构函数只会析构已构造完全的对象,因此在构造函数发生异常时,BookEntry的析构函数不会执行。所以,必须设计构造函数,使其能够自我清理,通常这只需要将所有可能的异常进行捕捉,执行清理工作,然后重新抛出异常即可:

BookEntry::BookEntry(const string& name, const string& address,
const string& imageFileName, const string& audioClipFileName)
: theName(name), theAddress(address), theImage(), theAudioClip()
{
try {
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
catch (...) { // 捕获所有异常
delete theImage; // 完成必要的清除代码
delete theAudioClip;
throw; // 继续传递异常
}
}

实际上,一个更好的答案是遵循上一条的忠告,以对象管理资源,将theImage和theAudioClip所指的资源交由局部对象管理,也就交由智能指针进行管理。

注意:不用为BookEntry中的非指针数据成员操心,在类的构造函数被调用之前数据成员就被自动地初始化。所以如果BookEntry构造函数体开始执行,对象的theName, theAddress 和 thePhones数据成员已经被完全构造好了。这些数据可以被看做是完全构造的对象,所以它们将被自动释放,不用你介入操作(实际上,在析构函数中,也从来不用手动释放过它们)。比如下面的测试代码:

class Inner {
public:
Inner() {
printf("this is inner ctor\n");
}
~Inner(){
printf("this is inner dtor\n");
}
};
class Test {
public:
Test() {
printf("This is test ctor\n");
throw 3.0;
printf("after throw 3.0\n");
}
~Test() {
printf("this is Test dtor\n");
}
private:
Inner inner;
};
int main() {
try {
Test t;
}
catch (double &d) {
printf("catch %f\n", d);
}
printf("over\n");
}

代码执行结果如下:

this is inner ctor
This is test ctor
this is inner dtor
catch 3.000000
over

如果想要捕获在初始化列表中可能发生的异常,则需要使用函数try块(function try block)语法:

template <class T> Handle<T>::Handle(T *p)
try : ptr(p), use(new size_t())
{
// function body
}
catch(const std::bad_alloc &e)
{
handle_out_of_memory(e);
}

关键字 try 出现在成员初始化列表之前,并且测试块的复合语句包围了构造函数的函数体。catch 子句既可以处理从成员初始化列表中抛出的异常,也可以处理从构造函数函数体中抛出的异常。

11:禁止异常流出析构函数

有两种情况下会调用析构函数。第一种是当对象在正常状态下被销毁,也就是对象离开它的生存空间,或是被显式地delete。第二种是当对象被异常处理机制,也就是异常传播过程中的栈展开(stack-unwinding)过程中,对象被销毁。

因此,当析构函数被调用时,可能有一个异常正在激活状态(无法在析构函数中区分是否有异常已被激活),这种情况下,如果析构函数内部又发生了异常且析构函数没有捕获该异常,则C++会调用terminate函数,直接结束程序。这种情况可能不是你愿意看到的,因此,需要在析构函数中使用try  catch捕获异常。

不允许异常传递到析构函数外面还有第二个原因:如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数的执行就是不完全的,也就意味着部分清理工作没有完成:

Session::Session()
{
logCreation(this);
startTransaction(); // 开始一个数据库事务
} Session::~Session()
{
logDestruction(this);
endTransaction(); // 结束数据库事务
}

在析构函数中,如果logDestruction(this)抛出异常,则数据库事务就不会结束。因此,这里也需要使用try catch捕获异常,结束事务。

12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异

从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别。但是实际上,调用函数时,程序的控制权最终还会返回到函数的调用处,但是当抛出一个异常时,控制权永远不会回到抛出异常的地方。所以他们还是有差别的:

void passAndThrowWidget()
{
Widget localWidget;
throw localWidget;
}

因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储。因此,上面的throw localWidget语句,将进行localWidget的拷贝操作。用 throw 表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问的空间。这个对象由 throw 创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的 catch,并且在完全处理了异常之后撤销。

因此,异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型。

Widget& rw = localSpecialWidget;      // rw 引用SpecialWidget
throw rw; //它抛出一个类型为Widget的异常

这里抛出的异常对象是Widget类型,即使rw引用的是一个SpecialWidget。因为rw的静态类型是Widget,而不是SpecialWidget。

空的throw语句是重新抛出当前的异常对象,而不是catch参数,比如下面的代码:

class Except
{
public:
Except(int a=):value(a) {}
int value;
}; void throwexcept() {
try {
throw Except();
}
catch (Except ee) {
printf("1catch except value is %d\n", ee.value);
ee.value = ;
printf("now except value is %d\n", ee.value);
throw;
}
} int main(){
try {
throwexcept();
}
catch (Except ee){
printf("2catch except value is %d\n", ee.value);
}
printf("over\n");
}

在throwexcept函数中,捕获异常后,修改的是参数ee,而不是异常对象,因此,重新throw之后,再次捕获时,异常对象的value值没有变。结果如下:

1catch except value is
now except value is catch except value is
over

当catch的参数是对象,而不是对象的引用时,需要付出两次复制的成本,一次是任何exception都会产生的异常对象,一次是异常对象复制到catch参数。如果catch的参数是引用,则只付出一次复制异常对象的成本。

如果catch的参数是指针,虽然可以避免复制(这里的异常对象成了指针,复制的是指针),但是当指针指向局部对象时,该局部对象会在异常离开scope时被析构,从而产生未定义行为。

异常与catch子句相匹配的过程中,仅有两种转换可能发生:一是继承架构中的类转换,针对base的catch子句,可以处理derived类型的异常,该规则适用于by value, by reference, by pointer。比如C++的异常继承体系中,range_over和overflow_error都继承自runtime_error:

catch (runtime_error) ...         // 可以捕捉类型为runtime_error,
catch (runtime_error&) ... // range_error,或overflow_error类型
catch (const runtime_error&) ... // 的异常 catch (runtime_error*) ... // 可以捕捉类型为runtime_error*,
catch (const runtime_error*) ... // range_error*,或overflow_error*类型的异常

第二种允许发生的转换是从一个“类型指针”转为“void指针”,因此,catch (const void*)可以捕获任何指针类型的异常

捕获异常采用最先匹配策略,因此,绝对不要将针对base class的catch子句放在针对derived class的catch子句之前。

13:以by reference的方式捕捉exception

传递异常有三种方式:by pointer, by value和by reference。

by pointer的方式虽然最有效率(只是复制指针,而不是复制对象),但是为了让指针所指物在控制权离开scope之后依然存在,指针所指物不能是局部对象,只能是全局或静态对象,或者是new出来的heap对象。但是catch异常的代码可能不知道该如何处理指针,不知道是否需要对指针执行delete,所以,不推荐使用by pointer的方式传递异常。

by value的情况,每当抛出时,需要复制两次,而且一旦catch参数为base class,而throw一个derived class,则会发生切割问题,所以也不推荐by value的方式。

所以,推荐以by reference的方式传递异常,它没有上述两种方式的缺点。

14:谨慎使用异常说明符(exception specifications

异常说明符明确指出一个函数可以抛出什么样的异常,如果函数抛出了一个不在异常说明符中的异常,标准库中的std::unexpected函数就会被调用,该函数默认是调用std::terminate函数结束进程。这可能不是预期的行为。异常说明符在C++11中已废弃。

如果函数A调用了函数B,B函数抛出了不在A异常说明符中的异常,这就会导致运行时调用std::unexpected函数。

因此,应该避免在拥有类型参数的函数模板中使用异常说明符,因为不知道模板的类型参数有可能会抛出什么样的异常。

std::unexpected函数调用当前的std::unexpected_handler(一个函数指针),该指针默认指向std::terminate。用户可以通过调用std::set_unexpected设置新的std::unexpected_handler,新的std::unexpected_handler函数可以重新抛出一个异常,该异常如果在异常说明符中,异常栈展开将继续进行,异常就能传递下去。如果新异常不在异常说明符中,但是异常说明符中包含了std::bad_exception,则抛出std::bad_exception,其他情况下,std::terminate就会被调用。

为了防止非预期的异常发生,可以用新的异常取代非预期的异常:

class UnexpectedException {};

void convertUnexpected()
{
throw UnexpectedException();
} void fun() throw(UnexpectedException)
{
std::set_unexpected(convertUnexpected);
throw 3.0;
}
int main()
{
try
{
fun();
}
catch (UnexpectedException)
{
printf("catch UnexpectedException\n");
}
catch (double)
{
printf("catch double\n");
}
catch (std::bad_exception)
{
printf("catch bad_exception\n");
}
}

fun函数中,double类型的异常被抛出,double不在异常说明符中,导致convertUnexpected函数被调用,该函数抛出了在异常说明符中的UnexpectedException异常,因此栈展开得以继续,在main中能捕获到该异常,打印出” catch UnexpectedException”。

如果将fun的异常说明符改为throw(std::bad_exception),则新的UnexpectedException也不再异常说明符中,但是栈展开还是能继续,打印出” catch bad_exception”。

如果fun的异常说明符为throw(),则直接调用std::terminate,打印出:terminate called after throwing an instance of 'UnexpectedException'。

异常说明符还有一个缺点,它会造成“较高层次的调用者已经准备好要处理发生的异常时,unexpected函数却被调用了”:

static void logDestruction(Session *objAddr) throw();

Session::~Session() {
try {
logDestruction(this);
}
catch (...) { }
}

尽管Session的析构函数明确指出可以捕获任何logDestruction抛出的异常,然而因为logDestruction带有一个异常说明符,保证不抛出任何异常。但是一旦logDestruction抛出了异常,unexpected函数就会被调用,程序被终止,根本没有给Session析构函数catch一个机会。

15:了解异常处理的成本

为了在运行时处理异常,程序要记录大量的信息:无论执行到什么地方,程序都必须能够识别出如果在此处抛出异常的话,哪些对象需要被析构;程序必须在每一个try语句块的进入点和离开点做记号;对于每一个try块,必须记录与其相关的catch子句以及这些catch子句能够捕获的异常类型。这种信息的记录不是没有代价的。

而且,运行时的比对工作(以确保符合异常说明符)也不是免费的,当异常被抛出时销毁适当对象并找出正确的catch子句也不是免费的。

因此,异常处理是有代价的,即使你没有使用try,throw或catch关键字,你同样得付出一些代价。

让我们先从不使用任何异常处理特性也要付出的代价谈起:你需要空间建立数据结构来记录哪些已被完全构造(参见条款10),你也需要CPU时间保持这些数据结构不断更新。这些开销一般不是很大,但是采用不支持异常的方法编译的程序一般比支持异常的程序运行速度更快所占空间也更小。

大部分支持异常的编译器生产商都允许你自由决定是否在生成的代码里包含异常支持能力。如果你确定你程序的任何部分都不使用try,throw或catch,也确定所连接的程序库也没有使用try,throw或catch,就可以采用不支持异常处理的方法进行编译,这可以缩小程序的尺寸和提高速度,否则你就得为一个不需要的特性而付出代价。

使用异常处理的第二个开销来自于try块,无论何时使用它,也就是当你想能够捕获异常时,那你都得为此付出代价。不同的编译器实现try块的方法不同,粗略估计,如果你使用try块,代码的尺寸将增加5%-10%,运行速度也同比例减慢。这还是假设程序没有抛出异常,只是代码中出现try语句块的成本而已。因此为了减少开销,你应该避免使用无用的try块。

编译器为异常说明符生成的代码类似于面对try语句块的作为,所以一个异常说明符通常具有与try语句块相同的成本。

现在我们来到了问题的核心部分,看看抛出异常的开销。事实上我们不用太关心这个问题,因为异常应该是很罕见的,根据80-20规则(参见条款16),这样的事件不会对整个程序的性能造成太大的影响。但是抛出一个异常,到底会有多大的冲击?答案是与一个正常的函数返回相比,可能会比较大。通过抛出异常从函数里返回可能会慢3个数量级。这个开销很大。但是仅仅当你抛出异常时才会有这个开销,一般不会发生。

上面提到的数据,比如说程序的尺寸将增大5%-10%,抛出异常执行会慢3个数量级,这些数据也许未必准确,但是重要的是了解本条款所描述的成本,以及采取必要的措施:只要可能就尽量采用不支持异常的方法编译程序;把try块和异常说明符限制在非用不可的地点;并且只有在确为异常的情况下才抛出异常。如果你在性能上仍旧有问题,利用分析工具(profiler)分析你的程序,以决定异常支持是否是一个起作用的因素。如果是,那就考虑选择其它的编译器,能在C++异常处理方面具有更高实现效率的编译器。

More Effective C++: 03异常的更多相关文章

  1. ###《More Effective C++》- 异常

    More Effective C++ #@author: gr #@date: 2015-05-24 #@email: forgerui@gmail.com 九.利用destructors避免泄漏资源 ...

  2. struts2 笔记03 异常支持、防止页面刷新和后退、方法验证

    Struts2对异常支持(声明式异常.自动的异常处理), 异常处理(运行期异常事务自动回滚) 1. 自定义异常类,继承RuntimeException或Exception实现构造方法. 2. 配置异常 ...

  3. PL/SQL 训练03 --异常

    --程序员在开发的时候,经常天真的认为这个世界是完美的,用户如同自己般聪明,总能按照自己设想的方式--操作系统输入数据.但残酷的事实告诉我们,这是不可能的事情,用户总会跟我们相反的方式操作系统--于是 ...

  4. Effective java -- 8 异常

    第五十七条:只针对异常的情况才使用异常应该都有这个意识吧,就像什么抓索引越界什么的,没有必要. 第五十八条:对可恢复情况使用受检查异常,对编程错误使用运行时异常三种可抛的异常:受检的异常(checke ...

  5. Effective C++: 03资源管理

    所谓资源,就是一旦用了它,将来必须还给系统.C++中的资源有:内存.文件描述符.互斥锁.数据库连接.网络socket等. 13:以对象管理资源 1:像下面这个函数: void f() { Invest ...

  6. Effective Java 03 Enforce the singleton property with a private constructor or an enum type

    Principle When implement the singleton pattern please decorate the INSTANCE field with "static ...

  7. const 修饰成员函数 前后用法(effective c++ 03)

    目录 const在函数后面 const修饰成员函数的两个作用 const在函数前面 总结 const在函数后面 类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态 ...

  8. Effective Java Index

    Hi guys, I am happy to tell you that I am moving to the open source world. And Java is the 1st langu ...

  9. python异常(概念、捕获、传递、抛出)

    异常 目标 异常的概念 捕获异常 异常的传递 抛出异常 01. 异常的概念 程序在运行时,如果 Python 解释器 遇到 到一个错误,会停止程序的执行,并且提示一些错误信息,这就是 异常 程序停止执 ...

随机推荐

  1. HDFS 块

  2. vue 如何发起网络请求 之 axios

    1   1 2 3 4 5 6 7 8 9 10 // axios 请求  在main.js里边写入 import Axios from 'axios'   // 配置请求信息 var $http = ...

  3. 海量大数据大屏分析展示一步到位:DataWorks数据服务+MaxCompute Lightning对接DataV最佳实践

    1. 概述 数据服务(https://ds-cn-shanghai.data.aliyun.com) 是DataWorks产品家族的一员,提供了快速将数据表生成API的能力,通过可视化的向导,一分钟“ ...

  4. [Array]268. Missing Number

    Given an array containing n distinct numbers taken from 0, 1, 2, ..., n, find the one that is missin ...

  5. Java中的String,StringBuffer和StringBuilder

    在了解这个问题的时候查了不少资料,最有帮助的是这个博文:http://swiftlet.net/archives/1694,看了一段时间,咀嚼了一段时间,写一个经过自己消化的博文,希望能帮到大家. 首 ...

  6. LintCode 合并两个排序

    将两个排序链表合并为一个新的排序链表 样例 给出 1->3->8->11->15->null,2->null, 返回1->2->3->8-> ...

  7. python使用cPickle模块序列化实例

    python使用cPickle模块序列化实例 这篇文章主要介绍了python使用cPickle模块序列化的方法,是一个非常实用的技巧,本文实例讲述了python使用cPickle模块序列化的方法,分享 ...

  8. MySQL--视图、触发器、事务、存储过程、内置函数、流程控制、索引

    视图 触发器 事务 存储过程 内置函数 流程控制 索引 视图 1.什么是视图 视图就是通过查询得到一张虚拟表,然后保存下来,下次直接使用即可 2.为什么要用视图 如果要频繁使用一张虚拟表,可以不用重复 ...

  9. 2019-3-1-安装-Sureface-Hub-系统-Windows-10-team-PPIPro-系统

    title author date CreateTime categories 安装 Sureface Hub 系统 Windows 10 team PPIPro 系统 lindexi 2019-03 ...

  10. python的工具pip进行安装时出现 No module named 'pip'

    现象: 解决: python -m ensurepip easy_install pip python -m pip install --upgrade pip #用于更新pip,默认安装的是pip9 ...