前言
C++禁止在构造和析构期间调用virtual函数,这是它与Java或者C#的一大不同之处。
实例
假设我们现有一个class用来模拟股市交易,显然每一笔订单都需要经过审计,那么在审计日志中必然也需要创建一笔交易记录,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0;//因交易类型不同做出不同的记录
...
};
Transaction::Transaction(){
...
logTransaction();//构造的最后执行记录
}
class BuyTransaction:public Transaction{
public:
virtual void logTransaction();//因交易类型不同做出不同的记录
...
};
试想一下,当这行语句执行时会发生什么?1
BuyTransaction b;
构造、析构次序与virtual函数
当我们创建derived class时,无疑bc的构造函数优先被调用,因为base class成分会在derived class自身成分被构造之前先构造完毕。这个时候问题来了,bc的构造函数最后使用了virtual function,但它调用的并不是derived class定义的版本,而是bc的pure virtual版。有一个不太恰当的解释:在构造base class构造期间,virtual函数并不是virtual函数。
因为base class的构造早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果在这期间调用derived class版本的virtual function,无疑该函数几乎必然使用local成员变量,而他们尚未初始化。
更根本的原因在于,在derived class的base成分构造期间,对象的类型是base class而非derived class。如果我们试图用dynmaic_cast或者typeid,这些函数也会把对象视为base class类型。这么对待对象是合理的:因为derived class的专属成分尚未被初始化,那么面对它们最安全的做法就是视而不见。对象在derived class构造函数开始执行前不会成为一个derived class对象。
相同道理也适用于析构函数。当进入了base class的析构函数后,析构对象已经变成了一个base class对象。你不可能在一个bc对象上调用dc的成员函数。
潜在危险
并非每一种virtual函数都明明白白地写在构造或者析构函数中等你改正,考虑下面这种情况:由于构造函数往往有多个,我们通常倾向于把构造函数们共同使用的那一部分代码放进一个初始化函数,但是init函数可能会包含一个virtual函数1
2
3
4
5
6
7
8
9
10
11class Transaction{
public:
Transaction() {init();}
virtual void logTransaction() const = 0;
...
private:
init(){
...
logTransaction();//此处调用了virtual
}
};
这种方法十分毒瘤,因为不会有任何连接器和编译器报错我们只能认真地检查构造函数和析构函数,确保不管是它们自身还是它们所调用的那些函数都不含有任何virtual函数。
根治策略
上述解决方法本质上仍然无法解决每当对象被创建就会自发记录的问题,我们已经了解无法在构造/析构函数中使用virtual函数,所以解决方法是将其改为non-virtual,要求dc构造函数必须传递必要信息给Transaction构造函数,而后那个构造函数就可以安全地调用non-virtual logTransaction。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Transaction{
public:
explicit Transaction(const string& logInfo) {}
void logTransaction(const string& logInfo) const;
...
};
Transaction::Transaction(const string& logInfo){
...
logTransaction(logInfo);
}
class BuyTransaction:public Transaction{
public:
BuyTransaction(parameters):
Transaction(createLogString(parameters))
{...}
...
private:
static string createLogString(parameters);
};
因为我们做不到virtual函数从base class向下调用,在构造期间,我们只能令derived class将必要的构造信息传递至base class的构造函数。
比起在成员初值列给予base class所需要的数据,利用辅助函数创建一个值传给base class往往更加可读。令此函数为static,也就不可能指向derived class内尚未初始化的成员变量。
总结
不要在构造和析构过程中调用virtual函数,因为此类调用从不会下降至dc层。