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

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第1张图片

上一部分参考:由结构体对齐所引发的对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的对象,就能够使用从两个父类继承下来的所有数据和方法(需要考虑权限问题)。我们来看一下它的内存模型:

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第2张图片

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

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;

}

内存模型如下:

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第3张图片

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

派生类放在最下面

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

子类总是包含全部的父类

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

暮光之城中有这么一种物种叫做狼人,

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

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

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第4张图片

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

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;

}

查看狼人类内存模型:

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第5张图片

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

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,

我们可以查看一下:

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第6张图片

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

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;

}

输出结果:

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

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

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第7张图片

可以看出:

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

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

小结一下

进行如图所示的虚继承

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第8张图片

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

由结构体对齐所引发的对C++类对象内存模型的思考(二)_第9张图片

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

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

本文由看雪论坛 蓝色淡风 原创  转载请注明来自看雪社区

你可能感兴趣的:(由结构体对齐所引发的对C++类对象内存模型的思考(二))