是的,由于虚拟继承,即使没有虚拟函数,编译器也会创建 vptr。为了理解使用 gcc 编译器,我们可以使用 (-fdump-tree-all) 标志并查看可以找到 vptr 和 vtable 布局的中间文件 (*.class)。
$ g++ -fdump-tree-all -Wall basic.cpp -o basic
现在我们可以从中间的 basic.class 文件中找到关于 vptr 和 vtable 布局的信息。
// 类信息
Class a
size=1 align=1
base size=0 base align=1
a (0x0x7fc8d707e2a0) 0 empty
//b类vptr和大小信息
Vtable for b
b::_ZTV4b: 3u entries
0 0u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI4bbbb)
VTT for b
b::_ZTT4b: 1u entries
0 ((& b::_ZTV4b) + 24u)
Class b
size=8 align=8
base size=8 base align=8
b (0x0x7fc8d7053e38) 0 nearly-empty
vptridx=0u vptr=((& b::_ZTV4bbbb) + 24u)
a (0x0x7fc8d707e300) 0 empty virtual
vbaseoffset=-24
//class c vptr和大小信息
Vtable for c
c::_ZTV4c: 3u entries
0 0u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI4cccc)
VTT for c
c::_ZTT4c: 1u entries
0 ((& c::_ZTV4c) + 24u)
Class c
size=8 align=8
base size=8 base align=8
c (0x0x7fc8d7053ea0) 0 nearly-empty
vptridx=0u vptr=((& c::_ZTV4c) + 24u)
a (0x0x7fc8d707e360) 0 empty virtual
vbaseoffset=-24
//class d vptr和大小信息
Vtable for d
d::_ZTV4d: 6u entries
0 0u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI4d)
24 18446744073709551608u
32 (int (*)(...))-8
40 (int (*)(...))(& _ZTI4d)
Construction vtable for b (0x0x7fc8d70f8000 instance) in d
d::_ZTC4d0_4b: 3u entries
0 0u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI4b)
Construction vtable for c (0x0x7fc8d70f8068 instance) in d
d::_ZTC4d8_4c: 3u entries
0 18446744073709551608u
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI4c)
VTT for d
d::_ZTT4d: 4u entries
0 ((& d::_ZTV4d) + 24u)
8 ((& d::_ZTC4d0_4b) + 24u)
16 ((& d::_ZTC4d8_4c) + 24u)
24 ((& d::_ZTV4d) + 48u)
Class d
size=16 align=8
base size=16 base align=8
d (0x0x7fc8d70cca80) 0
vptridx=0u vptr=((& d::_ZTV4d) + 24u)
b (0x0x7fc8d70f8000) 0 nearly-empty
primary-for d (0x0x7fc8d70cca80)
subvttidx=8u
a (0x0x7fc8d707e3c0) 0 empty virtual
vbaseoffset=-24
c (0x0x7fc8d70f8068) 8 nearly-empty
subvttidx=16u vptridx=24u vptr=((& d::_ZTV4d) + 48u)
a (0x0x7fc8d707e3c0) alternative-path
这解释了这里发生了什么以及为什么以及如何对象大小会根据创建的 vptr 数量而变化。我的机器是 x86_64 GNU/Linux,因此指针大小将是 8 而不是原始示例中的 4。