继承是C++作为OOD程序设计语言的三大特征(封装,继承,多态)之一,单一非多态继承是比较好理解的,本文主要讲解GCC环境下的多重继承和虚拟继承的对象内存布局。
一、多重继承
先看几个类的定义:
01 |
class Top
|
02 |
{
|
03 |
public:
|
04 |
int a;
|
05 |
};
|
06 |
07 |
class Left : public Top
|
08 |
{
|
09 |
public:
|
10 |
int b;
|
11 |
};
|
12 |
13 |
class Right : public Top
|
14 |
{
|
15 |
public:
|
16 |
int c;
|
17 |
};
|
18 |
19 |
class Bottom : public Left, public Right
|
20 |
{
|
21 |
public:
|
22 |
int d;
|
23 |
};
|
不难想象,Left和Right类的内存布局如下图所示:
我们如下进行验证:
1 |
Left *left = new Left();
|
2 |
Top *top = left; |
3 |
cout << left << '\t' << top << endl;//输出:0x902c008 0x902c008
|
4 |
Right *right = new Right();
|
5 |
top = right; |
6 |
cout << right << '\t' << top << endl;//输出:0x902c018 0x902c018
|
从输出结果可以看出,父类指针top指向子类对象left和right的起始地址,与上述内存布局吻合。
在非虚拟多重继承的情况下,子类的内存布局是什么样子的呢?如下所示:
可以看出,Bottom类由于继承了Left和Right,而Left和Right又分别继承了Top。因此,Bottom包含了Top两次!
下面进行验证:
1 |
Bottom *bottom = new Bottom();
|
1 |
// top = bottom; //error: ‘Top’ is an ambiguous base of ‘Bottom’ |
1 |
top = (Left *)bottom; |
1 |
left = bottom; |
2 |
cout << bottom << '\t' << top << '\t' << left << endl;//输出:0x9930028 0x9930028 0x9930028
|
3 |
top = (Right *)bottom; |
4 |
right = bottom; |
5 |
cout << bottom << '\t' << top << '\t' << right << endl;//输出:0x9930028 0x9930030 0x9930030
|
好了,到这里讲完了非虚拟继承下的多重继承的内存布局情况,相信大家应该有一个比较清晰的认识了。最重要的一点是: 多重继承时,父类共同继承的祖父类会在子类中有多份存在。
二、虚拟继承
平时讨论的最多的是虚函数,很少涉及到虚拟继承的情况。那么,虚拟继承到底是一个什么概念呢?
先来看一个例子:
01
|
#include
<iostream>
|
02
|
using namespace std;
|
03
|
04
|
class Father
|
05
|
{
|
06
|
public:
|
07
|
int a;
|
08
|
};
|
09
|
10
|
class Child
: virtual public Father
|
11
|
{
|
12
|
public:
|
13
|
int b;
|
14
|
};
|
15
|
16
|
int main()
|
17
|
{
|
18
|
cout
<< sizeof(Father)
<< '\t' << sizeof(Child)
<< endl;//输出:4
12
|
19
|
Child
child;
|
20
|
cout
<< &child << '\t' <<
&child.b << '\t' <<
&child.a << endl;//输出:0xbfc08124
0xbfc08128 0xbfc0812c
|
21
|
return 0;
|
22
|
}
|
对,你没有看错,类的大小输出不是4 8,而是4 12。虚拟继承时,编译器会在子类中安插上一个虚表指针。
从输出的对象成员地址来看,我们可以得到Child类的如下内存布局:
现在我们对多重继承的例子进行改造:
01
|
class Top
|
02
|
{
|
03
|
public:
|
04
|
int a;
|
05
|
};
|
06
|
07
|
class Left
: virtual public Top
|
08
|
{
|
09
|
public:
|
10
|
int b;
|
11
|
};
|
12
|
13
|
class Right
: virtual public Top
|
14
|
{
|
15
|
public:
|
16
|
int c;
|
17
|
};
|
18
|
19
|
class Bottom
: public Left, public Right
|
20
|
{
|
21
|
public:
|
22
|
int d;
|
23
|
};
|
把Left和Right改成了虚拟继承Top。
从上面验证简单虚拟继承时,编译器安插虚表指针的例子,我们可以想象出此时Bottom类的对象内存布局如下:
对,你没有看错!虚拟继承时,子类只有父类共同继承的祖父类的一份存在。这其实也就是虚拟继承的最大用途。此时,Top,Left,Right和Bottom对象的大小分别为:4 ,12 ,12 ,24。
既然有虚表指针了,那么Bottom的虚表是什么样的呢?请看:
有了虚表,内存布局情况一目了然。下面我们进行验证:
1
|
Bottom
*bottom = new Bottom();
|
2
|
top
= bottom;
|
3
|
cout
<< bottom << '\t' <<
top << endl;//输出:0x9fa5028
0x9fa503c
|
4
|
Left
*left = bottom;
|
5
|
cout
<< bottom << '\t' <<
left << endl;//输出:0x9fa5028
0x9fa5028
|
6
|
Right
*right = bottom;
|
7
|
cout
<< bottom << '\t' <<
right << endl;//输出:0x9fa5028
0x9fa5030
|
根据输出结果,我们可以知道指针的指向情况:
由于引入了虚指针和虚表,left指针和right指针可以根据虚表提供的偏移量信息,轻松访问到Top::a。
到此为止,已经讨论清楚了多重继承和虚拟继承下的对象内存布局情况。总结下:非虚拟多重继承时,子类会有父类
共同继承祖父类的多份存在;虚拟继承时,子类会被安插一个虚拟指针;多重虚拟继承时,子类只有父类共同继承祖父类的一
份存在。通过父类的虚拟指针,可以正确地访问祖父类中的成员。