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

总结:

    在构造或析构期间不要调用 virtual函数,因为这样的调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。


       不应该在构造或析构期间调用 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 中的那一个,而不是 BuyTransaction 中的那一个,即使被创建的对象类型是 BuyTransaction。基类构造期间,virtual函数从来不会向下匹配到派生类。

         更根本的原因:构造派生类对象时,首先运行基类构造函数初始化对象的基类部分。在一个派生类对象的基类构造期间(即执行基类构造函数时),对象的派生类部分是未初始化的,可以认为对象还不是派生类对象,而是基类。不仅 virtual函数会解析到基类,而且若使用到 runtime type information(运行时类型信息)的语言构件(例如,dynamic_cast和 typeid),也会将那个对象视为基类类型。本例中,当 Transaction 的 构造函数正打算初始化一个 BuyTransaction对象的基类部分时,该对象的类型是Transaction 。这样的对待是合理的:这个对象的 BuyTransaction专属部分还没有被初始化,所以最安全的做法是视它们不存在。对象在派生类构造函数开始执行前不会成为一个派生类对象。同样的道理也适用于析构函数。

        在上面的示例代码中,Transaction 的构造函数造成了对一个 virtual函数的直接调用,这很明显而且容易看出违反本条款。这一违背是如此显见,以致一些编译器会给出一个关于它的警告(另一些则不会)。

         在构造或析构期间调用 virtual函数的问题并不总是如此容易被察觉。如果 Transaction 有多个构造函数,每一个都必须完成一些相同的工作,为避免代码重复将共通的初始化代码,包括对 logTransaction 的调用,放入一个初始化函数中,叫做 init:

class Transaction {
public:
     Transaction()
     { init(); } // 调用non-virtual...
     virtual void logTransaction() const = 0;
     ...
private:
    void init() {
        ...
        logTransaction(); // 这里调用virtual!
     }
};

        这个代码在概念上和早先那个版本相同,但是它更阴险,因为一般来说它会躲过编译器和连接程序的抱怨。其实还是在构造函数内调用了virtual。避免这个问题的唯一办法就是确保你的构造函数或析构函数决不在被创建或析构的对象上调用 virtual函数,而它们所调用的所有函数也服从同样的约束。

         如何确保在每一次 Transaction继承体系中的一个对象被创建时,都会调用 logTransaction 的正确版本呢?将 Transaction 中的 logTransaction 转变为一个 non-virtual函数,然后要求派生类构造函数将必要的信息传递给 Transaction 构造函数,而后那个函数就可以安全地调用 non-virtual的 logTransaction。如下:

class Transaction {
public:
    explicit Transaction(const std::string& logInfo);

       void logTransaction(conststd::string& logInfo) const;

       // 如今是个non-virtual函数
    ...
};

Transaction::Transaction(const std::string& logInfo)
{
    ...
    logTransaction(logInfo); //如今是个non-virtual函数
}

class BuyTransaction: public Transaction {
public:
    BuyTransaction( parameters )
    : Transaction(createLogString( parameters ))
    { ... } // 将log信息传递给基类构造函数
    ...

    private:
    static std::string createLogString(parameters );
};

        换句话说,由于你不能在基类的构造过程中使用 virtual函数向下调用,你可以改为让派生类将必要的构造信息上传给基类构造函数作为补偿。

        在此例中,注意 BuyTransaction 中那个 private static 函数 createLogString 的使用。使用一个辅助函数创建一个值传递给基类构造函数,通常比通过在成员初值列给基类它所需数据更加便利(也更加具有可读性)。将那个函数设置为static,就不会有偶然触及到一个新生的 BuyTransaction object对象的仍未初始化的数据成员的危险。



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值