假设有个类的继承体系,用于建模股票交易,例如买入订单、卖出订单等。此类交易是可审计的,因此每次创建交易对象时,都需要在审计日志中创建适当的条目。
class Transaction { // 所有交易的基类
public:
Transaction();
virtual void logTransaction() const = 0; // 创建依赖于类型的日志条目
...
};
Transaction::Transaction() // 基类构造函数的实现
{
...
logTransaction(); // 最后,记录这笔交易
}
class BuyTransaction : public Transaction { // 派生类
public:
virtual void logTransaction() const; // 记录这种类型的交易
...
};
//考虑一下执行这段代码时会发生什么:
BuyTransaction b;
- 派生类对象的基类部分在派生类部分之前构造。
- 在构造基类时,虚函数永远不会进入派生类。
- 在运行基类构造函数时,派生类数据成员没有初始化。
- 而派生类函数几乎肯定会引用局部数据成员。
- 在派生类构造函数开始执行之前,对象被视为基类对象。
- 同样的道理也适用于析构函数。
检测虚函数在构造或析构期间的调用并不总是那么容易。如果类有多个构造函数,每个构造函数都必须执行一些相同的工作,将它们放在私有的非虚拟初始化函数中,以避免代码复制:
class Transaction {
public:
Transaction()
{
init();// 调用 non-virtual...
}
virtual void logTransaction() const = 0;
...
private:
void init()
{
...
logTransaction(); // ...这里调用virtual!
}
};
解决这个问题有很多方法。一种是将logTransaction转换为非虚函数,然后要求派生类构造函数将必要的日志信息传递给基类构造函数。然后该函数可以安全地调用非虚的logTransaction:
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;//现在不是虚函数
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // 现在调用的不是虚函数
}
class BuyTransaction : public Transaction {
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters)) // 将日志信息
// 传递给基类构造函数
{
...
}
...
private:
static std::string createLogString(parameters);
};
- 由于不能再基类的构造函数中使用虚函数向下调用,可以通过让派生类将必要的构造信息向上传递给基类的构造函数来进行补偿。
- 使用辅助函数来创建一个值并传递给基类的构造函数,通常比通过修改成员初始化列表来提供基类所需的值更方便(可读性也更好)。
- 通过将函数设置为静态,就不会有意外引用BuyTransaction对象尚未初始化的数据成员的危险。
不要在构造或析构过程中调用虚函数,因为这样的调用永远不会下降至派生的类(相对于正在执行的构造函数或析构函数的那一层)。