GDB 观察 C++ 对象内存布局:对齐、vptr 与虚表
使用 GDB 观察 C++ 空类、内存对齐、虚函数和虚继承对象的内存布局,并说明实验结论的 ABI 和编译器限制。
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、目标架构和优化级别变化后,实验结果都可能变化。