你不应该在构造或析构期间调用虚函数,因为这样的调用不会如你想象那样工作,而且它们做的事情保证会让你很郁闷。 假设你有一套模拟股票处理的类层次结构,例如,购入流程,出售流程等。对这样的处理来说可以核查是非常重要的,所以随时会创建一个 Transaction 对象,将这个创建记录在核查日志中是一个适当的要求。下面是一个看起来似乎合理的解决问题的方法:
class Transaction { // base class for all public: // transactions Transaction();
virtual void logTransaction() const = 0; // make type-dependent // log entry ... };
Transaction::Transaction() // implementation of { // base class ctor ... logTransaction(); // as final action, log this } // transaction
class BuyTransaction: public Transaction { // derived class public: virtual void logTransaction() const; // how to log trans- // actions of this type ... };
class SellTransaction: public Transaction { // derived class public: virtual void logTransaction() const; // how to log trans- // actions of this type ... }; |
考虑执行这行代码时会发生什么:
很明显 BuyTransaction 的构造函数会被调用,但是首先,Transaction 的构造函数必须先被调用,派生类对象中的基类部分先于派生类部分被构造。Transaction 的构造函数的最后一行调用虚函数 logTransaction,但是结果会让你大吃一惊,被调用的 logTransaction 版本是在 Transaction 中的那个,而不是 BuyTransaction 中的——即使被创建的对象类型是 BuyTransaction。基类构造期间,虚函数从来不会向下匹配(go down)到派生类。取而代之的是,那个对象的行为就好像它的类型是基类。非正式地讲,
基类构造期间,虚函数禁止。 这个表面上看起来匪夷所思的行为存在一个很好的理由。因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间调用的虚函数向下匹配(go down)到派生类,派生类的函数理所当然会涉及到本地数据成员,但是那些数据成员还没有被初始化。这就会
为未定义行为和悔之晚矣的调试噩梦开了一张通行证。调用涉及到一个对象还没有被初始化的部分自然是危险的,所以 C++ 告诉你此路不通。
在实际上还有比这更多的更深层次的原理。在派生类对象的基类构造期间,对象的类型是那个基类的。
不仅虚函数会解析到基类,而且语言中用到运行时类型信息(runtime type information)的配件(例如,dynamic_cast和 typeid),也会将对象视为基类类型。在我们的例子中,当 Transaction 构造函数运行初始化 BuyTransaction 对象的基类部分时,对象的类型是 Transaction。C++ 的每一个配件将以如下眼光来看待它,并对它产生这样的感觉:对象的 BuyTransaction 特有的部分还没有被初始化,所以安全的对待它们的方法就是视若无睹。在派生类构造函数运行之前,一个对象不会成为一个派生类对象。
同样的原因也适用于析构过程。一旦派生类析构函数运行,这个对象的派生类数据成员就被视为未定义的值,所以 C++ 就将它们视为不再存在。在进入基类析构函数时,对象就成为一个基类对象,C++ 的所有配件——虚函数,dynamic_casts 等——都如此看待它。