c++对象布局

本文讨论c++中的对象内存布局,包括c++对象的数据成员以及为支持多态机制而添加的额外的数据在对象中的分布。

实验环境

体系结构:x86_64

操作系统:Ubuntu 22.04 Linux os 6.2.0-36-generic

编译器:gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

Data Member的布局

Question 1:对象中不同访问类型的成员是怎么分布的?有可能同种访问类型的成员排列在一起,然后按照指定的顺序比如public、protected、private分布,也可能按照声明顺序分布。

编写如下测试程序,类Obj中的数据成员随意声明,检验编译器是否会把同种访问类型的成员安排在相邻的内存位置。

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

class Print;

class Obj
{
friend class Print;

public:
int a;
uint8_t b;

private:
double c;
float d;

protected:
short e;

public:
long f;
};

class Print
{
public:
void printObj()
{
Obj obj; // offset
cout << "&obj: " << &obj << endl; // 0
cout << "&obj.a: " << &obj.a << endl; // 0
cout << "&obj.b: " << (int *)&obj.b << endl; // 4
cout << "&obj.c: " << &obj.c << endl; // 8
cout << "&obj.d: " << &obj.d << endl; // 16
cout << "&obj.e: " << &obj.e << endl; // 20
cout << "&obj.f: " << &obj.f << endl; // 24
cout << "sizeof obj: " << sizeof obj << endl;
}
};

int main()
{
Print p;
p.printObj();

return 0;
}

执行结果如下:

1
2
3
4
5
6
7
8
&obj:   0x7fffd69afeb0
&obj.a: 0x7fffd69afeb0
&obj.b: 0x7fffd69afeb4
&obj.c: 0x7fffd69afeb8
&obj.d: 0x7fffd69afec0
&obj.e: 0x7fffd69afec4
&obj.f: 0x7fffd69afec8
sizeof obj: 32

可见,数据成员是按照声明顺序排列的,这一点与c语言中的结构体一致,编译器也不会特意安排不同访问类型的数据成员的前后地址。

继承无多态

为上述的类Obj添加一个以及两个父类,子类会继承父类的数据成员,查看父类的成员在子类中的分布。

Question 1: 父类的数据成员应该被安排在子类的开头处,如果有多个父类,则按照父类的继承顺序,先安置父类的数据成员,再安置子类的数据成员。

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

class Base1
{
public:
int x1;

protected:
int x2;

private:
int x3;
};

class Base2
{

public:
int y1;

protected:
int y2;

private:
int y3;
};

class Obj : public Base1, public Base2
.....
void printObj()
{
Obj obj;
cout << "&obj: " << &obj << endl; // 0

cout << "&obj.x1: " << &obj.x1 << endl; // 0
cout << "&obj.x2: " << &obj.x2 << endl; // 4
// 接下来是x3,但是x3对子类来说无法直接访问
cout << "&obj.y1: " << &obj.y1 << endl; // 12
cout << "&obj.y2: " << &obj.y2 << endl; // 16
// 接下来是y3,但是y3对子类来说无法直接访问
//然后才是子类自己声明的数据成员
cout << "&obj.a: " << &obj.a << endl; // 24
cout << "&obj.b: " << (int *)&obj.b << endl;
cout << "&obj.c: " << &obj.c << endl;
cout << "&obj.d: " << &obj.d << endl;
cout << "&obj.e: " << &obj.e << endl;
cout << "&obj.f: " << &obj.f << endl;
cout << "sizeof obj: " << sizeof obj << endl;
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
&obj:   0x7fffe0548610
&obj.x1: 0x7fffe0548610
&obj.x2: 0x7fffe0548614
&obj.y1: 0x7fffe054861c
&obj.y2: 0x7fffe0548620
&obj.a: 0x7fffe0548628
&obj.b: 0x7fffe054862c
&obj.c: 0x7fffe0548630
&obj.d: 0x7fffe0548638
&obj.e: 0x7fffe054863c
&obj.f: 0x7fffe0548640
sizeof obj: 56

可见,编译器确实会按照继承的顺序安排父类的数据成员。

加上多态——单继承

修改程序,给Base1加上虚函数,Obj仅继承自Base1:

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

class Base1
{
public:
int x1;
virtual void virtfunc1()
{

}
protected:
int x2;

private:
int x3;
};
....

class Obj : public Base1
{
friend class Print;

public:
int a;
uint8_t b;

private:
double c;
float d;

protected:
short e;

public:
long f;
};

class Print
{
public:
void printObj()
{
Obj obj;
cout << "&obj: " << &obj << endl; // 0

cout << "&obj.x1: " << &obj.x1 << endl; // 8
cout << "&obj.x2: " << &obj.x2 << endl; // 12
// 接下来是16 x3
// cout << "&obj.y1: " << &obj.y1 << endl;
// cout << "&obj.y2: " << &obj.y2 << endl;
cout << "&obj.a: " << &obj.a << endl; // 20
cout << "&obj.b: " << (int *)&obj.b << endl; // 24

cout << "&obj.c: " << &obj.c << endl;
cout << "&obj.d: " << &obj.d << endl;
cout << "&obj.e: " << &obj.e << endl;
cout << "&obj.f: " << &obj.f << endl;
cout << "sizeof obj: " << sizeof obj << endl;
}
};

执行结果如下:

1
2
3
4
5
6
7
8
9
10
&obj:   0x7ffc31b16580
&obj.x1: 0x7ffc31b16588
&obj.x2: 0x7ffc31b1658c
&obj.a: 0x7ffc31b16594
&obj.b: 0x7ffc31b16598
&obj.c: 0x7ffc31b165a0
&obj.d: 0x7ffc31b165a8
&obj.e: 0x7ffc31b165ac
&obj.f: 0x7ffc31b165b0
sizeof obj: 56

编译器在对象头部插入了虚指针,因此占用了8个字节。gdb查看obj对象头八个字节的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) set print object on  # 打印对象
(gdb) set print pretty on # 每行只会显示结构体的一名成员,而且还会根据成员的定义层次进行缩进
(gdb) x /x &obj
0x7fffffffde20: 0x55557d30 # 虚指针的值
(gdb) x /10x &obj
0x7fffffffde20: 0x55557d30 0x00005555 0xf7cc23a7 0x00007fff
0x7fffffffde30: 0xf7e28e88 0x00007fff 0xf7e28e88 0x00007fff
0x7fffffffde40: 0xf7e28e88 0x00007fff
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557d30 (subobject @ 0x7fffffffde20):
[0]: 0x5555555552ba <Base1::virtfunc1()>
(gdb) p obj
$4 = (Obj) {<Base1> = {
_vptr.Base1 = 0x555555557d30 <vtable for Obj+16>,
x1 = -137616473, x2 = 32767, x3 = -136147320}, a = 32767,
b = 136 '\210', c = 6.9533490812636458e-310,
d = -8.64173507e+33, e = 32767, f = 140737352208216}

通过gdb查看对象内存分布的功能,可见编译器确实将虚指针存放在了对象开头八字节,虚指针指向虚表(并非表头,虚指针所指的位置前面还有信息),虚表中存放着虚函数的地址,而虚指针指向第一个虚函数的地址条目。

接下来,我们探究对象的虚表内容,编写如下代码,似的类的继承关系为Obj -> Base2 -> Base1,Base1和Base2均有各自的虚函数:

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 Base1
{
public:
int x1;
virtual void virtfunc1()
{
cout << "Base 1 virtfunc1\n";
}

protected:
int x2;

private:
int x3;
};

class Base2 : public Base1
{

public:
int y1;
virtual void virtfunc1()
{
cout << "Base 2 virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Base 2 virtfunc2\n";
}

protected:
int y2;

private:
int y3;
};

class Obj : public Base2
{
friend class Print;

public:
int a;
uint8_t b;
virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}

private:
double c;
float d;

protected:
short e;

public:
long f;
};


int main()
{
Obj obj;
obj.virtfunc1();
obj.virtfunc2();
Base2 b2 = obj;
b2.virtfunc2();
Base1 b1 = b2;
b1.virtfunc1();
cout << hex << *(uintptr_t *)&b1 << endl; // base1 虚指针
cout << hex << *(uintptr_t *)&b2 << endl; // base2 虚指针
cout << hex << *(uintptr_t *)&obj << endl; // obj虚指针

return 0;
}

通过gdb查看三个类实例的虚表内容:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d30 (subobject @ 0x7fffffffddf0):
[0]: 0x55555555550c <Base1::virtfunc1()>
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d10 (subobject @ 0x7fffffffde10):
[0]: 0x555555555538 <Base2::virtfunc1()>
[1]: 0x555555555564 <Base2::virtfunc2()>
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557cf0 (subobject @ 0x7fffffffde30):
[0]: 0x555555555590 <Obj::virtfunc1()>
[1]: 0x5555555555bc <Obj::virtfunc2()>

每个类都有自己的虚表,虚表中的条目包含虚函数的地址,对象调用虚函数时通过查找虚表中的条目而获得虚函数的地址。可以发现,obj中继承自父类的虚函数的地址与父类的虚函数地址均不相同,因为在这里,子类都重写了父类的虚函数。如obj重写了继承自Base2和Base1的虚函数,而Base2重写了继承自Base1的虚函数。

这也可以解释为什么指针和引用可以实现多态,即使指针是父类类型,但是如果其指向一个子类,则虚指针指向的是子类的虚表。并且,定义了虚函数的类,其默认构造函数和拷贝构造函数不是bit Semantics的,因为构造对象(默认或者拷贝)时,构造函数会设置对象的虚指针指向该对象类型的虚表。因此main函数中,通过赋值构造的b1和b2的虚指针指向的是Base1和Base2的虚表。

注释掉Base2对继承自Base1的虚函数的重写,以及Object对继承自Base2的虚函数的重写,我们猜测,虚表中的相应的虚函数条目应该和父类保持一致。

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

class Base2 : public Base1
{

public:
int y1;
// virtual void virtfunc1()
// {
// cout << "Base 2 virtfunc1\n";
// }

virtual void virtfunc2()
{
cout << "Base 2 virtfunc2\n";
}

protected:
int y2;

private:
int y3;
};

class Obj : public Base2
{
friend class Print;

public:
int a;
uint8_t b;
virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

// virtual void virtfunc2()
// {
// cout << "Obj virtfunc2\n";
// }

private:
double c;
float d;

protected:
short e;

public:
long f;
};

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d30 (subobject @ 0x7fffffffddf0):
[0]: 0x55555555550e <Base1::virtfunc1()>
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d10 (subobject @ 0x7fffffffde10):
[0]: 0x55555555550e <Base1::virtfunc1()>
[1]: 0x55555555553a <Base2::virtfunc2()>
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557cf0 (subobject @ 0x7fffffffde30):
[0]: 0x555555555566 <Obj::virtfunc1()>
[1]: 0x55555555553a <Base2::virtfunc2()>

取消刚刚的注释,为Base1和Base2再添加一个虚函数,以及Obj类自己的虚函数,重新编译。同时,我们验证同类型的对象的虚指针是相同的,即程序只需要维护一份虚表(没必要为不同对象维护单独的一份虚表):

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
83
84
85
86
87
88
89
90
91
92
93
94
95

class Base1
{
public:
int x1;
virtual void base1_virt1()
{
cout << "Base 1 virtfunc1\n";
}
virtual void base1_virt2()
{
cout << "Base 1 virtfunc2\n";
}

protected:
int x2;

private:
int x3;
};

class Base2 : public Base1
{

public:
int y1;
virtual void base1_virt1()
{
cout << "Base 2 virtfunc1 from Base 1\n";
}

virtual void base2_virt2()
{
cout << "Base 2 virtfunc2 \n";
}

protected:
int y2;

private:
int y3;
};

class Obj : public Base2
{
public:
int a;
uint8_t b;
virtual void base1_virt1()
{
cout << "Obj 1 virtfunc1 from base 1\n";
}

virtual void base2_virt1()
{
cout << "Obj 1 virtfunc1 from base 2\n";
}

virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}

private:
double c;
float d;

protected:
short e;

public:
long f;
};


int main()
{
Obj obj;
Base2 b2 = obj;
Base1 b1 = obj;
Base1 b11; // 默认构造
Base2 b22 = b2; // 拷贝构造
cout << hex << *(uintptr_t *)&b1 << endl; // base1 虚指针
cout << hex << *(uintptr_t *)&b11 << endl; // base1 虚指针
cout << hex << *(uintptr_t *)&b2 << endl; // base2 虚指针
cout << hex << *(uintptr_t *)&b22 << endl; // base2 虚指针
cout << hex << *(uintptr_t *)&obj << endl; // obj虚指针

return 0;
}

gdb查看对象内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) p b1
$1 = {_vptr.Base1 = 0x555555557d28 <vtable for Base1+16>, x1 = -136147320, x2 = 32767, x3 = -136147320}
(gdb) p b11
$2 = {_vptr.Base1 = 0x555555557d28 <vtable for Base1+16>, x1 = 0, x2 = 0, x3 = 0}
(gdb) p b2
$3 = {<Base1> = {_vptr.Base1 = 0x555555557d00 <vtable for Base2+16>, x1 = -136147320, x2 = 32767,
x3 = -136147320}, y1 = 32767, y2 = -137033468, y3 = 32767}
(gdb) p b22
$4 = {<Base1> = {_vptr.Base1 = 0x555555557d00 <vtable for Base2+16>, x1 = -136147320, x2 = 32767,
x3 = -136147320}, y1 = 32767, y2 = -137033468, y3 = 32767}
(gdb) p obj
$5 = {<Base2> = {<Base1> = {_vptr.Base1 = 0x555555557cc0 <vtable for Obj+16>, x1 = -136147320,
x2 = 32767, x3 = -136147320}, y1 = 32767, y2 = -137033468, y3 = 32767}, a = -136147112,
b = 255 '\377', c = 6.9533490273497203e-310, d = -9.19059065e+33, e = 32767, f = 140737352208008}

b11和b1都是Base1对象,虚指针相同,而通过拷贝构造b2得到的b22,两个对象的虚指针也是一样的。但是我们可以发现,虚指针的值,并不是指向虚表开头,而是vtable+16。可见虚指针所指的位置前面还有16字节的内容。

接下来探究虚表中的内容。

查看Obj类对象的虚表前十六字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557cc0 (subobject @ 0x7fffffffe430):
[0]: 0x55555555563a <Obj::base1_virt1()>
[1]: 0x5555555555b6 <Base1::base1_virt2()>
[2]: 0x55555555560e <Base2::base2_virt2()>
[3]: 0x555555555666 <Obj::base2_virt1()>
[4]: 0x555555555692 <Obj::virtfunc1()>
[5]: 0x5555555556be <Obj::virtfunc2()>
(gdb) x /12x 0x555555557cc0
0x555555557cc0 <_ZTV3Obj+16>: 0x5555563a 0x00005555 0x555555b6 0x00005555
0x555555557cd0 <_ZTV3Obj+32>: 0x5555560e 0x00005555 0x55555666 0x00005555
0x555555557ce0 <_ZTV3Obj+48>: 0x55555692 0x00005555 0x555556be 0x00005555
(gdb) x /4x 0x555555557cc0-16 # 虚表开头16字节内容
0x555555557cb0 <_ZTV3Obj>: 0x00000000 0x00000000 0x55557d38 0x00005555
(gdb) x /4x 0x555555557d38 # 第二个64-bit字的内容
0x555555557d38 <_ZTI3Obj>: 0xf7e1dc30 0x00007fff 0x555560b1 0x00005555

虚指针所指处开始,每个条目为一个64-bit字,存储虚函数的地址。

通过c++filt命令查看de_mangle后的名字:

1
2
> c++filt _ZTI3Obj
> typeinfo for Obj

可见,第一个字是0,第二个字存了一个地址,保存着对象类型的信息。使用指针和引用时,我们可以通过其所指向的或所引用的对象的虚指针,找到对象的虚表,进而找到对象的类型信息,从而得知这个对象是父类对象还是子类对象,c++的多态机制应该也是通过这种方式实现的。

查看Base1对象的虚表:

1
2
3
4
5
6
7
8
9
10
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d28 (subobject @ 0x7fffffffe3b0):
[0]: 0x55555555558a <Base1::base1_virt1()>
[1]: 0x5555555555b6 <Base1::base1_virt2()>
(gdb) x /4x 0x555555557d28
0x555555557d28 <_ZTV5Base1+16>: 0x5555558a 0x00005555 0x555555b6 0x00005555
(gdb) x /4x 0x555555557d28-16 # 虚表开头16字节内容
0x555555557d18 <_ZTV5Base1>: 0x00000000 0x00000000 0x55557d68 0x00005555
(gdb) x /4x 0x555555557d68 # 第二个64-bit字的内容
0x555555557d68 <_ZTI5Base1>: 0xf7e1cfa0 0x00007fff 0x555560bd 0x00005555

c++filt查看_ZIT5Base1:

1
2
> c++filt _ZTI5Base1
typeinfo for Base1

查看Base2对象的虚表:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d00 (subobject @ 0x7fffffffe3f0):
[0]: 0x5555555555e2 <Base2::base1_virt1()>
[1]: 0x5555555555b6 <Base1::base1_virt2()>
[2]: 0x55555555560e <Base2::base2_virt2()>
(gdb) x /4x 0x555555557d00
0x555555557d00 <_ZTV5Base2+16>: 0x555555e2 0x00005555 0x555555b6 0x00005555
(gdb) x /4x 0x555555557d00-16 # 虚表开头16字节内容
0x555555557cf0 <_ZTV5Base2>: 0x00000000 0x00000000 0x55557d50 0x00005555
(gdb) x /4x 0x555555557d50 # 第二个64-bit字的内容
0x555555557d50 <_ZTI5Base2>: 0xf7e1dc30 0x00007fff 0x555560b6 0x00005555

c++filt查看_ZIT5Base2:

1
2
> c++filt _ZTI5Base2
typeinfo for Base2

我们可以将上述结论抽象为如下的图例:

Base1对象布局:

image-20231108201212879

Base2对象布局:

image-20231108201248944

Obj对象布局:

image-20231108201300556

多重继承

将上述程序修改为,Obj继承自Base1和Base2:

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 Base1
{
public:
int x1{};
virtual void base1_virt1()
{
cout << "Base 1 virtfunc1\n";
}
virtual void base1_virt2()
{
cout << "Base 1 virtfunc2\n";
}

int x2{};

int x3{};
};

class Base2
{

public:
int y1;
virtual void base2_virt1()
{
cout << "Base 2 virtfunc1 \n";
}

virtual void base2_virt2()
{
cout << "Base 2 virtfunc2 \n";
}

int y2;

int y3;
};

class Obj : public Base1, public Base2
{
public:
int a;
uint8_t b;
virtual void base1_virt1()
{
cout << hex << this << " Obj 1 virtfunc1 from base 1\n";
}

virtual void base2_virt1()
{
cout << hex << this <<" Obj 1 virtfunc1 from base 2\n";
}

virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}

public:
double c;
float d;
short e;

long f;
};

int main()
{
Obj obj;
Base1 b1;
Base2 b2;
obj.base1_virt1();
obj.base2_virt1();

return 0;
}

查看三个类对象的布局:

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
(gdb) p obj
$1 = {
<Base1> = { # Base1父类的数据成员以及一个虚指针
_vptr.Base1 = 0x555555557ca8 <vtable for Obj+16>,
x1 = 0,
x2 = 0,
x3 = 0
},
<Base2> = { # Base2父类的数据成员以及一个虚指针
_vptr.Base2 = 0x555555557ce0 <vtable for Obj+72>,
y1 = -136147320,
y2 = 32767,
y3 = -137033468
}, # 子类自己的数据成员
members of Obj:
a = 32767,
b = 88 'X',
c = 6.9533490273497203e-310,
d = -9.19059065e+33,
e = 32767,
f = 140737352208008
}
(gdb) p b1
$2 = {
_vptr.Base1 = 0x555555557d20 <vtable for Base1+16>,
x1 = 0,
x2 = 0,
x3 = 0
}
(gdb) p b2
$3 = {
_vptr.Base2 = 0x555555557d00 <vtable for Base2+16>,
y1 = 0,
y2 = 0,
y3 = 0
}

不难发现,在子类中,父类的数据成员按照声明的顺序排列,并且每个父类子对象都包含一个虚指针。我们可以认为,子类包含每个父类完整的子对象。三个类各自的虚表都不相同,因为虚表中存放的虚函数地址都是与类相关的。

查看Base1对象的虚表内容:

1
2
3
4
5
6
7
8
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d20 (subobject @ 0x7fffffffdd90):
[0]: 0x555555555420 <Base1::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
(gdb) x /4x 0x555555557d20-16 # Offset pointer to typeinfo
0x555555557d10 <_ZTV5Base1>: 0x00000000 0x00000000 0x55557d78 0x00005555
(gdb) x /4x 0x555555557d78
0x555555557d78 <_ZTI5Base1>: 0xf7e1cfa0 0x00007fff 0x555560b2 0x00005555

与上一节讨论的情况相同,b1的虚表中包含Base1的虚函数地址,以及一个偏移值(0)和一个指向类类型信息的指针。

同理,Base2对象的虚表内容也是一样的布局:

1
2
3
4
5
6
7
8
9
10
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d00 (subobject @ 0x7fffffffddb0):
[0]: 0x555555555478 <Base2::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>
(gdb) x /4x 0x555555557d00
0x555555557d00 <_ZTV5Base2+16>: 0x55555478 0x00005555 0x555554a4 0x00005555
(gdb) x /4x 0x555555557d00-16
0x555555557cf0 <_ZTV5Base2>: 0x00000000 0x00000000 0x55557d68 0x00005555
(gdb) x /4x 0x555555557d68
0x555555557d68 <_ZTI5Base2>: 0xf7e1cfa0 0x00007fff 0x555560ab 0x00005555

我们重点查看Obj类在多重继承的情况下,虚表的布局情况:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557ca8 (subobject @ 0x7fffffffddd0):
[0]: 0x5555555554d0 <Obj::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
[2]: 0x555555555524 <Obj::base2_virt1()>
[3]: 0x555555555582 <Obj::virtfunc1()>
[4]: 0x5555555555ae <Obj::virtfunc2()>

vtable for 'Base2' @ 0x555555557ce0 (subobject @ 0x7fffffffdde8):
[0]: 0x555555555577 <non-virtual thunk to Obj::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>

obj中包含两个虚指针。这实际上也是为多态机制服务的。在c++中,父类指针和子类指针都可以指向一个子类对象。严格来说,父类指针指向的是子类对象中的父类子对象。

修改main函数,验证上述说法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

int main()
{
Obj obj;
Base1 b1;
Base2 b2;
Obj * p1 = &obj;
Base1 * pb1 = &obj;
Base2 * pb2 = &obj;
cout << "Obj * -> obj: " << hex << p1 << endl;
cout << "Base1 * -> obj: " << hex << pb1 << endl;
cout << "Base2 * -> obj: " << hex << pb2 << endl;
pb1 = p1;
pb2 = p1;
cout << "Obj * -> obj: " << hex << p1 << endl;
cout << "Base1 * -> obj: " << hex << pb1 << endl;
cout << "Base2 * -> obj: " << hex << pb2 << endl;

return 0;
}

输出如下:

1
2
3
4
5
6
7
> ./test
Obj * -> obj: 0x7ffdc3d28750
Base1 * -> obj: 0x7ffdc3d28750
Base2 * -> obj: 0x7ffdc3d28768 # ?? 指向同一对象但是值不同
Obj * -> obj: 0x7ffdc3d28750
Base1 * -> obj: 0x7ffdc3d28750
Base2 * -> obj: 0x7ffdc3d28768 # ?? 直接将p1赋值到pb2也是相同结果

这是因为,c++编译器为我们做了调整,因为子类对象,我们也可以将它当成一个父类对象,子类对象包含父类对象的内容,即子类对象中的父类子对象。Obj类型的对象布局如下所示,首先是Base1的子对象,其次是Base2的子对象:

image-20231110195844016

当指向Base2对象的指针实际上指向的是Obj类型对象时,编译器将其调整为指向obj对象的Base2子对象。这样当我们通过Base2指针操作Obj对象中的Base2类的数据成员时,各成员的偏移是不需要更改的。

接下来探究obj对象中的两个虚指针所指向的虚表的内容:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557ca8 (subobject @ 0x7fffffffddd0):
[0]: 0x5555555554d0 <Obj::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
[2]: 0x555555555524 <Obj::base2_virt1()>
[3]: 0x555555555582 <Obj::virtfunc1()>
[4]: 0x5555555555ae <Obj::virtfunc2()>

vtable for 'Base2' @ 0x555555557ce0 (subobject @ 0x7fffffffdde8):
[0]: 0x555555555577 <non-virtual thunk to Obj::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>

打印obj的对象布局时,可以看到关于虚指针的描述信息:**_vptr.Base1 = 0x555555557ca8 <vtable for Obj+16>_vptr.Base2 = 0x555555557ce0 <vtable for Obj+72>**,这两个指针相差也确实是56字节,因此两个指针应该指向的是Obj类的虚表中的不同位置。回顾一下,单继承的多态下,对象头部八字节存放虚指针,指向虚表的第16字节处,虚表的前16字节包括一个8字节的偏移量(一般为0)以及一个指向类型信息的指针,然后是虚函数地址条目。

此处_vptr.Base1所指向的仍然是虚表的第16字节,而_vptr.Base2所指为虚表的第72字节。从第16字节到第72字节,包含5个虚函数条目,分别有Obj类继承自Base1的2个虚函数,重写Base2的一个虚函数,以及自身定义的两个虚函数。5个条目共占40字节。还剩下16字节,应该包含的是一个偏移量以及一个指向类型信息的指针。

通过gdb查看:

1
2
3
4
5
6
(gdb) x /4x 0x555555557ca8-16
0x555555557c98 <_ZTV3Obj>: 0x00000000 0x00000000 0x55557d30 0x00005555
(gdb) x /4x 0x555555557ce0-16
0x555555557cd0 <_ZTV3Obj+56>: 0xffffffe8 0xffffffff 0x55557d30 0x00005555
(gdb) x /4x 0x555555557d30
0x555555557d30 <_ZTI3Obj>: 0xf7e1dcf0 0x00007fff 0x555560a6 0x00005555

可见,第一个虚指针前16字节为偏移量0和指向obj类型信息的指针,而第二个虚指针前16字节为偏移量-24和指向obj类型信息的指针。因此Obj类的虚表其实相当于组合了两个父类的虚表,子类和第一个声明的父类共用第一个虚表(因为第一个声明的父类子对象就在子类对象的最前端),后续声明的父类的虚表相继排列在后面。第一个也称之为“主虚表”(primary virtual table),后续的虚表又称之为“次虚表”(secondary virtual table)。一个含有虚函数(无论是其本身的,还是继承而来的)的类,可以有一个主虚表和多个次虚表,主虚表和次虚表构成一个虚表组(virtual table group)。

现在我们可以完善Obj类型对象的布局图:

image-20231110202652872

为了支持多态,编译器使用虚指针前面的16字节存储两个信息:

  • 偏移量:当前父类子对象到该对象起始处的偏移,称其为top_offset
  • 类型信息指针:指向当前对象的类型信息,每个父类子对象的虚指针前8字节都指向同一个类型信息。

对于虚表中虚函数条目的排列顺序,在主虚函数表中,将父类的虚函数条目按序排列,如果子类重写了该虚函数,则条目设置为子类的虚函数地址,子类自定义的新的虚函数条目按序排在后面。

对于从虚表,按照父类的虚函数条目顺序排列,子类重写的虚函数则对应条目需要重新设置。不同于主表中直接设置为子类虚函数的地址,而是设置为指向一个子类虚函数的non-virtual thunk

gdb查看non-virtual thunk的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557ca8 (subobject @ 0x7fffffffde20):
[0]: 0x5555555554d0 <Obj::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
[2]: 0x555555555524 <Obj::base2_virt1()>
[3]: 0x555555555582 <Obj::virtfunc1()>
[4]: 0x5555555555ae <Obj::virtfunc2()>

vtable for 'Base2' @ 0x555555557ce0 (subobject @ 0x7fffffffde38):
[0]: 0x555555555577 <non-virtual thunk to Obj::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>
(gdb) x /10i 0x555555555577
0x555555555577 <_ZThn24_N3Obj11base2_virt1Ev>: endbr64
0x55555555557b <_ZThn24_N3Obj11base2_virt1Ev+4>: sub $0x18,%rdi # %rdi存储第一个参数 this指针,此处调整this指针指向子类对象
0x55555555557f <_ZThn24_N3Obj11base2_virt1Ev+8>: jmp 0x555555555524 <_ZN3Obj11base2_virt1Ev> # 调用子类虚函数
0x555555555581: nop
...

可见,non-virtual thunk是编译器根据top_offset,编写的一小段代码,调整指向父类子对象的指针指向子类对象,以便于调用子类虚函数时,this指针是指向当前对象的。

虚拟继承

精简每个类的数据成员,添加一个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

class Base
{
public:
int x{};
virtual void base_virt()
{
cout << "Base virtfunc\n";

}
};

class Base1: virtual public Base
{
public:
int x1{};
virtual void base1_virt()
{
cout << "Base 1 virtfunc\n";
}
};

class Base2: virtual public Base
{

public:
int x2;
virtual void base2_virt()
{
cout << "Base 2 virtfunc \n";
}
};

class Obj : public Base1, public Base2
{
public:
int y{};

virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}
};

int main()
{
Obj obj;
Base1 b1;
Base2 b2;

return 0;
}