Item 9: 绝不要在构造或析构期间调用虚函数
类构造和析构期间不会发生向下转换
假设你有一套模拟股票交易的类继承体系,例如,购入订单,出售订单等:
//@ 基类
class Transaction {
public:
Transaction();
virtual void logTransaction() const { cout << "Transaction logTransaction" << endl; };
};
Transaction::Transaction()
{
logTransaction();
}
//@ 买家子类
class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const { cout << "BuyTransaction logTransaction" << endl; }
};
//@ 卖家子类
class SellTransaction : public Transaction {
public:
virtual void logTransaction() const { cout << "SellTransaction logTransaction" << endl; };
};
int main()
{
BuyTransaction b;
return 0;
}
调用:
BuyTransaction b;
输出:
Transaction logTransaction
基类要先于子类构造,基类构造期间,虚拟函数从来不会向下匹配到派生类。取而代之的是,那个对象的行为好像它就是基类型。在一个派生类对象的基类构造期间,对象的类型是基类的类型。不仅虚拟函数会解析到基类,而且用到运行时类型信息的语言构件(例如,dynamic_cast 和 typeid),也会将那个对象视为基类类型。
同样的推理也适用于析构。一旦派生类析构函数运行,这个对象的派生类数据成员就呈现为未定义的值,所以 C++ 就将它们视为不再存在。在进入基类析构函数时,这个对象就成为一个基类对象,C++ 的所有构件—— 虚拟函数,dynamic_cast 等——都以此看待它。
在构造或析构期间调用虚拟函数的问题并不总是如此容易被察觉。如果 Transaction 有多个构造函数,每一个都必须完成一些相同的工作,软件工程为避免代码重复,将共同的初始化代码,包括对 logTransaction 的调用,放入一个私有非虚拟初始化函数中,叫做 init:
//@ 基类
class Transaction {
public:
Transaction();
virtual void logTransaction() const { cout << "Transaction logTransaction" << endl; };
private:
void init();
};
Transaction::Transaction()
{
init();
}
void Transaction::init()
{
logTransaction();
}
这个类中构造函数并非直接调用虚函数,而是间接调用,同样的问题依旧存在。
不能在基类的构造过程中使用虚拟函数向下匹配,你可以改为让派生类将必要的构造信息上传给基类构造函数作为补偿:
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // now a non-
// virtual func
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // now a non-
} // virtual call
class BuyTransaction: public Transaction {
public:
BuyTransaction(parameters)
: Transaction(createLogString( parameters )) // pass log info
{ ... } // to base class
... // constructor
private:
static std::string createLogString( parameters );
};
总结
- 在构造或析构期间不要调用虚拟函数,因为这样的调用不会转到比当前执行的构造函数或析构函数所属的类更深层的派生类。
- 不能在基类的构造过程中使用虚拟函数向下匹配,你可以改为让派生类将必要的构造信息上传给基类构造函数作为补偿。