C++ 标准并不规定所有对象在目标平台上的具体字节布局。对象大小、成员对齐、虚函数表和虚表指针的位置,可能受到编译器、目标架构和 ABI 的影响。

本文使用 GCC 11.4、GDB 12.1 和 Linux x86-64 进行观察。文中的字节数和虚表布局只代表这组实验环境,不能直接推广到 MSVC、其他架构或其他编译器。

一、准备调试环境

编译时加入调试信息,并关闭优化以便观察源代码中的对象:

g++ -std=c++17 -g3 -O0 -fno-omit-frame-pointer test.cpp -o test
gdb ./test

常用 GDB 命令如下:

(gdb) break 25          # 在指定行设置断点
(gdb) print object      # 打印对象,简写为 p
(gdb) print sizeof(obj) # 查看对象大小
(gdb) print/x &obj     # 以十六进制打印对象地址
(gdb) x/4gx &obj       # 按 8 字节单元查看内存
(gdb) info vtbl obj     # 查看对象的虚函数表
(gdb) info symbol addr  # 查找地址对应的符号

x/nfu address 用于查看内存:

  • n:显示的单元数量。
  • f:显示格式,例如 x 为十六进制、d 为有符号十进制、c 为字符。
  • u:单元大小,b 为字节、h 为 2 字节、w 为 4 字节、g 为 8 字节。

例如,x/4gx &obj 表示从 obj 地址开始,以 8 字节为一个单元显示 4 个十六进制值。

二、空类对象

class Empty {
};

int main() {
  Empty object;
  return sizeof(object) == 1 ? 0 : 1;
}

在常见实现中,空类对象的大小为 1。即使它没有数据成员,不同的对象仍然需要能够拥有不同的地址,因此编译器通常会为它保留至少一个字节。

(gdb) break main
(gdb) run
(gdb) print sizeof(object)
$1 = 1

空类对象大小

三、成员对齐

考虑下面的结构体:

struct Aligned {
  char first;
  double second;
  char third;
};

int main() {
  Aligned object{};
  return static_cast<int>(sizeof(object));
}

在默认对齐规则的 x86-64 GCC 环境下,它通常占用 24 字节:

char first       1 字节
填充             7 字节
double second    8 字节
char third       1 字节
尾部填充         7 字节
总计             24 字节

可以使用 offsetof 查看标准布局类型中成员的偏移:

(gdb) print sizeof(object)
(gdb) print/x &object
(gdb) print/d (char*)&object.second - (char*)&object

成员排列和填充会影响对象大小。调整成员声明顺序有时可以减少填充,但不能只根据一次实验结果编写依赖特定布局的可移植代码。

成员对齐实验

四、带虚函数的对象

class Base {
public:
  virtual ~Base() = default;
  virtual void Foo() {}

  int value = 0;
  char flag = 0;
};

int main() {
  Base object;
  return object.value;
}

在常见的 Linux x86-64 ABI 下,带虚函数的对象通常包含一个指向虚函数表的指针,称为 vptr。在该实验环境中,vptr 通常占 8 字节,随后是成员数据和对齐填充。

GDB 中可以观察对象地址和虚表:

(gdb) break main
(gdb) run
(gdb) print sizeof(object)
(gdb) print/x &object
(gdb) x/4gx &object
(gdb) info vtbl object

带虚函数的对象布局

info vtbl 是 GDB 针对 C++ 调试信息和目标 ABI 提供的观察手段。虚函数表的具体条目、RTTI 信息和辅助函数不属于可移植的 C++ 接口。

虚函数表内存内容

虚函数表符号信息

五、单继承对象

class Base {
public:
  virtual void Foo() {}
  virtual void Bar() {}

  int value = 0;
  char flag = 0;
};

class Derived : public Base {
public:
  void Foo() override {}
};

int main() {
  Derived first;
  Derived second;
  Base* base = &first;
  return base == nullptr;
}

在同一个编译单元和 ABI 下,不同 Derived 实例通常会把 vptr 指向同一张 Derived 虚函数表。派生类重写的 Foo 会使用派生类实现,没有重写的 Bar 仍然指向基类实现。

(gdb) info vtbl first
(gdb) print/x &first
(gdb) print/x &second

单继承对象模型

单继承虚函数表模型

不同编译器可能把 vptr 放在不同位置,也可能使用不同的虚表辅助布局,因此这里的观察结果只能用于理解当前 ABI 的实现。

六、虚继承对象

虚继承用于解决菱形继承中的公共基类重复问题。它通常需要额外的指针或偏移信息来定位虚基类:

class Base {
public:
  virtual void Foo() {}
  int value = 0;
};

class Derived : virtual public Base {
public:
  void Foo() override {}
  int extra = 0;
};

int main() {
  Derived object;
  return object.extra;
}

可以使用对象内存查看命令和 info vtbl 观察 GCC 在当前 ABI 下生成的布局:

(gdb) print sizeof(object)
(gdb) print/x &object
(gdb) x/8gx &object
(gdb) info vtbl object

虚继承对象布局

虚继承对象内存信息

虚继承的布局比单继承更依赖 ABI。代码不应该通过强制转换裸地址、假设某个偏移或直接读取虚表来实现业务逻辑。

七、实验结论的边界

通过 GDB 观察内存布局适合用于:

  • 理解对象、成员和填充字节的关系。
  • 学习虚函数调用可能需要的运行时数据。
  • 分析特定编译器生成的目标文件。
  • 排查 ABI 不匹配或内存破坏问题。

它不适合用于推导跨编译器、跨平台的 C++ 对象布局。需要可移植的代码时,应使用语言和标准库提供的接口,而不是依赖 vptr、虚表地址或成员偏移。

八、总结

  • 空类对象通常占用至少一个字节,但具体布局仍由实现决定。
  • 成员对齐和尾部填充会影响对象大小。
  • 含虚函数的对象通常包含 vptr,但位置和虚表结构属于 ABI 实现细节。
  • info vtbl、x/ 和 info symbol 可以帮助观察当前平台的对象布局。
  • GCC、GDB、目标架构和优化级别变化后,实验结果都可能变化。