前言
本文将讲解一个新手 C++ 程序员经常会犯的错误 - 在构造/析构函数中使用虚函数,并分析错误原因所在以及规避方法。
错误起因
首先,假设我们以一个实现交易的类为父类,然后一个实现买的类,一个实现卖的类为其子类。
这三个类的对象初始化过程中,都需要完成注册的这么一件事情 (函数)。然而,各自注册的具体行为是不同的。
有些人会写出以下这样的代码:
1 class Transaction { 2 public: 3 Transaction(); // 父类构造函数 4 //...... 5 private: 6 //...... 7 void logTransaction() const; // 父类的注册函数 8 //...... 9 }; 10 11 Transaction::Transaction() { 12 //...... 13 logTransaction(); // 父类构造函数调用类内部成员函数 14 //...... 15 } 16 17 class BuyTransaction : public Transaction { 18 private: 19 //...... 20 void logTransaction() const; // 子类一的注册函数 21 //...... 22 }; 23 24 class SellTransaction : public Transaction { 25 private: 26 //...... 27 void logTransaction() const; // 子类二的注册函数 28 //...... 29 };
在这段代码中,编写者认为,子类会继承父类的构造函数,而继承之后,不同的子类又会调用他们自己的实现的注册函数。
这是错误的。
因为在子类调用父类的构造函数期间,子类类型是其父类类型,这个时候执行父类的构造函数其内部调用的注册函数也是父类版本的,而非子类版本的。
错误的解决方案
由于上面所说的错误,一些人想到了虚函数解决方案:
1 class Transaction { 2 public: 3 Transaction(); // 父类构造函数 4 //...... 5 private: 6 //...... 7 virtual void logTransaction() const = 0; // 注册函数声明为虚函数 8 //...... 9 }; 10 11 Transaction::Transaction() { 12 //...... 13 logTransaction(); // 父类构造函数调用类内部成员函数 14 //...... 15 } 16 17 class BuyTransaction : public Transaction { 18 private: 19 //...... 20 virtual void logTransaction() const; // 子类一的注册函数 21 //...... 22 }; 23 24 class SellTransaction : public Transaction { 25 private: 26 //...... 27 virtual void logTransaction() const; // 子类二的注册函数 28 //...... 29 };
很遗憾,这么做还是行不通。一旦你构造一个子类对象,链接器会提示你链接失败 - 调用未定义的纯虚函数。这说明子类构造函数使用的注册函数依然是父类的。
很多人开始吐槽 C++ (第一次碰到这种情况的时候我也是),觉得这样的设定很奇葩。
但其实 C++ 这么设定是有原因的:在父类构造函数执行期间,子类的成员变量并没有初始化完全,因此在此阶段调用子类的成员函数应当被禁止。
正确的解决方案
首先,至此我们要明确:不能在构造函数中使用虚函数了,这么做根本无法实现多态。
然后,采用什么办法能够做到在父类构造函数中以调用成员函数的方式完成初始化呢?
本例中,正确的做法是在父类中将注册函数取消其虚函数声明,而在子类的构造函数中,自行调用父类构造函数并传递进子类对象部分相关信息。当父类构造函数获取到子类部分传递进来的信息之后,就能根据传递进来的信息,有选择的调用相应注册函数。
请看代码示例:
1 class Transaction { 2 public: 3 explicit Transaction(const std::string & logInfo); // 父类构造函数 4 //...... 5 private: 6 //...... 7 void logTransaction(const std::string & logInfo); // 改为非虚函数 8 //...... 9 }; 10 11 Transaction::Transaction(const std::string & logInfo) { 12 //...... 13 // 父类构造函数调用类内部成员函数 注册函数根据不同的logInfo做出不同的初始化处理 14 logTransaction(logInfo); 15 //...... 16 } 17 18 class BuyTransaction : public Transaction { 19 public: 20 BuyTransaction(/*parameters*/); // 子类构造函数 21 //...... 22 private: 23 //...... 24 // 采用静态函数生成子类部分初始化信息,确保不会使用到子类中未完成初始化的数据。 25 static std::string createLogString(/*parameters*/); 26 //...... 27 }; 28 29 // 子类构造函数定义 30 BuyTransaction :: BuyTransaction(/*parameters*/) : Transaction(createLogString(/*parameters*/)) 31 { 32 //...... 33 }
小结
1. 请仔细体会本文的几个类设计过程中所体现出的面向对象思想。
2. 本文焦点是构造函数,但同样适用于析构函数。