Skip to content

Latest commit

 

History

History
86 lines (72 loc) · 3.36 KB

File metadata and controls

86 lines (72 loc) · 3.36 KB

条款09:绝不在构造和析构过程中调用virtual函数

假设你有一个class继承体系,用来建模股市交易如买进、卖出的订单等等。这样的交易一定要通过审核,所以每当创建一个交易对象,在审核日志中也需要创建一笔适当记录。下面是一个看起来颇为合理的做法:

class Transaction {
public:
  Transaction();
  virtual void logTransaction() const = 0;
  ...
};
Transaction::Transaction() {
  ...
  logTransaction();
}
class BuyTransaction : public Transaction {
public:
  virtual void logTransaction() const;
  ...
};
class SellTransaction : public Transaction {
public:
  virtual void logTransaction() const;
  ...
};

现在,当下面这行被执行,会发生什么事:

BuyTransaction b;

BuyTransaction构造函数会被调用,但在此之前,Transaction构造函数会被调用。此时调用的logTransaction是Transaction内的版本,即使目前即将建立的对象类型是Transaction。在基类构造期间,virtual函数不是virtual函数。

相同的道理也适用于析构函数。一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值,所以C++视他们不存在。

上述示例中,Transaction构造函数直接调用一个virtual函数,这很明显看出违反本条例。logTransaction在Transaction中是个纯虚函数,链接器无法找到它的定义,如果调用会报错。

但是侦测构造函数或析构函数运行期间是否调用virtual函数并不总是这么简单。如果Transaction由多个构造函数,每个都要执行某些相同的工作,那么避免代码重读的一个优秀做法是把共同的初始化代码放进一个初始化函数内:

class Transaction {
public:
  Transaction() { init(); }
  virtual void logTransaction() const = 0;
  ...
private:
  void init() {
    ...
    logTransaction();
  }
};

这段代码不会引发编译器和链接器的报错。由于logTransaction是Transaction中的一个纯虚函数,当纯虚函数被调用,程序会终止。然而如果logTransaction是个virtual函数并在logTransaction中有实现代码,该版本就会被调用。唯一能够避免此问题的做法就是:确定你的构造函数和析构函数都没有调用virtual函数,而他们调用的所有函数也都服从同一约束。

如何确保每一次Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用共呢?一种做法是在Transaction内将logTransaction函数改为non-virtual,然后要求派生类构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtual 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(params)
    : Transaction(createLogString(params))
  {...}
  ...
private:
  static std::string createLogString(params);
};

在构造期间,你可以令派生类将必要的构造信息向上传递至基类构造函数以完成操作。

请记住

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类。