查看原文
其他

由结构体对齐所引发的对C++类对象内存模型的思考( 二)

2017-12-12 蓝色淡风 看雪学院

上一部分参考:由结构体对齐所引发的对C++类对象内存模型的思考(一)



虚基类的影响



1. 多继承


很多时候,一个子类可能有多个父类,比如美人鱼既是人也是鱼,冬虫夏草,可以看视频可以上网的手机,为了增强代码复用能力,就有了多继承,示例代码如下:

class Base_A

{

public:

    Base_A() :a(0x10), b(0x20)

    {  }

    int a;

    int b;

};

 

class Base_B

{

public:

    Base_B() :c(0x30), d(0x40)

    {  }

    int c;

    int d;

};

 

class Inherit :public Base_A, public Base_B

{

public:

    Inherit() :e(0x50)

    {  }

    int e;

};

 

int main()

{

    Inherit obj;

    return 0;

}


代码中,Inherit的对象,就能够使用从两个父类继承下来的所有数据和方法(需要考虑权限问题)。我们来看一下它的内存模型:

 


可以看到,子类对象包含着父类的全部数据,我们再看另外一种情况:


class Base_A

{

public:

    Base_A() :a(0x10), b(0x20)

    {  }

    int a;

    int b;

};

 

class Base_B

{

public:

    Base_B() :c(0x30), d(0x40)

    {  }

    int c;

    int d;

};

 

class Inherit :public Base_B, public Base_A

{

public:

    Inherit() :e(0x50)

    {  }

    int e;

};

 

 

 

int main()

{

    Inherit obj;

 

 

    return 0;

}


内存模型如下:




此时我们可以得出一些简单的结论:

  • 派生类放在最下面

  • 多个父类的情况下,谁在上,谁在下,由继承顺序决定。

  • 子类总是包含全部的父类


2.多继承中的二义性问题


暮光之城中有这么一种物种叫做狼人, 暮色之时是人类,新月到破晓就是狼人了,它有着锋利的牙齿,恐怖的速度,还能两个腿奔跑。它可以由狼类和人类共同派生出来。但是有一个问题,就是狼类中可能会有腿的数量,牙齿的数量等等属性,恰好人类中也有腿的数量,牙齿的数量等等属性。我们知道子类会具有全部父类的所有成员。那么此时此刻,狼人对象访问腿的数量,牙齿的数量的时候,会访问哪个父类的成员呢?


有人已经想出了办法,就是把狼和人都有的成员抽象出来,形成一个爷爷类,比如叫做动物类,在狼类和人类的上面,形成如下图所示的情况:

为了解决我们心中的疑惑,我们可以做个试验,先看下面这段代码:

class Animal

{

public:

    Animal() :m_nNumberOfLegs(5)//默认5条腿^o^

    {  }

public:

    int m_nNumberOfLegs;

};

 

class Wolf :public Animal

{

public:

    Wolf() :m_nWolfSomeThing(0x10)

    {  }

public:

    int m_nWolfSomeThing;

};

 

class Human :public Animal

{

public:

    Human() :m_nHumanSomeThing(0x20)

    {  }

public:

    int m_nHumanSomeThing;

};

 

 

class Werwolf :public Wolf, public Human

{

public:

    Werwolf() :m_nWerwolfSomeThing(0x30)

    {  }

public:

    int m_nWerwolfSomeThing;

};

 

 

 

 

 

int main()

{

    Werwolf obj;

 

    return 0;

}


查看狼人类内存模型:




我们发现有两份腿的数量,这是因为子类对象会包含全部的父类成员。对于狼来说,自然会包含动物类中的腿的数量。对于人来说,也是如此。对于狼人来说,会同时包含狼类和人类的所有成员。故而腿的数量这个字段,在狼人对象中依然是出现两份,一份在狼中,一份在人中,这是典型的菱形继承问题。


3.虚继承


为了解决上面这个问题,产生了一种叫做虚继承的机制:


虚继承是为了解决二义性的问题而产生的语法。用法是在继承之前加上一个virtual,我们来看一下最为简单的情况,下面的例子可以帮助我们理解虚继承:

class Base

{

public:

    Base() :m_B(0x10)

    {  }

public:

    int m_B;

};

 

class Inherit :virtual public Base

{

public:

    Inherit() :m_I(0x20)

    {  }

public:

    int m_I;

};

 

 

 

int main()

{

    Inherit obj;

    printf("虚继承的对象大小%d", sizeof(obj));

    return 0;

}


我们可以看一看输出结果:(结果可能会让你大吃一惊哦)



有人可能会问不是应该为8个字节么,怎么会是12呢,那多出来的四个字节究竟是什么?好,下面我们看一看它的内存模型:



我们可以看到在整个对象的开头多了一个奇怪的数据,并且神奇的是子类数据位于基类数据的上面,我们来解释它在干什么:


通过查阅相关文献,得知头四个字节实际上是一个地址,即0x01186b30,
我们可以查看一下:



刚才的那个地址,我们称之为虚基类表指针,指向的位置存储的是一共有两个元素,分别是两个差值:


1 本类地址与虚基类表指针地址的差
2 虚基类地址与虚基类表指针地址的差


struct VirtualBase

{

    int   Offset1;

    int   Offset2;

}


这里我们着重关注第二个,它能够实现这样的事情:基类与派生类可以不挨在一起,是通过虚基类表中的差值,从派生类就可以找到基类的数据。
我们直接看复杂一些的情况,结合上面的例子更加容易理解一些:


class Base

{

public:

    Base() :m_Base(0x10)

    {  }

public:

    int m_Base;

};

 

class Inherit_A :virtual public Base

{

public:

    Inherit_A() :m_A(0x20)

    {  }

public:

    int m_A;

};

 

class Inherit_B :virtual public Base

{

public:

    Inherit_B() :m_B(0x30)

    {  }

public:

    int m_B;

};

class Test :public Inherit_A, public Inherit_B

{

public:

    Test() :m_T(0x40)

    {  }

public:

    int m_T;

};

 

 

 

int main()

{

    Test obj;

    printf("虚继承的对象大小%d", sizeof(obj));

    return 0;

}


输出结果:



这个结果估计大多数人都没有猜到,呵呵


我们可以来看一下它的内存模型:




可以看出:


从上到下的顺序是A,B,派生类,基类Base。Base类被甩到了最后,并且只有一个。Inherit_A与Inherit_B共用一个虚基类。


这个机制,无论是几个中间内一层的类,都能保证虚基类的数据只有一份,这就是虚继承解决多继承中二义性的问题:



小结一下



  • 进行如图所示的虚继承


  • 编译器会把虚基类单独置于一处,派生类通过虚基类表指针指向位置存储的差值能够找到虚基类,当类似于图示的情况下的时候,使得孙子类无论从哪一条支路寻找爷爷类(虚基类),找到的都是同一个爷爷。

  • 对于类对象大小,每一个虚继承的子类由于都会有一个虚基类指针,故而多一个虚继承,整个对象的大小就会比正常大4个字节。(这一点与虚函数那边有点类似,呵呵)

  • 虚基类实际上不需要一定放在下面,放在任何位置都可以,因为大家是通过一个差值找到的它。






本文由看雪论坛 蓝色淡风 原创

转载请注明来自看雪社区


热门阅读


点击阅读原文/read,

更多干货等着你~


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存