Effective C++读书笔记(6)(一)

2014-11-24 12:20:04 · 作者: · 浏览: 3

新年了~忙着东奔西跑3天,是时候回归正常生活了……

条款08:别让异常逃离析构函数

Prevent exceptions from leavingdestructors

C++ 不禁止但不鼓励从析构函数引发异常。考虑:

class Widget {
public:
...
~Widget() { ... } // 假设这里可能吐出一个异常
};

void doSomething()
{
std::vector v;
...
} // v在这里被自动销毁

当 vector v 被析构时,它有责任析构它包含的所有 Widgets。但假设在那些调用期间,先后有两个Widgets抛出异常,对于 C++ 来说,这太多了。在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。在本例中将导致不明确行为,使用标准库的任何其他容器(如list,set)或TR1的任何容器甚至array,也会出现相同情况。C++ 不喜欢析构函数吐出异常。

如果你的析构函数需要执行一个可能失败而抛出一个异常的操作,该怎么办呢?假设使用一个class负责数据库连接,为了确保客户不会忘记在 DBconnection对象上调用 close(),一个合理的想法是创建一个用来管理DBConnection资源的类,并在其析构函数中调用close:

class DBConn { // 这个类用来管理DBConnection对象
public: // objects
...
~DBConn() // 确保数据库连接总是会被关闭
{ db.close();}
private:
DBConnection db;
};

它允许客户像这样编程

{ // 打开一个区块(block)
DBConn dbc(DBConnection::create());

// 建立DBConnection并交给DBConn对象以便管理
... // 通过DBConn的接口使用DBConnection对象
} //在区块结束点,DBConn对象被销毁,因而自动为DBConnection对象调用close

只要调用 close 成功,一切都美好。但是如果这个调用导致一个异常,DBConn 的析构函数将传播那个异常,也就是允许它离开析构函数。这就产生了问题,因为析构函数抛出了一个烫手的山芋。

有两个主要的方法避免这个麻烦。

· Terminatethe program:如果 close 抛出异常就终止程序,一般是通过调用 abort。

DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
制作运转记录,记下对close的调用失败;
std::abort();
}
}

它有一个好处是:阻止异常从析构函数中传播出去(那会导致不明确的行为)。也就是说,调用 abort 可以预先制“不明确行为”于死地。

· Swallowthe exception:吞下因调用close而发生的异常。在此例中将在第一种方法下去掉abort那句语句。

通常,将异常吞掉是个坏主意,因为它隐瞒了“某些动作失败”的重要信息!然而,有些时候,吞下异常比冒程序过早终止或不明确行为的风险更可取。程序必须能够在遭遇到一个错误并忽略之后还能继续可靠地运行,这才能成为一个可行的选择。

· 析构函数应该永不引发异常。如果析构函数调用了可能抛出异常的函数,析构函数应该捕捉所有异常,然后不传播它们或者终止程序。

以上方法的问题都在于两者无法对引起 close 抛出异常的情况做出回应。

一个更好的策略是重新设计 DBConn 的接口,以使客户有机会对可能发生的问题做出回应。

class DBConn {
public:
...

void close() // 供客户使用的新函数
{
db.close();
closed = true;
}

~DBConn()
{
if (!closed) {
try {
db.close(); // 关闭连接(如果客户不那么做的话)

}
catch (...) { // 如果关闭动作失败,记录下来并结束程序或吞下异常
制作运转记录,记下对close的调用失败;
...
}
}

private:
DBConnection db;
bool closed;
};

这样把调用 close 的责任从 DBConn 的析构函数移交给 DBConn 的客户(同时在 DBConn 的析构函数中仍内含一个“双保险调用”)。如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。这是因为析构函数)引发异常是危险的,永远都要冒着程序过早终止或 不明确行为的风险。在本例中,让客户自己调用 close 并不是强加给他们的负担,而是给他们一个处理错误的机会。他们可以忽略它,依靠 DBConn 的析构函数去调用 close。如果真有错误发生,close的确抛出异常而且DBConn吞下该异常或结束程序,客户没有立场抱怨,毕竟他们曾有机会第一手处理问题,而他们选择了放弃。

· 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(非析构函数)执行该操作。

条款09:绝不在构造和析构过程中调用virtual函数

Never call virtual functions duringconstruction or destruction

先概述重点:你不应该在构造或析构期间调用 virtual函数,因为这样的调用不会如你想象那样工作,而且会让你很郁闷。作为 Java 或 C# 程序员,也要更加注意本条款,因为这是C++与它们不相同的一个地方。

假设你有一套模拟股票交易的类继承体系,例如,购入、出售订单等。这样的交易一定要经过审计,所以每一个交易对象被创建,在一个审查日志中就需要创建一个相应的条目。下面是一个看起来似乎合理的解决问题的方法:

class Transaction { // 所有交易的基类
public:
Transaction();

virtual void logTransaction() const = 0; // 做出一份因类型不同而不同的日志记录
...
};

Transaction::Transaction() // 基类构造函数之实现
{
...
logTransaction(); // 最后动作是志记这笔交易
}

class BuyTransaction: public Transaction { //derived class
public:
virtual void logTransaction() const;
...
};

class SellTransaction: public Transaction {// derived class
public:
virtual void logTransaction() const;
...
};

考虑执行这行代码时会发生什么:

BuyTransaction b;

很明显一个 BuyTransaction 的构造函数会被调用,但是首先,一个 Transaction 的 构造函数必须先被调用,派生类对象中的基类成分先于派生类自身成分被构造之前构造。Transaction 的构造函数的最后一行调用 virtual函数 logTransaction,,被调用的 logTransaction 版本是在 Transaction 中