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

1、为什么不能在构造函数中调用virtual函数?

(1)考虑下面的场景:

class Transaction {         //所有交易的base class
public:
    Transaction();
    virtual void logTransaction() const = 0;  //做出一份因为类型不同而不同的日志记录,目前是一个纯虚函数
    ...
};

Transaction::Transaction()  //base class的构造函数的实现
{
    ...
    logTransaction();    //最后的动作是对这笔交易进行记录,调用了virtual函数
}

class BuyTransaction: public Transaction {    //derived class
public:
    virtual void logTransaction() const;   //对这种类型的交易进行记录(log)
    ...
};

class SellTransaction: public Transaction {   //derived class
public:
    virtual void logTransaction() const;   //对这种类型的交易进行记录(log)
    ...
};

即在构造函数中调用了virtual函数。
(2)现象
当执行下列代码时:

BuyTransaction b;

将会引发一个异常:
由于继承自Transaction的BuyTransaction类型的变量b 在创建时会首先创建其基类部分(Transaction),也就是会调用Transaction的默认构造函数,而在构造函数中调用了一个virtual 的函数logTransaction(),本身b是一个BuyTransaction的对象,但是在构造其基类部分时,调用的logTransaction()函数却是Transaction类版本的。而我们预想的应该是BuyTransaction类版本的logTransaction()函数才对。即:在构造函数中的virtual并没有起到多态的效果,而是直接使用所在类版本的函数。

2、上述异常产生的原因是什么?

  • 构造一个派生类对象时,首先构造其基类部分,其次构造其派生类部分。而当构造基类部分时,派生类部分的成员变量还没有被初始化,因此,如果此时,调用派生类版本的成员函数的话,很有可能用到没有初始化的派生类部分的成员变量,这在C++中显然会导致不明确行为。因此,C++编译器拒绝你这么做,因此它错误的调用所在类版本的函数,而不产生多态效果。
  • 在派生类对象的基类部分构造期间,对象的类型是基类类型,而不是派生类类型。不仅virtual函数被解析至(resolve) to base class ,运行期类型信息也会被解析成(resolve) to base class(比如:dynamic_casttypeid)。

3、为什么在析构函数中不能够调用virtual函数?

析构函数从外向里析构,也就是先执行派生类部分,再执行基类部分。如果基类部分中调用了派生类版本的virtual成员函数,此时派生类部分已经被析构了,因此类似于构造函数,仍是会造成不确定行为。因此当析构函数执行到里层时,仍是被编译器看做是一个基类类型的对象。

4、如何检测析构函数或者构造函数是否运行期间调用了virtual成员函数?

情况一:在构造函数或者析构函数中调用的virtual成员函数是pure的,那么会导致链接错误。因为找不到纯虚函数的实现代码。
情况二:在构造函数或者析构函数中调用的virtual成员函数是非pure的,那么编译器不会报任何错误。但是导致的结果却是我们所不想看到的。

因此,我们值能通过不在构造函数和析构函数中调用virtual成员函数来避免这个问题发生。

5、既然不能够在构造函数和析构函数中调用virtual成员函数,如何解决上述,类似于记录不同派生类对象的日志问题?

  • 将日志信息函数写成非virtual的。
  • 此时可以在构造函数中调用日志信息函数。
  • 但是为了确保不同的派生类对象生成的是不同版本的日志信息,需要在构造函数中多传递一个参数,以确定产生的是哪个类型的日志信息。
class Transaction {
public:
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std:;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信息传递给base class构造函数
        { ... }
    ...
private:
    static std::string createLogString( parameters );//注意点
};

这段程序有两个点注意:

  • 利用辅助函数代替成员初值列给base class传递构造信息,比较方便,可读性也好。
  • 将这个函数写成static的。避免将指向派生类对象 的派生类部分(即:未初始化的成员变量)。
    (第二点有待进一步理解。)
posted @ 2019-12-17 20:39  江南又一春  阅读(234)  评论(0编辑  收藏  举报