由结构体对齐所引发的对C++类对象内存模型的思考( 二)
上一部分参考:由结构体对齐所引发的对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,
更多干货等着你~