声明:
- 文中内容收集整理自《Effective C++(中文版)第三版》,版权归原书所有。
- 本内容在作者现有能力的基础上有所删减,另加入部分作者自己的理解,有纰漏之处敬请指正。
条款09:绝不在构造和析构过程中调用virtual函数
Never call virtual functions during construction or destruction.
class Transaction //所有交易的基类
{
public:
Transaction();
virtual void logTransaction() const = 0;//做出一份因类型不同而不同
//的日志记录
};
Transaction::Transaction() //基类构造函数的实现
{
logTransaction(); //日记记录这笔交易
}
class BuyTransaction : public Transaction //继承类
{
public:
virtual void logTransaction() const; //日志记录此交易
};
//derived class
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const; //日志记录此交易
}; //执行以下
BuyTransaction b;
有一个BuyTransaction构造函数被调用,但首先Transaction构造函数一定会更早被调用,Transaction构造函数里调用了虚拟函数logTransaction,这正是引发惊奇的起点。这时候被调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本。基类构造期间虚拟函数绝不会下降到继承类阶层,取而代之的是,对象的作为就像隶属基类类型一样,在基类构造期间,虚函数不是虚函数。
由于基类构造函数的执行更早于继承类构造函数,当基类构造函数执行时继承类的成员变量尚未初始化。如果此期间调用的虚函数下降到继承阶层,要知道继承类的函数几乎必然取用local成员变量,而这些变量尚未初始化。“要求使用对象内部尚未初始化的成分”是危险的。
根本原因是:在继承类对象的基类构造期间,对象的类型是基类而不是继承类。对象在derived class构造函数开始执行前不会成为一个derived class对象。
相同的道理也是用于析构函数。
如果类有多个构造函数,每个都需要执行某些相同的工作,那么避免代码重复的一个优秀做法是把共同的初始代码放进一个初始化函数如init内。
但你如何确保每次一个Transaction继承体系上的对象被创建,就会有适当版本中的logTransaction被调用呢?很显然,在Transaction构造函数内对着对象调用虚拟函数是一种错误做法。
解决办法 一种是在类 Transaction内将logTransaction函数改为non-virtual,然后要求继承类构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全调用non-virtual logTransaction。
class Transatcion
{
public:
explicit Transaction(cosnt std::string& logInfo);
void logTransaction(const std::string& logInfo) const; //non-virtual
};
Transaction::Transaction(const std::string& logInfo)
{
logTransaction(logInfo); //non-virtual调用
}
class BuyTransaction:public Transaction
{
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters)) {} //将log信息传递给基类构造函数
private:
static std::string createLogString(parameters);
};
换句话说由于你无法使用虚拟函数从基类向下调用,在构造期间,你可以藉由“令继承类将必要的构造信息向上传递至基类构造函数”替换之而加以弥补。
请记住:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class。