内存首地址为1000h_C++虚继承,菱形继承,内存分布

前言

在叙述C++虚继承之前,我先给大家抛出一个问题。例如现在有4个类,分别是class A, class B, class C, class D。它们的关系如下图。

b98b8d3bd0fa1b1d732adad621849735.png

如上如所示,class B和class C都继承class A;class D又继承class B和C。

我们用代码展示如下。

class A {public: void fun() { std::cout << "class A fun" << std::endl; } int data; };class B : public A { };class C : public A { };class D : public B, public C {};

如上面代码所示,当class B和class C都继承了class A。由单继承的原理我们可知,类B和类C中都继承了类A的成员变量data。所以类B的对象大小和类c的对象大小都为4。那么当类D

继承类B和类C,按照单继承的原理,派生类是要继承基类的public成员变量,因此类D的对象中会有来自类B的成员变量data,和类C的成员变量data。

因此,当实例化一个class D的时候,其对象的大小应该为8。其内存分布图为

4e3340ba5344c24107963c8cc5b648cb.png

但是呢,如果我们需要对data进行操作,我们可以直接写成下面这样吗?

D d; //实例化对象d.data = 100; //?可以这样吗?//我们用编译器编译后,报出了如下的错误。//error: request for member ‘data’ is ambiguous//表明我们使用的data是不明确的,模棱两可的

因为对象d中有2个data,如果我们只调用data,编译器是不知道我们到底要使用哪一个data。所以我们可以使用::(域限定符号)来表明我们要使用哪一个data。比如

D d;d.B::data = 100;d.C::data = 200;

貌似,我们好像已经解决了这个“二义性”的问题,但是深究这个二义性,其实更深层次的是因为公共基类A发生了两次实例化问题。

虚继承

虚继承的实现是在继承的时候加上关键字virtual。它的作用是只会在最后的派生类中将虚基类的构造函数调用一次,忽略虚基类的中间派生类对虚继承的构造函数的调用,从而保证虚基类的数据成员不会被初始化多次。

所以,通过虚继承,我们也能很好的解决多重继承下公共基类的多份拷贝问题.

例如

class A {public: void fun() { std::cout << "class A fun" << std::endl; } int data; };class B : virtual public A { };class C : virtual public A { };class D : public B, public C { };

如上面代码所示,class B和class C对class A使用了虚继承。那么当class D继承class B和C的时候,只能实例化一份class A的对象。所以D的对象中只有一份data数据。其内存分布如下

832a2d2aabbe241bb81f429ce83957f0.png

所以可以直接对类D的对象进行如下操作

D d;d.data = 100;std::cout << "data = " << data << std::endl; //100

注意

1. 虽然是虚基类,但是依然可以使用基类的指针或者引用来指向派生类的对象。

2. 虚继承只是解决了多重继承中公共基类被实例化多次的问题

虚继承下派生类的对象内存分布分析。

这一节是延续我之前写的《C++虚函数继承之对象内存分布》这篇文章,其实也是本篇文章的重点。

虚继承的派生类的内存布局与普通继承有很多不同,主要体现如下:

1. 虚继承的子类,如果本身定义了虚函数,则编译器会为其生成一个虚函数指针(vptr)及虚函数表。该vptr在对象内存的最前面。这里跟普通的继承就有区别了:普通的继承如果基类有虚函数,那么派生类会在基类的虚表之后继续扩展基类的虚表,与基类共用一个虚函数指针

2. 虚继承的子类会单独保留基类的虚函数指针vptr和虚表。

总而言之,通过虚继承的子类会生成一个虚基类指针(vbptr)。虚基类表指针总在虚函数表指针之后。所以,如果一个类它是虚继承的子类并且它有自身的虚函数,那么虚基类指针便在虚函数指针之后,有一个指针大小的偏移量。如果该类没有虚函数,那么它的虚基类表指针在对象内存最前面。

虚基类表:虚基类指针指向的是虚基类表,虚基类表中也是存储有多条数据,不过与虚函数表不同的是,虚函数表中每个元素存储的是虚函数的地址,虚基类表中存储的是偏移值。第一个元素存储的是虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由上面的分析我们可知,这个值要么是0(该类本身没有虚函数),要么是-4(该类有虚函数)。虚基类表的第二个,第三个元素记录依次为该类的最左虚继承父类,次左虚继承父类...的内存地址相对于虚基类表指针的偏移值。我们可以通过下图加深理解。

40798da79e976be6004a3a4d31b6d95f.png

接下来我们通过几个案例,来阐述一下虚继承后,派生类中的内存分布情况。

2.1 单虚拟继承

接下来我们通过几个案例,来阐述一下虚继承后,派生类中的内存分布情况。2.1 单虚拟继承

如上述代码所示,Child虚继承类Base。Base类中有2个虚函数,child类中也有自身的虚函数,并且重写了Base的fun1函数。

在64位系统下,sizeof(Child)=32。具体的内存分布图如下。

8dec401780e9a6350fae561f9661c34a.png

由图可知。

1. Child类的首地址存放的是Child类自身的虚表指针,指向的是存放自身虚函数地址的虚函数表(VTable)。

2. 接着存放的是虚基类指针,虚基类指针指向的是虚基类表,虚基类表中存放的是偏移值。第一个元素为虚基类指针相对于内存首地址的偏移值,第二个元素为虚继承的基类的内存与虚基类指针的偏移值。

3. 接着存放Child类成员变量data

4. 接着存放基类Base的虚表指针。虚表指针指向的虚表中,由于Child类重写了fun1,所以虚函数表的信息也有所改变。

5. 最后存放基类Base的成员变量data

2.2 多虚继承

class Base1 {public: virtual void Base1_fun1() { std::cout << "Base1::Base1_fun1" << std::endl; }   virtual void Base1_fun2() { std::cout << "Base1::Base1_fun2" << std::endl; }  int Base1_data;};class Base2 {public: virtual void Base2_fun1() { std::cout << "Base2::Base2_fun1" << std::endl; }   virtual void Base2_fun2() { std::cout << "Base2::Base2_fun2" << std::endl; }  int Base2_data; };class Child : virtual public Base1, virtual public Base2 {public: void Base1_fun1() override { std::cout << "Child::Base1_fun1" << std::endl; }   void Base2_fun1() override { std::cout << "Child::Base2_fun1" << std::endl; }  virtual Child_fun3() { std::cout << "Child::Child_fun3" << std::endl; }  int child_data;};

如上述代码所示,类child虚继承类Base1和类Base2。并且类child重写了类Base1和类Base2的部分虚函数。

具体的内存分布图如下。不考虑内存对齐,只阐述各个元素的位置。

d0be1b1acb8ab3e4ebfce13eab456663.png

由上图可知,

1. Child类实例化一个对象后,其对象内存首地址存放的是指向child类自身虚函数表的虚表指针

2. 接着放入虚基类指针,指向的是虚基类表。虚基类表中存放的是偏移值。第一个元素是虚基类指针与对象内存首地址之间的偏移值;第二个元素存放的是对象中Base1(child先继承的基类)的首地址与虚基类指针的偏移值;第三个元素存放的是对象内存中Base2(child第二继承的基类)的首地址与虚基类指针的偏移值

3. 接着存放的是子类的成员变量

4. 接着放入先继承的Base1的虚表指针

5. 接着放入Base1的成员变量

6. 接着放入后继承的Base2的虚表指针

7. 最后放入Base2的成员变量

2.3 菱形继承

菱形继承其实就是本篇文章最前面提的案例。

class A {public: virtual void fun1() { std::cout << "A::fun1" << std::endl; } int A_data;};class B : virtual public A {public: virtual void fun2() { std::cout << "B::fun2" << std::endl; } int B_data;};class C : virtual public A {public: virtual void fun3() { std::cout << "C::fun3" << std::endl; } int C_data;};class D : public B, public C {public: virtual void fun4() { std::cout << "D::fun4" << std::endl; } int D_data;};

如代码所示,类B和类C虚继承类A,类D公有实继承类B和类C,那么类D的对象内存分布图如下

0e7fbb8c3bf8f5b60782b4c45b4f15fc.png

由上图可知,因为类B和类C都虚继承了类A,由上面的案例可知,类B和类C中都有一个vbptr(虚基类指针),然后由于类D继承了类B和C,所以推断类D中有2个vbptr,分别来自类B和C。

并且由于类D是实继承,不同于虚继承,所以根据之前学习的继承原理,类D中的虚函数地址应该放在第一个继承的基类的虚函数表之后(扩展)。并且由于类A是被虚继承的,类A的数据在类D中只有一份,由类B和类C的虚基类指针,通过偏移量获取类A的数据。

总结

以上主要从菱形继承会产生的问题,到引出虚继承,以及简单从单虚继承,多虚继承,菱形虚继承这几个方面简单阐述了虚继承的实现,功能,以及虚继承后的类对象内存分布情况。

当然,本文只是简单分析了内存分布情况,并没有实际考虑内存对齐等情况,因此具体问题需要具体分析。

总之,C++是一门高深的语言,我们都不断的在学习的过程中,望共勉之。

上面如有概念错误之处,烦请指正,谢谢!

最后,喜欢的小伙伴麻烦点个赞和关注,以后定期会发一些更多的文章,谢谢!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值