由结构体对齐而引发的思考(三)——考虑虚基类时c++类对象内存情况

来源:互联网 发布:js 直接写 和 onload 编辑:程序博客网 时间:2024/06/18 07:53

   多继承

  很多时候,一个子类可能会有多个父类,比如美人鱼既是人也是鱼,能打电话的Pad等等,为了增强代码复用能力,就有了多继承,已解决有多个父类的问题。示例代码如下

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 :publicBase_A, public Base_B

{

public:

    Inherit() :e(0x50)

    {  }

    int e;

};

 

int _tmain(intargc, _TCHAR*argv[])

{

    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 :publicBase_B, public Base_A

{

public:

    Inherit() :e(0x50)

    {  }

    int e;

};

 

int _tmain(intargc, _TCHAR*argv[])

{

    Inherit obj;

    return 0;

}

 

内存模型如下

 


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

      1 派生类放在最下面

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

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

  多继承中的二义性问题

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


这是一个不知道的问题。看你想使用谁的,使用谁的就加上谁的作用域。Oh,这简直太蠢了,示例代码如下:

class Wolf

{

public:

    Wolf() :m_nWolfSomeThing(0x10)

    {  }

public:

    int m_nNumberOfLegs;

    int m_nWolfSomeThing;

};

 

class Human

{

public:

    Human() :m_nHumanSomeThing(0x20)

    {  }

    int m_nNumberOfLegs;

public:

    int m_nHumanSomeThing;

};

 

 

class Werwolf :publicWolf, public Human

{

public:

    Werwolf() :m_nWerwolfSomeThing(0x30)

    {  }

public:

    int m_nWerwolfSomeThing;

};

 

 

int _tmain(intargc, _TCHAR*argv[])

{

    Werwolf obj;

    //obj.m_nNumberOfLegs;//错误的,使用的不明确

    obj.Human::m_nNumberOfLegs;

    obj.Wolf::m_nNumberOfLegs;

    return 0;

}

 

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


想向中应该是这个样子

实际上是不是这样的呢?请看以下示例代码:

class Animal

{

public:

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

    {  }

public:

    int m_nNumberOfLegs;

};

 

class Wolf :publicAnimal

{

public:

    Wolf() :m_nWolfSomeThing(0x10)

    {  }

public:

    int m_nWolfSomeThing;

};

 

class Human :publicAnimal

{

public:

    Human() :m_nHumanSomeThing(0x20)

    {  }

public:

    int m_nHumanSomeThing;

};

 

 

class Werwolf :publicWolf, public Human

{

public:

    Werwolf() :m_nWerwolfSomeThing(0x30)

    {  }

public:

    int m_nWerwolfSomeThing;

};

 

int _tmain(intargc, _TCHAR*argv[])

{

    Werwolf obj;

    return 0;

}

 


可惜天不遂人愿,依然是有两个腿的数量,这是因为子类对象会包含全部的父类成员。对于狼来说,自然会包含动物类中的腿的数量。对于人来说,也是如此。对于狼人来说,会同时包含狼类和人类的所有成员。故而腿的数量这个字段,在狼人对象中依然是出现两个,一份在狼中,一份在人中。这显然是不符合我们的想法的,没有办法,计算机跟不上人的思维很正常,这是典型的菱形继承问题。

三虚继承

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

首先我们来单聊一下虚继承,首先虚继承是为了解决二义性的问题而产生的语法。用法是在继承之前加上一个virtual,我们来看一下最为简单的情况,下面的例子什么问题都没有解决,但是可以帮助我们理解虚继承到底做了什

class Base

{

public:

    Base() :m_a(0x10)

    {  }

public:

    int m_a;

};

 

class Inherit :virtualpublic Base

{

public:

    Inherit() :m_b(0x20)

    {  }

public:

    int m_b;

};

 

int _tmain(intargc, _TCHAR*argv[])

{

    Inherit obj;

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

    return 0;

}

 

现在我们可以猜一猜obj的大小是多大呢?你绝对不会想到。



假如这个你确实猜到了,那么后面的内容,你绝对不会猜到了,下面是obj的内存模型


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

4个字节实际上是一个地址即:0x095858.

我们到那里看一看


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

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

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

 

struct VirtualBase

{

    int   Offect1;

    int   Offect2;

}

这里我们着重关注第二个,它能够实现这样的事情:基类与派生类可以不挨在在一起,是通过虚基类表中的差值,从派生类就可以找到基类的数据。

我们直接看复杂一些的情况,结合上面的例子更加容易理解一些

class Base

{

public:

    Base() :m_a(0x10)

    {  }

public:

    int m_a;

};

 

class Inherit_A :virtualpublic Base

{

public:

    Inherit_A() :m_b(0x20)

    {  }

public:

    int m_b;

};

 

class Inherit_B :virtualpublic Base

{

public:

    Inherit_B() :m_c(0x30)

    {  }

public:

    int m_c;

};

class Test :publicInherit_A, publicInherit_B

{

public:

    Test() :m_d(0x40)

    {  }

public:

    int m_d;

};

 

int _tmain(intargc, _TCHAR*argv[])

{

    Test obj;

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

    return 0;

}

在开始之前,大家可以先猜测一下这个对象的大小是多大?本身一共4int型,妥妥的应该是16个字节。实际上


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


可以看出:

         从上到下的顺序是AB,派生类,基类BaseBase类被甩到了最后,并且只有一个。Inherit_AInherit_B共用一个虚基类。

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

此时此刻我们便可以总结一下:

1 进行如图所示的虚继承


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


2 对于类对象大小,每一个虚继承的子类由于都会有一个虚基类指针,故而多一个虚继承,整个对象的大小就会比正常大4个字节。

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