完整构造函数的初始化顺序

构造函数分为完整构造函数(Complete Object Constructor)和基类构造函数(Base Object Constructor)。

初始化顺序

显式构造一个对象的时候调用的是完整构造函数。该函数执行的初始化流程如下:

  1. 初始化所有基类子对象。基类子对象包括有直接关系的具体继承基类,也包括有直接或间接关系的虚基类。其中虚基类先于具体基类被初始化,尽管虚基类子对象被放在对象的最后面。
  2. 初始化自己的 vptr。
  3. 按声明顺序初始化成员。若在初始化列表中提供了初始化规则,则使用;否则,如果在声明时给出了初始化表达式,则使用;否则,采用默认初始化。
  4. 执行用户提供的代码块中的代码。

可以参考书中第 216 页。

如何初始化虚基类

对于每个直接或间接拥有虚基类的类,其构造函数都必须在成员初始化列表中调用虚基类的构造函数来初始化它。如果不显式调用虚基类构造函数,则视为使用其默认构造函数。下面的代码中取消注释 VirtualBase() = default; 并删掉 C(): A(), B(), VirtualBase("C") {} 中的 VirtualBase("C"),则代码仍能编译。

#include <iostream>

struct VirtualBase {
    VirtualBase(const char *s) {
        printf("VirtualBase(%s)\n", s);
    }
    // VirtualBase() = default;
};

struct A : virtual VirtualBase {
    A(): VirtualBase("A") {
        puts("A()");
    }
};

struct B : virtual VirtualBase {
    B(): VirtualBase("B"){
        puts("B()");
    }
};

struct C : A, B {
    C(): A(), B(), VirtualBase("C") {}
};

int main() {
    C c;
}

vptr

由于构造函数和析构函数执行过程中会逐层修改 vptr,这两类函数中调用的虚函数只能使用同类型函数版本。编译器甚至可以优化代码,去掉经由 vptr 的虚函数调用操作(反正通过虚函数调用机制能得到的函数也是可确定的)。