Cpp diamond hierarchy

Author Avatar
Gaoustcer 10月 11, 2024

C++拾遗(菱形继承、虚继承)

菱形继承

一个类继承自两个父类,同时两个父类继承自同一基类

基础知识:C++类的继承

调用派生类构造函数之前,先调用基类构造函数,如果基类的构造函数存在多个,我们可以采用如下的方式手动选择调用特定的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base{
Base(){
// Default init
}
Base(param_list){
// another init
}
};
class Derive:public Base{
Derive(param_list): Base(param_list){
// call the base init
}
};

构造函数不能为虚函数(先需要构造虚表,然后向虚表中填写实际调用地址实现运行时绑定)

菱形继承

一个菱形继承的例子是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base{
public:
int x;
Base(){
x = 1;
}
};
class Derive1: public Base{
public:
Derive1(){
x = 2;
}
};
class Derive2: public Base{
public:
Derive2(){
x = 3;
}
};
class Derive: public Derive1,public Derive2{};

直接访问Derive类的x对象会报错ambigious,因为两个基类的成员变量x在内存中保存了两份,sizeof(Derive)输出8,写一段代码验证这一点(同理也适用于两个派生父类同时重写了基类的某个函数的情况)

1
2
3
4
5
Derive d;
int * ptr = (int *)&d;
cout << *ptr << endl;
ptr = ptr + 1;
cout << *ptr << endl;

输出2和3

解决二义性的一个办法是使用::运算符访问对象,例如在Derive类的方法中,使用Derive1::x这样的访问可以精确访问某个具体的变量,但是这样做仍然会导致数据冗余

借助编译器-fdump-class-hierarchy选项可以生成.class文件,包含每个类的内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Class Base
size=4 align=4
base size=4 base align=4
Base (0x0x7f5c5fbea5a0) 0

Class Derive1
size=4 align=4
base size=4 base align=4
Derive1 (0x0x7f5c5fa941a0) 0
Base (0x0x7f5c5fbea780) 0

Class Derive2
size=4 align=4
base size=4 base align=4
Derive2 (0x0x7f5c5fa94208) 0
Base (0x0x7f5c5fbea960) 0

Class Derive
size=8 align=4
base size=8 base align=4
Derive (0x0x7f5c5faa7620) 0
Derive1 (0x0x7f5c5fa94270) 0
Base (0x0x7f5c5fbeab40) 0
Derive2 (0x0x7f5c5fa942d8) 4
Base (0x0x7f5c5fbeaba0) 4

对于下面一个包含虚函数的派生类内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base{
public:
virtual void func(){
call_count = 1;
}
void call(){
call_count = 3;
}
};
class Derive:public Base{
public:
void func(){
call_count = 2;
}
};

其内存布局为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Vtable for Base
Base::_ZTV4Base: 3u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 (int (*)(...))Base::func

Class Base
size=8 align=8
base size=8 base align=8
Base (0x0x7f5efa6125a0) 0 nearly-empty
vptr=((& Base::_ZTV4Base) + 16u)

Vtable for Derive
Derive::_ZTV6Derive: 3u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI6Derive)
16 (int (*)(...))Derive::func

Class Derive
size=8 align=8
base size=8 base align=8
Derive (0x0x7f5efa4bc1a0) 0 nearly-empty
vptr=((& Derive::_ZTV6Derive) + 16u)
Base (0x0x7f5efa6126c0) 0 nearly-empty
primary-for Derive (0x0x7f5efa4bc1a0)

如果不用virtual做方法重写

1
2
3
4
5
6
7
8
9
10
Class Base
size=1 align=1
base size=0 base align=1
Base (0x0x7f0e6abb75a0) 0 empty

Class Derive
size=1 align=1
base size=1 base align=1
Derive (0x0x7f0e6aa611a0) 0 empty
Base (0x0x7f0e6abb76c0) 0 empty

派生类增加了一个虚表,即增加了一个对象,指向虚表,因此带虚继承的派生类size为8,无虚继承的size为1

回到菱形继承的case,如果我们希望减少对同一个base继承产生的二义性,虚继承的本质是

虚继承只需要修改Derive1/Derive2

1
2
3
4
5
6
class Derive1: virtual public Base{
//
};
class Derive2: virtual public Base{
//
};

此时的内存布局为、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
Class Base
size=4 align=4
base size=4 base align=4
Base (0x0x7f48512935a0) 0

Vtable for Derive1
Derive1::_ZTV7Derive1: 3u entries
0 8u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI7Derive1)

VTT for Derive1
Derive1::_ZTT7Derive1: 1u entries
0 ((& Derive1::_ZTV7Derive1) + 24u)

Class Derive1
size=16 align=8
base size=8 base align=8
Derive1 (0x0x7f485113d1a0) 0 nearly-empty
vptridx=0u vptr=((& Derive1::_ZTV7Derive1) + 24u)
Base (0x0x7f4851293780) 8 virtual
vbaseoffset=-24

Vtable for Derive2
Derive2::_ZTV7Derive2: 3u entries
0 8u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI7Derive2)

VTT for Derive2
Derive2::_ZTT7Derive2: 1u entries
0 ((& Derive2::_ZTV7Derive2) + 24u)

Class Derive2
size=16 align=8
base size=8 base align=8
Derive2 (0x0x7f485113d208) 0 nearly-empty
vptridx=0u vptr=((& Derive2::_ZTV7Derive2) + 24u)
Base (0x0x7f48512939c0) 8 virtual
vbaseoffset=-24

Vtable for Derive
Derive::_ZTV6Derive: 6u entries
0 16u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI6Derive)
24 8u
32 (int (*)(...))-8
40 (int (*)(...))(& _ZTI6Derive)

Construction vtable for Derive1 (0x0x7f485113d270 instance) in Derive
Derive::_ZTC6Derive0_7Derive1: 3u entries
0 16u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI7Derive1)

Construction vtable for Derive2 (0x0x7f485113d2d8 instance) in Derive
Derive::_ZTC6Derive8_7Derive2: 3u entries
0 8u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI7Derive2)

VTT for Derive
Derive::_ZTT6Derive: 4u entries
0 ((& Derive::_ZTV6Derive) + 24u)
8 ((& Derive::_ZTC6Derive0_7Derive1) + 24u)
16 ((& Derive::_ZTC6Derive8_7Derive2) + 24u)
24 ((& Derive::_ZTV6Derive) + 48u)

Class Derive
size=24 align=8
base size=16 base align=8
Derive (0x0x7f4851150620) 0
vptridx=0u vptr=((& Derive::_ZTV6Derive) + 24u)
Derive1 (0x0x7f485113d270) 0 nearly-empty
primary-for Derive (0x0x7f4851150620)
subvttidx=8u
Base (0x0x7f4851293c00) 16 virtual
vbaseoffset=-24
Derive2 (0x0x7f485113d2d8) 8 nearly-empty
subvttidx=16u vptridx=24u vptr=((& Derive::_ZTV6Derive) + 48u)
Base (0x0x7f4851293c00) alternative-path

此时sizeof(Derive)为24,因为其包含一个int变量以及两个指针(虚继承指向的基类指向一张表,表中存在若干指针,每个指针指向一个基类)

Diamond Hierarchy From memory layout perspective

普通的单继承

派生类继承了基类的所有属性和行为,需要保存基类的所有副本

1
2
3
4
5
6
7
8
class A{
int a;
int b;
};
class B:public B{
int x;
int y;
};

B的内存布局为

起始地址(偏移量) 内存
0 A::a
4 A::b
8 B::x
12 B::y

多继承

对于一个最简单的多继承

1
2
3
4
5
6
7
8
9
class Base1{
int x;
}
class Base2{
int y;
};
class Derive{
int z;
};

内存布局为

偏移 内存
0 Base1::x
4 Base2::y
8 Derive::z

virtual虚函数

对于virtual修饰的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base{
public:
virtual void func(){
cout << "Base\n";
}
};
class Derive:public Base{
public:
void func(){
cout << "Derive\n";
}
int x;
};

Base类包含一个指针成员(vfptr),指向虚表,虚表中的每一项是一个函数指针,Base指针调用虚方法的过程

之前我们提到过,C++中所谓的类方法本质上就是接收一个类对象的函数,这种将调用自身的函数保存在一张表的过程有点类似于回调

菱形继承和虚继承

对于我们之前给到的一个虚继承的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base{
public:
int x;
Base(){
x = 1;
}
};
class Derive1: virtual public Base{
public:
Derive1(){
x = 2;
}
};
class Derive2: virtual public Base{
public:
Derive2(){
x = 3;
}
};
class Derive: public Derive1, public Derive2{};

通过这段代码查看内存内容sizeof(Derive)=24

1
2
3
4
5
6
Derive d;
int * virtual_ptr = (int *)&d;
for(int i = 0;i < 6;i++){
cout << *virtual_ptr << endl;
virtual_ptr++;
}

输出

1
2
3
4
5
6
687893688
21854
687893712
21854
3
21854

说明实际上包含两个指针,最后int为了和8比特对齐被扩展到了8比特,下面分析虚继承

1
2
3
4
5
6
class Base{
int b;
};
class Derive:virtual public Base{
int x;
};

和显式继承

1
2
3
class Derive:public Base{
int x;
};

两者的内存布局不同,显示继承需要存储基类所有对象,对于虚继承,一个内存布局为

偏移 内存
0 Derive::vbptr
8 Derive::x
12 Base::b

类大小为16字节,其中Derive::vptr指向一个虚表,派生类中基类的存储内容在派生类成员变量后,虚继承的不同之处还在于将会把自己的成员变量放在前面,底层来看,指向虚基表virtual table的指针vbptr指向的表中的是保存的基类距离自身的相对偏移,例如这里可能虚表第一项是12,对于虚继承

1
2
3
4
5
graph TD;
Base-->A
Base-->B
A-->Derive
B-->Derive

不考虑虚函数的情况,类A和B大小均为16(8+8,其中包含4字节对齐),其中包含一个指向虚表的指针,两者最终指向的虚拟地址是一样的,类

1
2
3
4
5
6
7
class Base{
public:
int x = 7;
};
class A:virtual public Base{};
class B:virtual public Base{};
class Derive:public A,public B{};

实例化一个Derive类,大小为24(8+8+4+4)

偏移 内存 备注
0 A::vbptr 指向A虚表
8 B::vbptr 指向B虚表
16 Base:x 共同继承的虚类

Derive d使用gdb查看虚表内容

1
info locals
1
2
d = {<A> = {<Base> = {x = 7}, _vptr.A = 0x555555755cc0 <vtable for Derive+24>}, <B> = {
_vptr.B = 0x555555755cd8 <VTT for Derive>}, <No data fields>}

查看两个虚表存储内容,虚表中第二项分别存储16和8,和虚函数表不同,虚继承表存储的是相对位置,最终的派生类Derive实际上继承的是两个虚继承表指针

本站访客数人次