C++ 通过虚表实现动态多态。在 Visual Studio 中可以通过向编译器添加命令行参数 /d1 reportAllClassLayout
来查看类和虚表的内存布局。
实继承
对于“实继承”,在子类对象的开头,会有一个父类对象的拷贝。例如:
class Foo {
public:
int value;
virtual void fn() {}
};
class Bar : public Foo {
virtual void fn() override {}
};
Foo
和 Bar
在 amd64 架构上的内存布局如下:
class Foo size(16):
+---
0 | {vfptr}
8 | value
| <alignment member> (size=4)
+---
class Bar size(16):
+---
0 | +--- (base class Foo)
0 | | {vfptr}
8 | | value
| | <alignment member> (size=4)
| +---
+---
虚继承
为了解决菱形继承导致产生多个基类对象拷贝的问题,C++ 引入了虚继承。例如:
class Animal {
public:
int age = 0;
virtual void fn() {}
};
class WingedAnimal : virtual public Animal {
virtual void fn() override {}
};
class Mammal : virtual public Animal {
virtual void fn() override {}
};
class Bat : public WingedAnimal, public Mammal {
virtual void fn() override {}
};
此时在 amd64 上的内存结构为:
class Animal size(16):
+---
0 | {vfptr}
8 | age
| <alignment member> (size=4)
+---
class WingedAnimal size(24):
+---
0 | {vbptr}
+---
+--- (virtual base Animal)
8 | {vfptr}
16 | age
| <alignment member> (size=4)
+---
WingedAnimal::$vbtable@:
0 | 0
1 | 8 (WingedAnimald(WingedAnimal+0)Animal)
class Mammal size(24):
+---
0 | {vbptr}
+---
+--- (virtual base Animal)
8 | {vfptr}
16 | age
| <alignment member> (size=4)
+---
Mammal::$vbtable@:
0 | 0
1 | 8 (Mammald(Mammal+0)Animal)
class Bat size(32):
+---
0 | +--- (base class Mammal)
0 | | {vbptr}
| +---
8 | +--- (base class WingedAnimal)
8 | | {vbptr}
| +---
+---
+--- (virtual base Animal)
16 | {vfptr}
24 | age
| <alignment member> (size=4)
+---
Bat::$vbtable@Mammal@:
0 | 0
1 | 16 (Batd(Mammal+0)Animal)
Bat::$vbtable@WingedAnimal@:
0 | 0
1 | 8 (Batd(WingedAnimal+0)Animal)
对于基类 Animal
,因为它存在 virtual
方法,所以是一个虚类。它包含一个 vfptr
(虚函数表指针),记录各个虚函数的地址。
对于类 WingedAnimal
和 Mammal
,因为他们虚继承自 Animal
,它们的 Animal
对象拷贝只存在一份,且置于各自对象的尾部,而不是实继承时的头部。
在头部,有一个 vbptr
(虚基类指针)指向一个虚基类表,虚基类表中含有 N 项,N = (直接或间接虚父类的数量) + 1。每一项占 4 个字节的空间。
第一项的值记录着 vfptr
相对于 vbptr
的偏移量。
因为 WingedAnimal
和 Mammal
没有增加额外的虚函数,它们各自的对象实体中不需要增加新的虚函数表,自然也就没有 vfptr
;所以,这里的偏移量是 0。
如果为 Mammal
添加一个新的虚函数,那么 Mammal
对象中就有了额外的 vfptr
,此时 Bat::$vbtable@Mammal@
第一项的值就是 -4。
后续各项记录了虚继承的父类对象在实体对象中相较于当前类的偏移量。例如,WingedAnimal
对象在 Bat
中的偏移量为 8,而 Animal
的偏移量为 16,所以 Bat::$vbtable@WingedAnimal
的第二项就是 8。