☰
vptr(虚表指针) 和 vtable(虚表)
2026/9/28 4:05:16 网站建设 项目流程

1.1 什么是 vptr 和 vtable

C++ 通过虚函数表(vtable)实现运行时多态。当一个类含有虚函数时,编译器会:

  1. 为这个类生成一张虚表(vtable):本质是一个函数指针数组,里面存放该类所有虚函数的地址。

  2. 在每个对象的内存布局最前面插入一个隐藏指针(vptr):指向该类的 vtable。

也就是说,一个含虚函数的对象,内存布局大致是:

+-------------------+ | vptr | <- 8 字节(64 位系统),指向 vtable +-------------------+ | 成员变量 1 | +-------------------+ | 成员变量 2 | +-------------------+ | ... | +-------------------+

1.2 关键区别

概念位置说明
vptr在对象内存里每个对象一份,指向所属类的 vtable
vtable在只读数据段(.rodata)每个类一份,被所有对象共享

不同编译器(如 MSVC)的 vtable 布局可能不同,但核心思想一致。64 位系统下指针是 8 字节,vptr 占 8 字节。

二、实验代码

#include <iostream> class Base { public: Base() : a(0x11) {} virtual ~Base() {} virtual void func1() { std::cout << "Base::func1\n"; } virtual void func2() { std::cout << "Base::func2\n"; } virtual int func3(int x) { return x + 1; } int a; }; class Derived : public Base { public: Derived() : b(0x22) {} ~Derived() override {} void func1() override { std::cout << "Derived::func1\n"; } void func2() override { std::cout << "Derived::func2\n"; } virtual void func4() { std::cout << "Derived::func4\n"; } int b; }; int main() { Base base; Derived derived; Base* p1 = &base; Base* p2 = &derived; p1->func1(); p2->func1(); return 0; }

编译时必须加-g保留调试信息,并关闭优化避免布局被改变:

g++ -g -O0 -o vtable_demo vtable_demo.cpp //-g 生成调试信息,供 gdb 等调试器使用 //-O0 关闭所有优化(这是默认级别,显式写出是为了强调)

优化级别对比

级别含义编译速度运行性能调试友好度
-O0不优化(默认)最快最差最好
-O1基础优化快中等较好
-O2中等偏强优化中等好一般
-O3激进优化慢最好(多数情况)差
-Os优化代码体积中等较好一般
-Ofast超越-O3,可能违反标准慢最高差

三、用 GDB 观察 vptr

3.1 启动 GDB 并设置断点

gdb ./vtable_demo

在 GDB 里执行:

(gdb) break main (gdb) run

程序停在main开头。

设置断点的基本语法

break <位置> # 简写 b //当前文件的某一行: (gdb) b 42 //指定文件的某一行 (gdb) b vptr.cpp:42 //如果文件不在当前目录,GDB 可能找不到,需要用相对或绝对路径 (gdb) b /home/user/project/vptr.cpp:42 //普通函数 (gdb) b main (gdb) b func1 //类成员函数 (gdb) b Derived::func1 (gdb) b Base::func1 //带参数的函数名 (gdb) b Derived::func1(int) (gdb) b Derived::func1(const std::string&) //指定文件里的函数 (gdb) b vptr.cpp:Derived::func1

3.2 单步到对象构造完成

(gdb) next # 执行 Base base; (gdb) next # 执行 Derived derived;

现在base和derived都已构造完成,vptr 已经被设置。

3.3 查看对象的内存布局

查看 base 对象:

(gdb) print base

输出类似:

$1 = {_vptr.Base = 0x555555557d30 <vtable for Base+16>, a = 17}
(gdb) print derived

输出类似:

$2 = {<Base> = {_vptr.Base = 0x555555557d10 <vtable for Derived+16>, a = 17}, b = 34}

可以看到,derived的 vptr 指向Derived的 vtable(地址不同)。

💡+16是什么意思?这是 GDB 的友好提示:vtable 数组前面还有 2 个 8 字节的元数据(offset-to-top 和 typeinfo 指针),所以 vptr 实际指向的是 vtable 的第 3 个槽位(偏移 16 字节)。后面会详细讲。

3.4 直接查看对象内存的原始字节

上面的print是 GDB 用调试信息“美化”后的结果。要看真实内存,用x命令:

(gdb) x/4gx &base

输出类似:

0x7fffffffe2d0: 0x0000555555557d30 0x0000000000000011 0x7fffffffe2e0: 0x0000000000000000 0x0000000000000000

解读:

  • 第 1 个 8 字节0x0000555555557d30:vptr,指向 Base 的 vtable。

  • 第 2 个 8 字节0x0000000000000011:成员a = 0x11 = 17。

(gdb) x/4gx &derived

输出类似:

0x7fffffffe2c0: 0x0000555555557d10 0x0000000000000011 0x7fffffffe2d0: 0x0000000000000022 0x0000000000000000

解读:

  • 第 1 个 8 字节0x0000555555557d10:vptr,指向 Derived 的 vtable。

  • 第 2 个 8 字节0x0000000000000011:基类成员a = 0x11。

  • 第 3 个 8 字节0x0000000000000022:派生类成员b = 0x22。

注意:Derived对象的成员a和b是连续存放的,vptr 在最前面。

四、深入观察 vtable 的内容

4.1 找到 vtable 的起始位置

前面看到base的 vptr 是0x555555557d30,GDB 提示它等于vtable for Base+16。这意味着 vtable 的真正起始地址是:

0x555555557d30 - 16 = 0x555555557d20

用 GDB 查看这个地址开始的内存:

(gdb) x/8gx 0x555555557d20

输出类似:

0x555555557d20: 0x0000000000000000 0x0000555555557c80 0x555555557d30: 0x0000555555555b0a 0x0000555555555b2c 0x555555557d40: 0x0000555555555b4e 0x0000555555555b70 0x555555557d50: 0x0000555555555b92 0x0000000000000000

4.2 解析 vtable 结构

对于 GCC 的 Itanium C++ ABI,vtable 的布局是:

+---------------------------+ | offset-to-top (8 字节) | <- vtable 起始 +---------------------------+ | typeinfo 指针 (8 字节) | <- 指向 RTTI 信息 +---------------------------+ | 虚函数 1 地址 (8 字节) | <- vptr 指向这里 +---------------------------+ | 虚函数 2 地址 (8 字节) | +---------------------------+ | ... | +---------------------------+

所以上面的内存解读是:

地址值含义
0x555555557d200x0offset-to-top = 0(单继承,无偏移)
0x555555557d280x555555557c80typeinfo 指针(指向typeinfo for Base)
0x555555557d300x555555555b0a虚函数 1 地址(vptr 指向这里)
0x555555557d380x555555555b2c虚函数 2 地址
0x555555557d400x555555555b4e虚函数 3 地址
0x555555557d480x555555555b70虚函数 4 地址(析构相关)

4.3 把地址翻译成函数名

用info symbol把地址翻译成函数名:

(gdb) info symbol 0x555555555b0a

输出类似:

Base::func1() in section .text

依次查看:

(gdb) info symbol 0x555555555b2c (gdb) info symbol 0x555555555b4e (gdb) info symbol 0x555555555b70

你会看到类似:

Base::func2() in section .text Base::func3(int) in section .text Base::~Base() in section .text

这证明了:vtable 里按声明顺序存放了虚函数的地址。

4.4 对 Derived 的 vtable 做同样操作

(gdb) x/8gx 0x555555557d00 (gdb) info symbol 0x555555555b0a # 应该看到 Derived::func1

你会看到Derived的 vtable 里,func1和func2指向的是Derived的版本(地址不同),这就是虚函数覆盖(override)在内存层面的体现。

五、观察运行时多态的真实过程

5.1 在多态调用处下断点

(gdb) break vtable_demo.cpp:39 (gdb) continue

或者直接在p2->func1()那行下断点。

5.2 查看调用时的 vptr

当程序停在p2->func1()时:

(gdb) print p2 (gdb) print *p2

你会看到p2是Base*类型,但*p2的 vptr 指向的是Derived的 vtable。这就是动态绑定的关键:

编译器生成的代码是:(*p2->vptr[0])(p2),即通过 vptr 找到 vtable,再取第 0 个函数指针调用。由于p2实际指向Derived对象,vptr 指向Derived的 vtable,所以调用的是Derived::func1。

5.3 反汇编验证

(gdb) disassemble /m main

你会看到类似:

mov -0x18(%rbp), %rax ; 把 p2 加载到 rax mov (%rax), %rax ; 取 vptr(rax 指向对象首地址) mov (%rax), %rax ; 取 vtable[0](func1 地址) mov -0x18(%rbp), %rdi ; 把 p2 作为 this 参数 call *%rax ; 间接调用

这就是多态的底层实现:两次解引用(vptr → vtable → 函数)。


六、进阶实验:多重继承的 vptr

如果类有多重继承,对象里会有多个 vptr。修改代码:

class A { public: virtual void fa() {} int a; }; class B { public: virtual void fb() {} int b; }; class C : public A, public B { public: void fa() override {} void fb() override {} int c; };

用 GDB 查看C对象:

(gdb) x/8gx &c

你会看到两个 vptr,分别指向C的两个 vtable(主 vtable 和次 vtable)。这是多重继承下对象布局的典型特征。

七、常见坑与注意事项

坑说明
优化等级-O2会打乱布局,必须用-O0
调试信息必须加-g,否则 GDB 无法识别类型
vtable 地址不固定每次运行 ASLR 会改变地址,用info symbol而非硬编码
MSVC 布局不同Windows 下 MSVC 的 vtable 布局与 GCC 不同,本教程不适用
虚析构函数析构函数也是虚函数,会占用 vtable 槽位
纯虚函数纯虚函数在 vtable 里通常指向__cxa_pure_virtual,调用会崩溃

八、完整的 GDB 命令速查

# 启动与断点 gdb ./vtable_demo break main run # 查看对象 print base print derived print *p2 # 查看内存 x/4gx &base x/4gx &derived # 查看 vtable x/8gx 0x555555557d20 # 地址翻译 info symbol 0x555555555b0a # 反汇编 disassemble /m main # 查看类型信息 ptype Base ptype Derived

九、总结:一张图看懂全貌

对象内存 vtable(.rodata) +----------------+ +---------------------+ | vptr ---------|-----------> | offset-to-top = 0 | +----------------+ +---------------------+ | 成员 a = 0x11 | | typeinfo ptr | +----------------+ +---------------------+ | &Base::func1 | +---------------------+ | &Base::func2 | +---------------------+ | &Base::func3 | +---------------------+ | &Base::~Base | +---------------------+

多态调用p->func1()的底层路径:

p -> vptr -> vtable[0] -> Base::func1 或 Derived::func1

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx ,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习:链接

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询