构造函数与析构函数中不调用虚函数

时间:2021-02-27 19:27:31

本文参考《effective C++》第九条款
在C++中,提倡不能在构造函数和析构函数中调用虚函数。
这是为什么呢?

首先,我们先回顾一下C++虚函数的作用。 虚函数的引入是c++运行时多态的体现,通过调用虚函数可以在运行程序时实现动态绑定,体现了面向对象编程多态的思想。


那为何提倡不能在构造函数与析构函数中不能调用虚函数。接下来我们通过代码分析在构造函数或者虚构函数中调用虚函数是否可行。

假设我们有两种商品A, B。 我们要记录着两种商品的销售记录,每当销售一种商品时,我们就要记录下来。

class item {
public:
    item();
    virtual void saleRecord() const=0;  //销售记录,纯虚函数
    ...
};

item::item()
{
    ...
    virtual void saleRecord();
    ...
}

class itemA :  public item {
public:
    itemA();
    virtual void saleRecord();
    ...
};

class itemB : public item {
public:
    itemB();
    virtual void saleRecord();
    ...
};

我们执行如下代码:

itemB b;

一个derived class B 对象会被创建, 在调用构造函数itemB() 之前, 基类的构造函数会首先被调用,即:基类成分会在派生类特有成分被构造之前被构造。 而在基类的构造函数中又调用了虚函数saleRecord(), 在此,你认为基类构造函数中所调用的虚函数saleRecord的实现版本是哪一个呢,是基类的实现版本还是派生类B的实现版本呢?

我们会很自然的认为, 现在构建的是itemB的对象, 构造函数中调用的当然是派生类B的实现版本。 其实我们可以通过运行程序确认基类构造函数中调用的虚函数实现版本是基类所有的,而不是派生类的实现版本。即使我们现在创建的是一个派生类。

原因其实很简单,在创建派生类的时候,基类先于派生类被构造,编译器或者程序其实目光是很短浅的或者说是很现实的,在调用基类构造函数的时候,它并不知道你最终是要创建一个基类还是派生类, 它只要把现在手头上的工作做好——创建一个基类。 对编译器来说,此时所有可见的信息包括data member 以及data function 都是基类所有的,它并不知道派生类的任何信息, 而且它所做的行为也只是初始化派生类空间专属于基类的那一部分, 不会越界。用一句话总结: 对象在调用什么构造函数的时候,它就是什么对象,他并不会像先知一样能看的更远。
以之前的代码为例。 创建派生类的时候, 基类首先被创建, 此时它只是一个基类,它的所见所为都完完全全跟创建一个基类对象一样,并不会下降到派生类。


我们可以极端一点,假设基类在创建的时候可以调用虚函数的派生类实现版本(其实不可能)。 此时,又会发生什么呢。从意图上讲,我们要调用虚函数的不同版本,从根本上讲,是要对不同的数据进行操作, 这样函数才有意义。比如说基类的虚函数版本是对基类的数据进行操作, 派生类的虚函数版本是对派生类的虚函数进行操作。

然而,我们不要忽略一点,假设基类可以调用虚函数的派生类实现版本,那么我们操控的数据则是派生类所有的数据, 此时,派生类的构造函数还没有调用,派生类的数据成员也不会有相应的初始化,这时候对派生类的数据成员操作完全是无意义的,从程序崩溃到机器冒烟都有可能。

对于析构函数, 与构造函数是几乎一样的,不能调用虚函数。

从上面的分析可以得出两点结论:
1. 构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别。
2. 即使构造函数或者析构函数如果能成功调用虚函数, 程序的运行结果也是不可控的