C++ 继承和虚继承的内存布局

发布于 2025 年 3 月 12 日

图片由 Mika Baumeister 提供。

C++ 通过虚表实现动态多态。在 Visual Studio 中可以通过向编译器添加命令行参数 /d1 reportAllClassLayout 来查看类和虚表的内存布局。

实继承

对于“实继承”,在子类对象的开头,会有一个父类对象的拷贝。例如:

class Foo {
public:
    int value;
    virtual void fn() {}
};

class Bar : public Foo {
    virtual void fn() override {}
};

FooBar 在 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(虚函数表指针),记录各个虚函数的地址。

对于类 WingedAnimalMammal,因为他们虚继承自 Animal,它们的 Animal 对象拷贝只存在一份,且置于各自对象的尾部,而不是实继承时的头部。 在头部,有一个 vbptr(虚基类指针)指向一个虚基类表,虚基类表中含有 N 项,N = (直接或间接虚父类的数量) + 1。每一项占 4 个字节的空间。

第一项的值记录着 vfptr 相对于 vbptr 的偏移量。 因为 WingedAnimalMammal 没有增加额外的虚函数,它们各自的对象实体中不需要增加新的虚函数表,自然也就没有 vfptr;所以,这里的偏移量是 0。 如果为 Mammal 添加一个新的虚函数,那么 Mammal 对象中就有了额外的 vfptr,此时 Bat::$vbtable@Mammal@ 第一项的值就是 -4。

后续各项记录了虚继承的父类对象在实体对象中相较于当前类的偏移量。例如,WingedAnimal 对象在 Bat 中的偏移量为 8,而 Animal 的偏移量为 16,所以 Bat::$vbtable@WingedAnimal 的第二项就是 8。