effective C++ 条款 9:绝不在析构和构造函数中调用virtual函数
每当创建一个交易对象,在审计日志中也要创建一笔适当记录。下面是一个看起来比较合理的做法:
class Transaction
{
public:
Transaction()
{
init();
}
virtual void logTransaction() const { std::cout << "transaction" ;};
protected:
private:
void init()
{
logTransaction();
}
};
class BuyTransaction : public Transaction
{
public:
BuyTransaction():a(5){}
virtual void logTransaction() const { std::cout << "BUytransaction"<< a;};
protected:
private:
int a;
};
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const;
protected:
private:
};
inline void testItem9()
{
BuyTransaction b;
}
base class构造期间virtual函数绝不会下降到derived classes阶层。取而代之的是,对象的作为就像隶属base类型一样。
非正式的说法:在base class构造期间,virtual函数不是virtual函数。
由于base class 的构造函数的执行更早于derived class构造函数,当base class的构造函数执行时,derived class的成员变量尚未初始化。如果期间调用的virtual函数下降至derived class阶层, derived class的函数几乎必然取用local成员变量,而那些成员变量尚没有初始化。这会导致不明确行为和彻夜调试。
其实还有更根本的原因,在derived对象的base class 构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息(runtime type information,例如dynamic_cast和typeid),也会把对象视为base class类型。本例中当Transaction构造函数正执行起来打算初始化“BuyTransaction对象内的base class成分”时,该对象的类型是Transaction。这个对象内的“BuyTransaction专属成分”尚未被初始化,所以面对这些,最安全的做法是视他们不存在。对象在derived class构造函数开始执行之前不会成为一个derived class对象。
相同道理也适用于析构函数。一旦derived class 析构函数开始执行, 对象内的derived class成员变量便呈现未定义值,
所以c++视他们仿佛不存在,进入base class析构函数后,对象就变成一个base class对象。
如何确保每次一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?在构造函数中调用virtual函数是一种错误的做法。
其他方案可以解决这个问题。一种做法是在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;
protected:private:
};
Transaction::Transaction(const std::string &logInfo)
{
logTransaction(logInfo)
}class BuyTransaction : public Transaction
{
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters))
{
...;
}
protected:
private:
static std::string createLogString(parameters);
int a;
};换句话说由于你无法使用virtual函数从base class向下调用,在构造期间,你可以藉由“令derived class将必要的构造信息向上传递至base class构造函数”替换之而加以弥补。
注意本例之BuyTransaction内的private static函数createLogString的运用。是的,比起在成员初始化列表里给予base class
所需数据。利用辅助函数创建一个值传给base class构造函数往往比较方便。令此函数为static,也就不可能意外指向“初值未成熟的BuyTransaction对象内尚未初始化的成员变量”。
在构造函数和析构函数期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造和析构的那层)。