这是一条在C++中非常重要的条款,也是C++和其他更高级的语言(例如Java
、C#
等)不同的地方。
假设现在我们编写了一些类,用于抽象股市中买卖股票的订单:
class Transaction
{
public:
Transaction()
{
//...
this->logTransaction();
}
virtual void logTransaction() const = 0;
};
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
//...
};
class SellTransaction : Transaction
{
public:
virtual void logTransaction() const;
//...
}
现在,来思考当以下代码被执行时,发生了哪些函数调用:
int main()
{
BuyTransaction transaction;
return 0;
}
首先,BuyTransaction
的构造函数一定会被调用,因而首先会调用Transaction
的构造函数。Transaction
构造函数的最后一行调用了虚函数logTransaction()
,这是引发问题的地方:此时调用的是Transaction
中的实现,而不是BuyTransaction
中的实现。
也就是说,在构造函数中的虚函数调用绝不会绑定到子类的实现中,就好像虚函数不是虚函数一样。
其实有一个很好的理由可以解释这一现象:当调用父类的构造函数时,子类的所有成员变量都尚未被初始化,此时调用子类的成员函数必然会引发问题。所以,C++干脆拒绝掉这种调用请求。
另一方面,从编译器的角度来看,当调用父类的构造函数时,该对象的类型并不是子类,而是属于父类对象。原因是相同的,在调用父类构造函数的时候子类的成员变量一定没有初始化,因此干脆就视它们不存在好了。
相同的道理也适用于析构函数:一旦子类的析构函数开始执行,其专属的成员变量必然处于非法状态,因此也干脆视他们不存在,此时对象的类型就是其父类的类型。
虽然现在明白了这个道理,但是在实际的应用中却不能很方便地侦测到这一点,尤其是当遇到类似下面的情况时:
class Transaction
{
public:
Transaction() { init(); }
virtual void logTransaction() const = 0;
private:
void init()
{
//...
logTransaction();
}
};
这段代码的结构和上一个例子基本相同,但是它却暗中为害。因此,写相关的代码时必须时刻提醒自己:在构造函数和析构函数中坚决不能调用虚函数,并且在其中调用的函数内部也不能去调用虚函数。
现在仅剩的一个问题是:如何实现类似的需求,即创建子类对象时有正确版本的成员函数被调用呢?
答案是在构造父类对象时提供足够的额外信息,即将所有的必需参数都在父类的构造函数参数中提供:
class Transaction
{
public:
explicit Transaction(const std::string &msg)
{
logTransaction(msg);
}
void logTransaction(const std::string &msg);
};
class BuyTransaction : public Transaction
{
public:
BuyTransaction(/* ... */) : Transaction(this->createLogMessage()) {}
private:
std::string createLogMessage() { /* ... */ }
};
【注意】
在构造函数和析构函数中一定不能调用虚函数,因为这种调用不会绑定到子类的具体实现中。