图解C++菱形继承、虚继承对象的内存分布

目录

菱形继承的内存分布

虚继承的内存分布

虚继承(有虚函数)的内存分布

菱形继承的构造/析构顺序

虚继承的构造/析构顺序


菱形继承的内存分布

假设继承体系如左图,则 D 的对象内存布局如右图。

【结论】D的对象可以分成3块,每个直接基类(B和C)各占一块,按继承的顺序排放,最后是派生类(D)自己的成员变量。而B和C的数据块中都拷贝了一份间接基类A的数据。

 代码验证:

class A{
public:
    int d = -1;
    int i = 1;
    char c = 'a';
    void f(){cout<<"A::f()"<<endl;}
};
class B: public A{
public:
    int i = 2;      // 覆盖基类
    char c = 'b';   // 覆盖基类
    void f(){cout<<"B::f()"<<endl;}
};
class C: public A{
public:
    char c = 'c';   // 覆盖基类
    int i2 = 3;
    void f(){cout<<"C::f()"<<endl;}
};
class D: public B, public C{
public:
    int j = 4;
    char c = 'd';   // 覆盖基类
};

int main(){
    D x;
    cout<<&x<<" size:\t"<<sizeof(x)<<endl<<endl;

    // A的所有成员都不能显式访问 error: 'A' is an ambiguous base of 'D'
    // x.A::f(); cout<< x.A::d << x.A::c << x.A::i;

    x.B::f();
    cout<<"B::d  "<<&x.B::d       <<"\t"<<x.B::d<<endl
        <<"B::i  "<<&x.B::i       <<"\t"<<x.B::i<<endl
        <<"B::c  "<<(void*)&x.B::c<<"\t"<<x.B::c<<endl<<endl;      // ok

    x.C::f();
    cout<<"C::d  "<<&x.C::d       <<"\t"<<x.C::d<<endl
        <<"C::i  "<<&x.C::i       <<"\t"<<x.C::i<<endl
        <<"C::c  "<<(void*)&x.C::c<<"\t"<<x.C::c<<endl
        <<"C::i2 "<<&x.C::i2      <<"\t"<<x.C::i2<<endl<<endl;     // ok

    cout<<"D::i2 "<<&x.D::i2      <<"\t"<<x.D::i2<<endl
        <<"D::j  "<<&x.D::j       <<"\t"<<x.D::j<<endl
        <<"D::c  "<<(void*)&x.D::c<<"\t"<<x.D::c<<endl<<endl;      // ok

    // D中定义(或覆盖)的成员、继承的唯一性成员可以直接访问,等价于使用 D::
    cout<<"x.i2  "<<&x.i2         <<"\t"<<x.i2<<endl
        <<"x.j   "<<&x.j          <<"\t"<<x.j<<endl
        <<"x.c   "<<(void*)&x.c   <<"\t"<<x.c<<endl<<endl;

    // 有二义性的不能访问
    // cout<<x.d    <<x.i;          // ambiguous
    // cout<<x.D::d <<x.D::i;       // ambiguous
    // x.f(); x.D::f();             // ambiguous

    // 打印不能直接访问的地址
    int* p = &x.B::d;
    cout<<p+1<<"\t"<<*(p+1)<<endl
        <<p+2<<"\t"<<*(char*)(p+2)<<endl;
    p = &x.C::i;
    cout<<p+1<<"\t"<<*(char*)(p+1)<<endl;
}

运行结果:

 

虚继承的内存分布

在上面的菱形继承中,如果直接访问间接基类A的成员,就会出现二义性调用的错误。因为B和C都有一份拷贝,所以必须使用作用域限定符,指定使用哪个直接基类的数据。而使用虚继承可以消除对间接基类A的重复拷贝,使得D的对象中只有一份拷贝。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

使用虚继承得到的派生类(B和C),它的对象中有隐藏的虚基类表指针(vbptr)指向虚基类表(vbtable),表中记录了虚基类表指针与虚基类之间的偏移量。而虚基类部分被放到对象内存的最后面。

虚基类表中存放了2个偏移量:

  • 第1个是当前的vbptr 相对于虚函数表指针(vfptr)的偏移量(由于这里没有定义虚函数,也就没有虚表指针,所以偏移量为0);
  • 第2个是当前的vbptr 相对于虚基类(A)部分的偏移量。

 

假设继承体系如左图,则 D 的对象内存布局如右图。 

代码验证:

class A{
public:
    int d = -1;
    int i = 1;
    char c = 'a';
    void f(){cout<<"A::f()"<<endl;}
};
class B: virtual public A{
public:
    int i = 2;      // 覆盖基类
    char c = 'b';   // 覆盖基类
};
class C: virtual public A{
public:
    char c = 'c';   // 覆盖基类
    int i2 = 3;
    void f(){cout<<"C::f()"<<endl;}
};
class D: public B, public C{
public:
    int j = 4;
};

int main(){
    D x;
    cout<<&x<<" size:\t"<<sizeof(x)<<endl<<endl;

    x.B::f();
    cout<<"B::d  "<<&x.B::d       <<"\t"<<x.B::d<<endl
        <<"B::i  "<<&x.B::i       <<"\t"<<x.B::i<<endl
        <<"B::c  "<<(void*)&x.B::c<<"\t"<<x.B::c<<endl<<endl;      // ok

    x.C::f();
    cout<<"C::d  "<<&x.C::d       <<"\t"<<x.C::d<<endl
        <<"C::i  "<<&x.C::i       <<"\t"<<x.C::i<<endl
        <<"C::c  "<<(void*)&x.C::c<<"\t"<<x.C::c<<endl
        <<"C::i2 "<<&x.C::i2      <<"\t"<<x.C::i2<<endl<<endl;

    x.D::f();
    cout<<"D::d  "<<&x.D::d       <<"\t"<<x.D::d<<endl
        <<"D::i  "<<&x.D::i       <<"\t"<<x.D::i<<endl
        //<<"D::c  "<<(void*)&x.D::c<<"\t"<<x.D::c<<endl        // ambiguous
        <<"D::i2 "<<&x.D::i2      <<"\t"<<x.D::i2<<endl
        <<"D::j  "<<&x.D::j       <<"\t"<<x.D::j<<endl<<endl;

    x.A::f();
    cout<<"A::d  "<<&x.A::d       <<"\t"<<x.A::d<<endl
        <<"A::i  "<<&x.A::i       <<"\t"<<x.A::i<<endl
        <<"A::c  "<<(void*)&x.A::c<<"\t"<<x.A::c<<endl<<endl;

    x.f();
    cout<<"x.d   "<<&x.d          <<"\t"<<x.d<<endl
        <<"x.i   "<<&x.i          <<"\t"<<x.i<<endl
        //<<"x.c   "<<(void*)&x.c   <<"\t"<<x.c<<endl           // ambiguous
        <<"x.i2  "<<&x.i2         <<"\t"<<x.i2<<endl
        <<"x.j   "<<&x.j          <<"\t"<<x.j<<endl<<endl;
}

运行结果:

 

【结论】

  • 虚基类成员的可见性:因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。假设 B 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
    • 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 B 的成员,此时不存在二义性。
    • 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
    • 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。
  • 使用 x. 和 x.D:: 访问是等价的。

  

虚继承(有虚函数)的内存分布

关于带虚函数的类的内存分布,详见:图解C++虚函数表和类对象的内存分布

这里分析既有虚函数又有虚继承的类的内存分布。

假设继承体系如左图(所有函数均为虚函数),则Derive对象的内存布局如右图。 

【结论】

  • 每个基类(直接基类B1,B2、虚基类A)都有一个虚函数表指针(vfptr),而直接基类(B1,B2)又都有一个虚基类表指针(vbptr),虚函数表指针在虚基类表指针的前面。
  • 派生类Derive定义的虚函数保存在第一个虚函数表中。
  • 每个类的虚函数表都只保存自己新定义的虚函数,继承或重写的虚函数保存在最早定义这个虚函数的类的虚函数表中
  • 上一条可知,每个虚函数只出现在一个虚函数表中。
  • 如果虚函数表中的某个函数被子类重写,则表中保存子类版本。

代码验证:

class A {
public:
	virtual void f() { cout << "A::f" << endl; }
	virtual void i() { cout << "A::i" << endl; }
	virtual void j() { cout << "A::j" << endl; }
	int a;
};

class B1 : virtual public A {
public:
	virtual void f() { cout << "B1::f" << endl; }
	virtual void h1() { cout << "B1::h1" << endl; }
	int b1;
};

class B2 : virtual public A {
public:
	virtual void i() { cout << "B2::i" << endl; }
	virtual void h2() { cout << "B2::h2" << endl; }
	int b2;
};

class Derive : public B1, public B2 {
public:
	virtual void f() { cout << "Derive::f" << endl; }
	virtual void h2() { cout << "Derive::h2" << endl; }
	virtual void g() { cout << "Derive::g" << endl; }
	int c;
};

用VS查看Derive内存分布

                                                   

【注】不同IDE下的内存分布可能不同。比如在CodeBlocks下Derive对象只有28字节,少了B1和B2的vbptr,可能把虚基类表放到了vfptr所指的虚函数表后面(即vfptr和vbptr合并成一个指针,虚基类表和虚函数表合并成一张表)。

 

菱形继承的构造/析构顺序

因为菱形继承的 D 是多继承,所以要遵循多继承的构造/析构顺序,即按继承顺序调用相应基类(B、C)的构造函数,而初始化B、C之前又要分别调用它们的基类(A)的构造函数,所以构造和析构顺序如下:

A()
B()
A()
C()
D()

~D()
~C()
~A()
~B()
~A()

代码验证: 

class A{
public:
    A(){cout<<"A()"<<endl;}
    ~A(){cout<<"~A()"<<endl;}
};
class B: public A{
public:
    B(){cout<<"B()"<<endl;}
    ~B(){cout<<"~B()"<<endl;}
};
class C: public A{
public:
    C(){cout<<"C()"<<endl;}
    ~C(){cout<<"~C()"<<endl;}
};
class D: public B, public C{
public:
    D(){cout<<"D()"<<endl;}
    ~D(){cout<<"~D()"<<endl;}
};

int main(){
    D x;
    cout<<"sizeof(D): "<<sizeof(D)<<endl;
}

运行结果如下。注意到D的对象大小为2字节,这是因为本例中的所有类都是空类,C++规定空类必须占1字节,所以这2个字节分别来自B、C。

 

虚继承的构造/析构顺序

先初始化虚基类(A),再按继承顺序初始化直接基类(B、C)。若有多个虚基类,按声明的顺序依次初始化。析构顺序正好相反。

因此本例的构造/析构顺序为:

A()
B()
C()
D()

~D()
~C()
~B()
~A()

代码验证:

class A{
public:
    A(){cout<<"A()"<<endl;}
    ~A(){cout<<"~A()"<<endl;}
};
class B: virtual public A{
public:
    B(){cout<<"B()"<<endl;}
    ~B(){cout<<"~B()"<<endl;}
};
class C: virtual public A{
public:
    C(){cout<<"C()"<<endl;}
    ~C(){cout<<"~C()"<<endl;}
};
class D: public B, public C{
public:
    D(){cout<<"D()"<<endl;}
    ~D(){cout<<"~D()"<<endl;}
};

int main(){
    D x;
    cout<<"sizeof(D): "<<sizeof(D)<<endl;
}

运行结果如下。注意到D的对象大小为8字节,这是因为它的内存中有2个指针(4字节),分别对应直接基类B和C。

 【例2】一个更复杂的继承体系:

F的构造和析构顺序:

C()
E()
A()
B()
D()
F()

~F()
~D()
~B()
~A()
~E()
~C()


 

  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值