1.3 c++虚基类的用途以及内存模型

1.3 虚基类

1.3.1 虚基类(菱形继承)的语法实现

对于如下的继承体系,定义了一个公共基类A。类B和类C都由类A公有派生,类D由类B和类C公有派生。

Alt
其示例代码如下所示,这段代码的45行是无法通过编译器的,这即是多重继承存在的一个问题:存在二义性。对象B和对象C里都有保存一个对象A,导致编译器不知道访问哪一个。不仅如此,由于基类对象A在派生类对象B,C中都有存储,会造成存储空间的浪费。这时在vs的命令行使用命令cl /d1reportSingleClassLayoutD CppObjectModelV.cpp,可以得到其空间存储如下所示,可以发现类D内部会存储两个类A的对象:
Alt

class A {

public:
    int x;

    void SetX(int a) { x = a; }

    explicit A(int a = 0) : x(a) { cout << "调用A构造函数" << endl; }

    void print_a() { cout << "PA" << x << endl; }
};

class B : public A {
public:
    int y;

    void SetY(int a) { y = a; }

    explicit B(int a = 0, int b = 0) : A(a), y(b) { cout << "调用B构造函数" << endl; }

    void print_b() { cout << "PB" << "x=" << x << "y=" << y << endl; }
};

class C : public A {
public:
    int z;

    void SetZ(int a) { z = a; }

    explicit C(int a = 0, int b = 0) : A(a), z(b) { cout << "调用C构造函数" << endl; }

    void print_c() { cout << "PC" << "x=" << x << "z=" << z << endl; }
};

class D : public B, C {
    int m;
public:
    explicit D(int a = 0, int b = 0, int c = 0, int d = 0, int e = 0) : B(a, b), C(c, d), m(e) {
        cout << "调用D构造函数" << endl;
    }

    void print_d() {
        print_b();
        print_c();
        cout << "x=" << x << "y=" << y << "z=" << z << endl;
    }
};

若要在菱形继承中,希望公共基类在派生类中只有一个拷贝,则可将该基类作为虚基类进行继承,即在定义派生类时,在继承的公共基类类名前加上关键字virtual,其余保持不变即可。新定义后就不会再出现编译错误,其代码如下所示:

class A {

public:
    int x;

    void SetX(int a) { x = a; }

    explicit A(int a = 0) : x(a) { cout << "调用A构造函数" << endl; }

    void print_a() { cout << "PA " << x << endl; }
};

class B : virtual public A {
public:
    int y;

    void SetY(int a) { y = a; }

    explicit B(int a = 0, int b = 0) : A(a), y(b) { cout << "调用B构造函数" << endl; }

    void print_b() { cout << "PB " << "x= " << x << " y= " << y << endl; }
};

class C : virtual public A {
public:
    int z;

    void SetZ(int a) { z = a; }

    explicit C(int a = 0, int b = 0) : A(a), z(b) { cout << "调用C构造函数" << endl; }

    void print_c() { cout << "PC " << " x= " << x << " z= " << z << endl; }
};

class D : public B, C {
    int m;
public:
    explicit D(int a = 0, int b = 0, int c = 0, int d = 0, int e = 0) : B(a, b), C(c, d), m(e) {
        cout << "调用D构造函数" << endl;
    }

    void print_d() {
        cout << "x= " << x << " y= " << y << " z= " << z << " m= " << m << endl;
    }
};

int main() {
    D d(1, 2, 3, 4, 5);
    d.print_d();
    return 0;
}
1.3.2 菱形继承内存模型

在虚拟继承的情况下,类B和类C的结构分别如下所示:注意因为这里采用了虚拟继承,所以类B和类C都会多产生一个虚基表指针,指向一个虚基表。此虚基表的作用是记录基类的数据在子类内存中存储位置(距离子类起始位置的偏移量),同时在虚基表的第一项也存储了基类虚基表指针距离此基类起始位置的偏移量。
如下所示在子类B的内存起始位置即存储了一个4个字节大小的虚基表指针vbptr,然后存储B自己内部的数据y,然后存储基类A。可以发现虚继承时,基类是存储在子类内存里的尾端的。同时虚基表指针vbptr指向B的虚基表,此虚基表的第一项存储了虚基表指针距离基类的偏移值,显然vptr存储在类B的头部,因此偏移值是0。第二项记录了基类A的数据项x距离基类起始地址的偏移值,这里显然是8。因为类C和类B的内存一致,所以这里不再重复分析。
其实虚基类只要没有虚函数,则其第一项通常都是0,如下所示:
Alt

Alt

Alt

如下所示是D类的内存模型,可以发现这时D会含有两个虚基表与两个虚基表指针,其表内容分析和上述一样。也可以参考博客:
图说C++对象模型:对象内存布局详解

Alt

可以看到这时不再会出现编译报错,但是可以发现x的结果是0不是1。这是因为类D的构造函数调用了BC的构造函数,类BC又调用了类A的构造函数,但是因为由于虚基类AD中只有一个拷贝,编译器无法确定是由B还是C的构造函数调用A的构造函数。因此编译器约定,执行BC的构造函数时不调用虚基类A的构造函数,而在类D的构造函数中直接调用A的默认的构造函数。
如果派生类继承了多个基类,基类有虚基类和非虚基类,那么在创建该派生类的对象时,先调用虚基类的构造函数,然后调用非虚基类的构造函数,最后调用派生类的构造函数。若虚基类有多个,则虚基类构造函数的调用顺序取决于它们继承时的说明顺序。
Alt

1.3.3 虚基表存在的原因

可以看到,为了实现虚继承,必须定义相应的虚继承表用于指向虚基类在继承类中的偏移量,虚基类表与虚基类指针的设定都是在编译阶段由编译器生成相应的默认构造函数完成的。
若编译器已经知道了所有的偏移量,也就是D在最终的内存构造,那么所有的偏移量都可以在编译阶段计算出来,则虚基表存在的意义是什么?
还是对于上述的继承模型,若存在如下函数f,则对于第二行的调用,显然在编译期间编译器并不知道会生成哪个实际对象,同时若在函数f内有对基类变量的操纵,显然因为编译期间无法确定实际生成的是对象B还是对象D,那也就无法在此处定义此基类变量的实际偏移量,因此必须使用虚基表来定位这个偏移量。

A* f(B* b) {//……}
B* b = (rand() % 2 == 0) ? new B : new D; f(b);

虚函数表的作用:
Why does virtual inheritance need a vtable even if no virtual functions are involved?

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值