尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

多重继承与虚继承:两个 vptr 与掰弯的指针

发布时间:2026/9/29 3:30:43

资讯中心
01
ARTICLE

多重继承与虚继承:两个 vptr 与掰弯的指针

多重继承与虚继承:两个 vptr 与掰弯的指针
① 钩子一个对象能有两个 vptr一个对象能有两个 vptr——能。只要你多重继承两个带虚函数的基类。更反直觉的是调用第二个基类的虚函数时程序得先把指针**“掰弯”**加一个偏移才能找到正确的虚表和子对象。而一旦牵涉虚继承钻石问题代价还要再翻一档。这一集我们从sizeof实测出发把多重继承和虚继承的布局、指针调整adjustor thunk、虚基类偏移表全部解剖开。② 源码 vs 实测对照structA{virtualvoidfa(){}inta;};structB{virtualvoidfb(){}intb;};structC:A,B{virtualvoidfc(){}intc;};structVBase{intv;};structD1:virtualVBase{intd1;};structD2:virtualVBase{intd2;};structDiamond:D1,D2{intd;};voidcall_fb(C*c){c-fb();}// 调用第二个基类的虚函数本机实测sizeofg 15.2.0x86-64Itanium ABIA16 B16 C32 VBase4 D116 D216 Diamond40C的布局——两个 vptr偏移 0 8 12 16 24 28 32 ┌──────────┬────┬────┬──────────┬────┬────┐ │ vptrA │ a │ pad │ vptrB │ b │ c │ └──────────┴────┴────┴──────────┴────┴────┘ A 子对象16B B 子对象16Bcall_fb(C*)的汇编-O2——看掰弯发生在哪call_fb(C*): leaq B::fb(%rip), %rdx ; 预取 B::fb 地址投机去虚化见 E08 movq 16(%rcx), %rax ; ← 从偏移 16 取 vptrB 子对象的虚表 movq (%rax), %rax ; vtable[0]fb 的槽位 cmpq %rdx, %rax jne .L5 ret ; 投机命中B::fb 是空函数 .L5: addq $16, %rcx ; ← 指针掰弯C* 加 16 → 指向 B 子对象 jmp *%rax ; 然后才做虚调用Diamond的布局——共享的 VBase 只出现一次偏移 0 16 32 36 40 ┌──────────┬──────────┬────┬────┐ │ D1 子对象 │ D2 子对象 │ d │ v │ │ vptrd1 │ vptrd2 │ │ │ └──────────┴──────────┴────┴────┘ 16B 16B 4B 4B ← 唯一的 VBase 共享子对象③ 为什么这么设计多重继承 每个多态基类放一份子对象各带一个 vptr。C里有 A 子对象和 B 子对象所以对象开头有两个 vptr、两个虚表。对象更大虚调用也更绕。掰弯是必须的调用fb()时编译器手里的指针是C*指向对象开头但 B 子对象在偏移 16 处。它得先算B 子对象在哪取那里的 vptr查表再把指针调整到 B 子对象上——这条addq $16就是指针调整adjustor thunk。每次跨基类虚调用都背着这笔开销。虚继承解决钻石歧义Diamond同时虚继承自D1和D2而两者又都虚继承VBase。普通继承会让VBase出现两份虚继承保证只存一份上面布局里v只出现一次。虚继承的代价虚基类子对象的位置不再固定要靠在 vtable 里的偏移表vbase offset运行时查找。所以虚继承访问虚基类成员比普通继承多一次间接对象也更大多了用于寻址的 vptr/偏移空间。对比一句话普通多重继承是静态布局编译期算偏移虚继承是运行时查偏移表。④ 深入一把指针掰弯这件事拆到骨子里call_fb里的addq $16, %rcx是关键。它回答了一个微妙的问题虚函数fb是 B 的成员它的this应该指向 B 子对象而不是 C 对象的开头。C 对象的开头是 A 子对象含 vptrAB 子对象从偏移 16 开始调用fb()时进入B::fb的this必须是 B 子对象的地址所以编译器必须在跳转前把C*调整成B*16。这就是adjustor thunk的一种形态。更常见的情形是B::fb thunk: addq $16, %rdi ; this 16把 C 的 this 调成 B 子对象 jmp B::fb ; 再跳真正的实现当派生类覆盖了基类虚函数时编译器会为从 B 视角调用生成一个 thunk专门负责把 this 掰到正确子对象。所以派生类的虚表里指向派生覆盖函数的槽位有时存的是 thunk 的地址而不是函数本体——这就是隐藏代码的又一例。⑤ 深入二虚继承的运行时查偏移普通继承Diamond里的v在编译期就固定了偏移36movl 36(%rax), %eax一条指令搞定。虚继承因为VBase子对象的位置不固定虚基类可以出现在对象尾部且不同最派生类布局不同编译器必须运行时查表; 访问 diamond.v虚基类成员 movq 8(%rax), %rax ; 从 vptr 附近取虚基类偏移表指针 movl (%rax), %eax ; 查偏移可能还要加 addq %rax, %rcx ; 定位 VBase 子对象 movl (%rcx), %eax ; 这才读到 v对比普通继承的一条movl 偏移虚继承多了两次间接访问 一次加法。这就是虚继承是持续税的机器理由每次访问虚基类成员都在为运行时才知道基类在哪买单。⑥ 常见误区误区 1“多重继承很慢”不一定。非虚的多重继承在访问自己/第一基类成员时和单继承一样快编译期偏移只有跨基类虚调用、虚继承访问才慢。误区 2“钻石问题只能靠虚继承解决”虚继承只是共享子对象的答案很多场景改用组合把公共部分作为成员更简单、更快。误区 3“C*转B*是零成本”错涉及指针偏移16是真实的一条指令见 E10 的dynamic_cast更贵。误区 4“虚继承让对象变小”正相反虚继承对象通常更大多了偏移表指针/寻址开销Diamond40就是证据。而且虚继承访问虚基类成员每次都要查表定位指令数多于普通继承的直接偏移。误区 5“两个 vptr 只是浪费 16 字节”不止——跨基类虚调用每次都要指针调整虚继承访问每次都要查表这是运行时的持续开销。换句话说多继承的税分两笔一笔是对象变大空间一笔是每次跨基类调用/虚基类访问多几条指令时间。误区 6“dynamic_cast到基类也是运行时开销”向上转型派生→基类通常是编译期完成的指针调整是常量偏移只有向下/跨枝转型基类→派生、兄弟基类才需要运行时__dynamic_castE10 详述。别把所有转换都当慢。向上转型C*→A*或C*→B*在汇编里只是addq $0/$16零运行时决策。⑦ 实战启示别为看着面向对象随意多重继承每多一个多态基类 多一个 vptr 更绕的虚调用 更大的对象。接口多继承可以但要有数纯抽象接口无成员作为基类代价主要是 vptr 数量带数据成员的多继承成本明显更高。能用组合就组合真出现钻石依赖先想是不是该用组合/指针成员代替继承而不是直接上虚继承——虚继承的运行时间接访问是持续税。跨基类的类型转换有成本把C*转成B*涉及指针偏移调整dynamic_cast到兄弟基类更是运行时路径见 E10。关心布局就用static_assert(sizeof(...)N)守门把关键类的体积固化成断言别人改坏了编译立刻失败E23 会展开 ABI 一致性。⑧ 扩展专题一C 的对象模型其实是 ABI 的产物你可能好奇为什么C里 A 子对象在前、B 在后为什么 vptr 在最前这些不是 C 标准规定的而是Itanium C ABI本系列用的 g/clang 遵循它规定的布局规则。标准只保证可观察语义布局细节交给 ABI。理解这一点很重要换编译器/换平台布局可能变E23 会实测 Windows vs Linux 的差异但同一 ABI 内布局稳定所以sizeof/offsetof可以跨翻译单元一致依赖布局的代码序列化、内存映射结构体必须锁死 ABI否则就是定时炸弹。C的 32 字节、Diamond的 40 字节都是 Itanium ABI 在 x86-64 下的具体产物。我们本系列所有实测数字都以g 15.2.0 x86-64 Itanium ABI为基准。⑨ 扩展专题二怎么看见对象的完整布局工程上想知道一个复杂继承对象的布局有三个手段offsetof逐个打只对标准布局类型合法——注意带虚函数的类不是 standard layout见 E06clang -fdump-record-layouts直接把每个类的成员偏移、大小、对齐画成 ASCII 布局图这是最直观的看构造函数反汇编vptr 被写入哪些偏移、成员在哪些偏移构造都能从-O0 -S还原出布局。例如用 clang 的-Xclang -fdump-record-layouts编译E09_mixin.cpp你会看到每个类的字段偏移表和本集画的 ASCII 图完全一致——这是把猜测布局变成读官方数据的利器。⑩ 扩展 FAQQC里为什么是 32 字节而不是 28AA 子对象 16 字节vptr8 a4 尾对齐4B 子对象 16 字节vptr8 b4 c4加起来正好 32且整体对齐 8。c 在 B 子对象的 b 之后没有额外填充。Q多重继承下typeid(*c)返回什么A返回最派生类型C的 typeinfo。vptr 通常指向最派生类的 vtableRTTI 信息从那里取E10 详述。QDiamond里v为什么在最后AItanium ABI 把虚基类子对象放在对象尾部越虚越靠后普通基类/成员按声明顺序在前。这是该 ABI 的约定不是 C 标准要求的。Q虚继承真的比组合慢很多吗A访问虚基类成员每次多 2 次间接但现代 CPU 的 cache 可能让差距很小真正的成本在每条访问都多指令。除非有明确的钻石需求否则组合更省心。Qoverride/final和多重继承冲突吗A不冲突。final类不能作为基类但多重继承里可以给个别虚函数加final编译器就能对那个槽位去虚化E08 讲过。⑪ 扩展实验跑sizeof编译运行E09_mixin.cpp确认A16 B16 C32、VBase4 D116 D216 Diamond40。看 thunk给C覆盖fb()void fb() override {}g -O2 -S在汇编里找B::fb相关的 thunk带addq $16的那段。-fdump-record-layouts用 clang 看每个类的字段偏移表和本集 ASCII 图对照。组合对照把Diamond改成组合两个非虚成员 共享 VBase 成员sizeof对比哪个更小、访问哪个更快。跨基类转换C*转B*和转A*反汇编看addq偏移是否不同A 是 0B 是 16。⑬ 扩展专题三多重继承下虚表不止一张的完整图景本集说C有两个 vptr、两张 vtable但更准确地说一个多态类有几条多态路径就有几个 vtable。以C : A, B为例一张primary vtable主表对应 A 子对象从对象开头被 vptrA 指向——含 A 的虚函数 C 新增的fc一张secondary vtable次表对应 B 子对象从偏移 16 被 vptrB 指向——含 B 的虚函数。两条路径的虚表槽位各自独立。如果 C 覆盖了 B 的fb那么vptrB 指向的表里fb槽位存的是thunk先把 this 16再跳C::fbvptrA 指向的表里fb不在其中那是 A 路径的表。这个一对象多表 thunk 串场的设计让通过任何基类指针调用虚函数都正确代价就是内存里有 N 张表、跨路径调用多一层指针调整。⑭ 扩展专题四纯抽象接口的最佳实践与成本模型接口多继承mixin是多重继承最常见的合法用途structDrawable{virtualvoiddraw()const0;virtual~Drawable()default;};structClickable{virtualvoidonClick()0;virtual~Clickable()default;};structButton:Drawable,Clickable{...};纯接口无数据成员所以每个接口贡献 8 字节 vptr两个接口 对象开头两个 vptr若所有接口都是纯虚无成员、无实现跨接口虚调用仍要 thunk 调整Button*→Clickable*子对象析构函数必须是虚的否则通过Drawable*delete 会泄漏Button的部分E07 讲过。成本模型每多一个多态基类 ≈ 8 字节 vptr 跨基类虚调用 1 次指针调整。对真需要多接口的设计这是合理代价对只想要一个接口但图省事多继承就是浪费。⑮ 扩展 FAQ第二轮Qstd::is_polymorphic_vC和std::is_multipleAis_polymorphic只问有没有虚函数有即 true不关心继承形态。多重继承没有专门的 trait用sizeof与alignof判断布局即可。Q虚继承和虚函数在表上如何共存A一个类的 vtable 里既有虚函数槽位也可能有虚基类偏移条目vbase offset。它们住在同一张表的不同区域这正是查表访问虚基类的入口。QDiamond里访问v有几种路径A从Diamond*直接访问是最派生类视角偏移固定或查主表从D1*/D2*访问则是从虚基类子对象视角查各自的偏移表。所以同一个v不同视角的成本不同。Q为什么 C 里继承两个带实现的类是坏味道A除了布局成本还有语义混乱两份数据、二义性、菱形。实践中实现复用用组合/继承单类接口抽象用多接口继承。C 允许多重继承但工程上慎用本集给你的是为什么的机器理由。Qfinal类能多重继承吗Afinal限制的是它作为基类它自己仍然可以继承多个基类。final的作用仍是给编译器不会有更派生类的信息利于去虚化。Q本集所有数字都是Windows 下的 g吗A是的基线是 g 15.2.0 x86-64 Itanium C ABI。Windows 的 MSVC 用的不是 Itanium ABI布局/虚表细节可能不同但多基类多 vptr、跨基类要调指针、虚继承要查偏移表这些原则在所有主流 ABI 下成立。E23 会专门做 Windows vs Linux 的 ABI 对照。⑯ 扩展实验第二轮thunk 深度观察覆盖 B 的fb后g -O2 -S在C::fb附近找addq $16, %rdi; jmp C::fb形态的 thunk确认串场跳转。三接口对比继承 1/2/3 个纯接口sizeof分别是 8/16/24验证每接口 8 vptr。虚基类访问反汇编Diamond*访问vvsD1*访问v对比汇编里查表间接vs直接偏移的差别。组合替代实验把接口多继承改造成成员对象 转发函数sizeof和调用开销对比体会继承 vs 组合的取舍。⑱ 扩展专题五一个会变的偏移——为什么虚基类在尾部你可能已经注意到Diamond布局里vVBase 的成员被放到了对象尾部。这不是偶然而是 Itanium ABI 的明确约定虚基类子对象排在所有普通基类和成员之后且多个虚基类按先继承的先排、越虚越靠后排序。为什么这么设计因为虚基类子对象的位置不能影响普通部分的布局如果 VBase 放在 D1 前面那么 D1 里的d1偏移会被虚基类是否真的出现、有多大影响把它甩到尾部普通成员d1、d2、d的偏移就固定了与是否有其他类也虚继承 VBase无关代价是普通继承里编译期算偏移变成运行时查表定位本集 ⑤ 已展开。这个固定普通部分、偏移虚部分的取舍是 ABI 在布局稳定与共享子对象之间找的平衡点。理解它你就理解了为什么虚继承必然比普通继承多一次间接——位置不固定就必须运行时问在哪。⑲ 扩展专题六从 E09 到 E10 的桥——RTTI 如何利用 vptr本集讲完多 vptr 布局正好回答 E10 的一个细节typeid怎么知道对象的真实类型每个多态对象的 vptr 指向它所属类的 vtablevtable 的前面vptr 指向位置再往前偏移 8 字节存着该类的type_info指针所以typeid(*b)的机器实现是b→ vptr → 往前 8 字节 →type_info再和typeid(Der)的type_info比较。这就是为什么 E10 里你会看到movq -8(%rax), %rcx这条指令——对象头一个 vptr既通向函数表往后查也通向类型信息往前查。多重继承下vptrB 往前 8 字节拿到的则是B 视角的类型信息而dynamic_cast更复杂要靠 vtable 里的更多信息沿继承链走E10 会给出__dynamic_cast的运行时调用。⑳ 扩展 FAQ第三轮Q多重继承 虚继承一起用布局会怎样A会同时出现多个普通子对象多 vptr 虚基类子对象在尾部 偏移表。布局更复杂sizeof通常更大。工程上这类组合很少见出现就该反思设计。Q为什么说多重继承是 C 的黑魔法A因为它的正确性依赖 ABI 的布局规则thunk、vbase offset、RTTI 表而这些规则对使用者是隐藏代码。用得好是 mixin用得乱就是 UB 温床。Q-fdump-record-layouts需要 clangg 怎么办Ag 可用-fdump-lang-class较老或-fdump-class-layout。不同版本选项名略有差异g --helpcommon | grep dump可查。看汇编也能还原布局vptr 写入偏移、成员构造偏移。Q对象里 vptr 的顺序能自定义吗A不能。Itanium ABI 规定vptr 在对象开头每个多态子对象的开头布局顺序由继承顺序决定。标准不管这些但 ABI 管——换 ABI 布局就变。Q多重继承和按位可拷贝冲突吗A带虚函数的类本来就不是 trivially copyable 意义下的可 memcpy 类型。多重继承只是让它更明显——两个 vptr 一起拷若类型不符就是 UB。㉑ 扩展实验第三轮三基类struct X : A, B, D1 { };sizeof(X)与布局对比验证每多一个普通基类子对象 多 16 字节vptr 成员。虚继承 多态给 VBase 加虚函数Diamond的sizeof再涨反汇编看虚基类偏移表如何与 vtable 共存。RTTI 联动对C对象执行typeid反汇编确认从 vptr 往前 8 字节拿 type_info。组合 vs 继承性能分别用组合共享成员和虚继承共享成员实现同功能-O2后对比访问共享成员的汇编条数。ABI 换平台的差异同一cpp用-m32若支持或换 clang 交叉目标编译sizeof是否变化——体会A 布局是 ABI 的产物。㉒ 悬念编译器到底怎么知道一个对象的真实类型好让dynamic_cast、typeid工作虚表里还藏着一个指向类型信息的指针。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。