Effective C++笔记之九:绝不在构造和析构过程中调virtual函数
扫描二维码
随时随地手机看文章
假设你有个class 继承体系,用来塑模股市交易如买进、卖出的订单等等。这样的交易一定要经过审计,所以每当创建个个交易对象,在审计日志(audit log) 中也需要创建一笔适当记录。下面是一个看起来颇为合理的做法:
class Transaction {// 所有交易的baseclass
public:
Transaction( );
virtual void logTransaction() const = 0;// 做出一份因类型不同而不同的日志记录
......
};
Transaction::Transaction () // base class 构造函数之实现
{
......
logTransaction(); // 最后动作是志记这笔交易
}
class BuyTransaction: public Transaction { // derivedclass
public:
virtual void logTransaction() const; // 志记(log) 此型交易
....
} ;
class SellTransaction: public Transaction { // derivedclass
public:
virtual void logTransaction() const; // 志记(log) 此型交易
....
};
现在,当以下这行被执行,会发生什么事:
BuyTransaction b;
无疑地会有一个BuyTransaction 构造函数被调用,但首先Transaction 构造函数一定会更早被调用;是的, derived class 对象内的base class 成分会在derived class自身成分被构造之前先构造妥当。Transaction 构造函数的最后一行调用virtual 函数logTransaction,这正是引发惊奇的起点。这时候被调用的logTransaction 是Transaction 内的版本,不是BuyTransaction 内的版本一一即使目前即将建立的对象类型是BuyTransaction。是的, base class 构造期间virtual 函数绝不会下降到derived classes 阶层。取而代之的是,对象的作为就像隶属base 类型一样。非正式的说法或许比较传神:在base class 构造期间, virtual 函数不是virtual 函数。这一似乎反直觉的行为有个好理由。由于base class 构造函数的执行更早于derived class 构造函数,当base class 构造函数执行时derived class 的成员变量尚未初始化。如果此期间调用的virtual 函数下降至derived classes 阶层,要知道derived class的函数几乎必然取用local 成员变量,而那些成员变量尚未初始化。这将是一张通往不明确行为和彻夜调试大会串的直达车票。"要求使用对象内部尚未初始化的成分"是危险的代名词,所以C++ 不让你走这条路。
其实还有比上述理由更根本的原因:在derived class 对象的base class 构造期间,对象的类型是base class 而不是derived class。
相同道理也适用于析构函数。一旦derived class 析构函数开始执行,对象内的derived class 成员变量便呈现未定义值,所以C++ 视它们仿佛不再存在。进入baseclass 析构函数后对象就成为一个base class 对象,而C++ 的任何部分包括virtual 函数、dynamic casts 等等也就那么看待它。
其他方案可以解决这个问题。一种做法是在class Transaction 内将logTransaction 函数改为non-virtual,然后要求derived class 构造函数传递必要信息给Transaction 构造函数,而后那个构造函数便可安全地调用non-virtual logTransaction。像这样:
class Transaction {
public:
explicit Transaction(const std::string& logInfo) ;
void logTransaction(const std::string& logInfo) const; // 如今是个non-virtual函数
......
};
Transacton::Transaction(conststd::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 );
};
换句话说由于你无法使用virtual 函数从base classes 向下调用,在构造期间,你可以藉由"令derived classes 将必要的构造信息向上传递至base class 构造函数"替换之而加以弥补。请注意本例之BuyTransaction 内的private static 函数createLogString 的运用。是的,比起在成员初值列(member initialization list) 内给予base class 所需数据,利用辅助函数创建一个值传给base class 构造函数往往比较方便(也比较可读)。令此函数为static ,也就不可能意外指向"初期未成熟之BuyTransaction 对象内尚未初始化的成员变量"。这很重要,正是因为"那些成员变量处于未定义状态",所以"在base class 构造和析构期间调用的virtual 函数不可下降至derived classes" 。
需要记住的
在构造和析构期间不要调用virtual 函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。