条款09:绝不在构造和析构过程中调用virtual函数
此条款,针对的是在构造函数和析构函数中调用虚函数的问题,其内容包括:
a、为什么不要在构造函数/析构函数调用虚函数
b、有什么方法改进
一、为什么不要在构造函数/析构函数调用虚函数
原书中的例子为:
- 创建一个基类Transaction和两个派生类BuyTransaction、SellTransaction
- 基类Transaction有一个虚函数logTransaction(),其会在构造函数中调用虚函数
- 使用基类指针构造一个派生类对象
// 表示不同的事务交易
class Transaction {
public:
Transaction() { logTransaction(); } //记录日志
virtual void logTransaction() const = 0; //记录交易日志, 是个虚函数
};
//购买交易
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
// 销售交易
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
Transaction *pb = new BuyTransaction;
这种情况分析一下,因为:
a、基类的指针构造了一个派生类对象,那么派生类在进行构造时会先执行基类的构造函数
b、基类的构造函数调用了虚函数 logTransaction()
因此造成了想要执行派生类的 logTransaction(),但实际上这个时候派生类还未完成构造,变成了想要执行基类的logTransaction(),可这是个虚函数,编译器是不会允许的
析构函数也是如此,派生类已经析构了,基类再析构,造成调用基类的虚函数的实际效果
二、有什么方法改进
总会有人,我偏偏就要这样做,有没有方法?
作为万能的程序猿,就要拿出解决方案,等效即可,即提交日志的行为因为类的不同而发生改变
原书给了一个错误的解决示例。
class Transaction {
public:
Transaction() { init(); } // 初始化
virtual void logTransaction() const = 0; //记录交易日志, 是个虚函数
private:
void init() {
// 做一些初始化, 比如记录日志等
logTransaction();
}
};
此问题看上去是等效了,但实际还是执行基类的logTransaction(),那个虚函数,编译器只会两手一摊,给你报错
正确的解决方案是:
- 在 Transaction 类中将 logTransaction() 函数改为 non-virtual 函数
- 要求子类构造函数传递必要信息给 Transaction 构造函数
取原书例子:
class Transaction {
public:
explicit Transaction(const std::string& logInfo) { logTransaction(logInfo) }
// 初始化日志信息
void logTransaction(const std::string& logInfo) const;
};
class BuyTransaction : public Transaction
{
public:
// 构造自己时同时初始化基类
BuyTransaction(相关参数): Transaction(createLogString(相关参数)) {}
private:
static std::string createLogString(相关参数);
};
class SellTransaction : public Transaction
{
public:
// 构造自己时同时初始化基类
SellTransaction(相关参数): Transaction(createLogString(相关参数)) {}
private:
static std::string createLogString(相关参数);
};
这一做法,初读的时候还是有些迷糊的,后来仔细分析了一下才有些了然。
个人认为,此做法针对的是 “ 先基类构造,后派生类构造” 的虚函数问题,将其变为 “ 先派生类构造,后基类构造 ” 的普通成员函数传参处理,以此达到原来大致相同的效果
三、总结
在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)
此部分,个人觉得还有一个点要关注,在于解决问题的一个思路,需求不会变,就要确定目的,制造等效方法,此例,给出的 调整冲突顺序 就是制造整体等效的方法之一,这些都是说起来简单,做起来难的地方