深入 C 底层的 RTTI 与多态这事我琢磨了挺久。很多 C 开发者用虚函数、dynamic_cast 用得飞起但一旦问起“底层到底是怎么找到正确函数地址的”、“RTTI 到底存了什么”多半会卡壳。说实话C 的抽象机制一向是“凡是用到的地方都给你封得严严实实”但真要搞懂性能瓶颈、踩过类型转换的坑、或者自己手写一套反射系统那就绕不开这些底层的组织了。我个人一直觉得虚函数表和 RTTI运行时类型识别是 C 最迷人的部分之一——因为它把“面向对象”从语法层面真正落到了内存和指令层面理解了这里你才算真正搞懂多态。开篇先把核心价值说清楚这篇博文主要聊聊 vtable虚函数表、vptr虚函数表指针、type_info、typeid、dynamic_cast 这几位“主角”在底层是怎么配合的同时会扒一下它们在不同场景下的实现差别和性能开销。不管你是 C 新手想搞懂多态的底层逻辑还是老手想彻底搞清楚 RTTI 细节、优化自己的对象模型这篇文章都会有你想看的硬核内容。1. 先说清楚 RTTI 到底管哪几件事1.1 从“运行时类型”这个需求出发我们平时说 RTTI指的是 Run-Time Type Information注意它和编译期的静态类型statically known type是相对的。多态的核心就是“同一段调用代码在不同的实际对象类型上有不同的行为”这就要求程序在运行时能拿到一些关于真实类型的线索。这里很多人其实混淆了一个很常见的记忆点虚函数本身就是 RTTI 的一个应用场景。你在基类调用virtual std::string name() const底下干掉一个派生类对象编译器在编虚函数调用时不可能知道实际类型的静态信息所以只能走“查表”也就是虚函数表。而完整意义上的 RTTI 还包含另外两个非常出名的运行时操作typeid返回一个std::type_info对象可以用来比较类型是否完全相同或者拿到类型的名字。dynamic_cast从某个基类引用/指针安全地转换到派生类引用/指针如果转换关系不成立就返回空指针指针版或抛std::bad_cast引用版。注意C 标准对 RTTI 只规定了一些 API 和存储布局的“存在性”但 vtable 应该怎样排布、typeinfo 具体放哪个区段这些全由编译器自定义。不同编译器、不同优化级别下布局可能都不一样这就是为什么需要深入底层看清本质。1.2 RTTI 的数据是从哪来的RTTI 不是运行时凭空生成的它实际上是一种“只读元数据”是编译器在编译期间把每个类、每个虚函数相关信息写进最终产物里。这意味着类的所有类型信息在编译期间就固定下来运行时只需要拿到这个类对象的“类型标识”指针然后通过查表形式访问RTTI 数据通常存放在只读数据段.rodata或者链接时生成的有关段里不是在堆上动态分配。这是理解 RTTI 很重要的一步它不是魔法每个类型对应一段静态数据。程序里可能有很多同样的类型但它们的 type_info 都指向同一份数据因此typeid(*objA) typeid(*objB)实际上是“两个指针相等”的比较代价极低。2. 虚表与虚指针多态的第一块基石2.1 一个简单虚继承类在内存里长什么样我们来看一个最普通的情况class Animal { public: virtual ~Animal(); virtual void speak() const { } protected: int age_; }; class Dog : public Animal { public: void speak() const override { } private: int weight_; };假设 64 位平台每个对象最开始就放一个指向 vtable 起始地址的指针也就是 vptr通常占用 8 字节。所以Animal的布局大体是offset 0: vptroffset 8: age_对齐后整体大小一般是 16 字节若开启虚继承会更复杂Dog 则额外再拼上 weight_offset 0: vptr指向 Dog 自己的虚表offset 8: age_offset 12: weight_整体大小一般是 16 字节这里有个需要特别留意的点Dog 对象里存储的 vptr指向的并不是 Animal 的虚表而是 Dog 自己的虚表。虚表里关于 Animal 的函数槽位在 Dog 虚表里存的内容是经过适配后的 Dog 对应函数地址。构造对象时编译器会在构造函数里给 vptr 赋值而析构函数则会把它恢复到当前类对应的虚表这也就是为什么虚调用在构造析构期间有特殊表现的原因。2.2 虚函数调用的寻址过程现在看一条虚函数调用Animal* a new Dog(); a-speak();编译器实际上做的是这个逻辑从a指向的内存地址中取前 8 字节得到vptr *(void**)a在虚表中查找speak()的槽位这个槽位是一个固定的偏移量在 GCC/Clang 上speak 通常位于vptr 0或近似偏移具体取决于有无析构函数等前置槽位把vptr加上偏移读出一个函数指针然后跳转执行。这就是多态的核心一个间接寻址再加一次间接调用。相比普通直接调用多了一次内存访问和跳转所以虚函数调用比普通成员函数稍慢在极端性能敏感且内联不上的场景里会有明显差距。现在问题来了dynamic_cast和typeid用的虚表信息又在哪里其实vptr不仅仅指向一串函数指针在大部分主流 ABI 里vtable 的前边有一个“偏移到顶部”和“RTTI 指针”字段。这跟很多人记忆里“vtable 就是函数指针数组”有差别这也是底层真相里的重要部分。X86-64 上比较常见的 Itanium C ABI 里vtable 通常这样摆偏移 -24offset-to-top用于从 vptr 恢复到对象地址多重继承/虚继承关键信息偏移 -16typeinfo pointer用于 RTTI偏移 -8虚基类偏移或其它附加信息若存在则不同偏移 0 及之后虚函数指针槽位顺序由声明顺序、覆盖关系等决定这个布局直接解释了为什么typeid能在 O(1) 时间内完成进入对象首址 - 取 vptr - 在 vptr 的固定负偏移处取得std::type_info*然后就拿到了类型信息。3. typeid 与 dynamic_cast 的底层工作流程3.1 typeid 为什么不慢typeid的实现其实非常简单核心逻辑就是“从对象的 vptr 中读取类型信息指针然后返回引用”。在绝大多数主流编译器里typeid 会被优化成类似这样的操作const std::type_info typeid_func(const void* obj) { void* vptr *reinterpret_castvoid* const*(obj); const std::type_info* info *(reinterpret_caststd::type_info* const*(vptr) - 2); return *info; }注意这里的“-2”对应 vtable 中 typeinfo 的固定负偏移。因为 vptr 指向的是第一个虚函数槽也就是函数指针数组的起始所以要往回跳过几个字段。在实际代码里typeid的汇编通常就这么几行mov rax, [obj] ; 取 vptr mov rax, [rax - 16] ; 取 typeinfo 指针这也是typeid几乎不产生额外运行时开销的原因但如果对象是不含虚函数的普通类型编译器就会将它退化成一个编译期已知的常量可能直接比较类型名字字符串或使用一个全局唯一的 typeinfo 对象地址。对于含虚函数的对象即使优化级别为零typeid也不会变成字符串比较尤其不会做慢速的类型名字字符串扫描。这在面试里经常被问起以前有人误认为typeid是字符串比较实现的这是个常见误区。3.2 dynamic_cast 的寻人启事dynamic_cast 就没 typeid 那么便宜了。它的主要场景是从基类向派生类转换或者跨继承层级转换。标准只要求它能够回答“目标类型和当前类型是否有合法的静态转换路径”但这个“静态转换路径”到了底层得靠一种类似“遍历类层级关系图”的算法来推断。底层常见做法是从对象的 vptr 里拿到的 typeinfo 指针出发通过一个称为__si_class_type_info或__vmi_class_type_info的结构体来描述这个类的继承关系。具体来说编译器会把类的继承体系描述成一张图单一继承每个类用__si_class_type_info描述里面有一个 base 指针指向直接基类。多继承使用__vmi_class_type_info描述里面是一个基类描述数组每一项包含基类偏移、访问控制标志、虚基类标志等。还有完全独立的__class_type_info用于像typeid查询相等这样不需要层级遍历的操作。当调用 dynamic_cast 时库会从当前对象实际类型的 typeinfo 出发沿着这张继承图走一层层判断是否存在“是从该基类派生的”或“可以向上转换成目标类型”的关系。这个过程虽然高效但数量级上是指数级遍历树/图的搜索而不是 O(1)所以性能比 typeid 差不少。这里有个非常实用的认知在继承层级浅、类图结构简单的场景dynamic_cast 就很快一旦出现大量多继承、跨模块、深层次继承dynamic_cast 的成本就会直线上升就可能应认真考虑换一种设计。那么为什么 dynamic_cast 会用到对象的 vptr 而不是直接比较 typeinfo因为对象本身的类型信息里虽然带着指针但你还需要结合 “指向的究竟是哪个子对象” 来算偏移。多重继承下一个派生类对象可以拥有多个 vptr每个子对象都指向自己对应的虚表部分dynamic_cast 必须根据入口子对象的 vptr重新恢复出“完整对象”的地址然后再在类图上搜索。这个“恢复完整对象地址”的动作靠的就是 vtable 前部那offset-to-top字段。简单描述就是取当前对象的 vptr从 vptr 的-24位置读出 offset-to-top用 “当前对象地址 - offset-to-top” 得到最完整对象的地址再从最完整对象的类型信息出发沿继承图查找目标类型与类之间的关系如果找到对应子对象就根据 vbase offset/static offset 计算出目标子对象地址并返回。这也是为什么 dynamic_cast 在“多重继承 虚继承”下特别容易出 bug 的原因任何一步偏移算错都可能导致 undefined behavior而且这些崩溃现场经常无法直接复现。3.3 typeid 和 dynamic_cast 在“无多态类型”上的行为差异标准规定如果 typeid 作用于一个无虚函数的普通对象那它返回的是编译期类型的 type_info与运行时实态无关。如果 dynamic_cast 作用于一个无虚函数的类型编译器会直接报错‘dynamic_cast’ with type operand that does not contain any polymorphism.这个差异不只是一个语法限制背后是 RTTI 的实现依赖问题没有 vptr就没有地方挂 typeinfo没有 typeinfodynamic_cast 就无法确定当前对象的精确类型和继承图关系。所以dynamic_cast只能用于带至少一个虚函数的类型这不是标准拍脑袋而是底层实现的要求。4. 多重继承和虚继承时的 vtable 与内存布局解析4.1 一个带有两个基类的陷阱来看多重继承的典型场景class Base1 { public: virtual void f1(); int a; }; class Base2 { public: virtual void f2(); int b; }; class Derived : public Base1, public Base2 { public: void f1() override; void f2() override; int c; };这种情况下Derived对象是这样布局的offset 0: Base1 subobject含它自己的 vptroffset 8: aoffset 16: Base2 subobject含它自己的 vptroffset 24: boffset 32: c总大小 40 字节考虑对齐注意Derived 对象中有两个 vptr一个是 Base1 的 vptr指向一张覆盖后的虚表部分另一个是 Base2 的 vptr指向另一张虚表部分。这两张虚表并不是各自独立的完整表而是同一派生类虚表的一部分通过偏移关联起来所以你可以将 Derived 的虚表视为“组合虚表”。当你写下Derived d; Base2* b2 d; // 这里 b2 的地址被编译器调整成 d 的地址 16这就是一个关键事实b2并不等于d指针本身带有偏移指向的是 Derived 对象内部的 Base2 子对象起始位置。调用虚函数时编译器知道该从哪个 vptr 查找因此没问题。但如果你把一个Base2*强转到Derived*而又不做任何调整得到的指针就是坏的访问c可能直接越界或者读到错误数据。这也是多重继承下极其经典的踩坑点。4.2 vtt 结构到底是干什么的多重继承真正复杂的地方不在普通成员变量而在构造/析构时的 vptr 赋值和虚函数调用的修正。比如当你在 Derived 构造函数里调用Base2::f2()时编译器需要保证当前 vptr 指向一张“既能反映 Base2 虚函数表、又能正确回调到 Derived 虚函数覆盖”的虚表。标准里没有明确说明但 Itanium ABI 就引入了一个附加结构vttconstruction vtable table。vtt 可以理解成一堆虚表指针的集合专门用于构造函数、析构函数和基类构造期间正确切换 vptr。每个“构造子对象”在构造过程的不同阶段vptr 会先指向“基类专用的构造虚表”然后再切换回“最终派生类的虚表”。这是为了确保在构造基类子对象期间虚调用会被正确路由到当前正在构造的类而不是提前调用最派生类的虚函数——后者的行为是未定义的因为该对象还没有完整构造。举个例子Derived 继承 Base1 和 Base2那么在进入 Base1 构造函数时对象的 vptr 指向 Base1 的构造虚表进入 Base2 构造函数时指向 Base2 的构造虚表等进入 Derived 构造函数体之前再被切换为 Derived 最终虚表。这套切换逻辑就是由编译器生成的代码配合 vtt 里存储的表地址完成的。vtt 是底层细节中的细节但如果你真的去调试一个多重继承对象的构造函数汇编会发现每种编译器生成的代码里都掺和了这些表。弄懂 vtt 后很多“构造函数里虚调用为何不是预期结果”的诡异问题就不攻自破了。4.3 虚继承在 vtable 里的额外玄机虚继承virtual inheritance是另一个容易出乱子的地方。以菱形继承为例class A { virtual void fa(); }; class B : virtual public A { virtual void fb(); }; class C : virtual public A { virtual void fc(); }; class D : public B, public C { virtual void fd(); };这种情况下D 中只有一个 A 子对象它被所有“虚路径”共享但D 的内存布局里不再有“A 在固定偏移处”这种保证。A 子对象的偏移需要通过虚基类表vbase offset或 vtable 里的偏移量计算得到运行时偏移。为了支持这种动态计算vtable 中在某个固定位置存放了vbase offset。当访问 D 的 A 子对象时编译器会通过 D 的某个 vptr 查找偏移然后加上得到 A 的地址。这也是为什么虚继承对象要比普通多继承更大、更慢的原因多了一次查表偏移的动作。同样地在发生dynamic_cast时如果目标类型位于虚基类路径上甚至需要更复杂的搜索逻辑遍历 DAG 图中的所有路径来确认是否存在合法转换。这一点在实际中常被人忽略导致在菱形继承下误以为dynamic_cast一定安全一旦不成立就直接 UB。标准其实已经保证了相关行为但前提是确保转换路径存在且完整。5. 常见 RTTI 性能杀手与优化思路5.1 什么时候 dynamic_cast 会变成性能黑洞dynamic_cast 的开销在普遍情况下不只是一个函数调用它可能需要获取当前对象 vptr从 vptr 计算完整对象地址取 typeinfo遍历继承图对目标类型与当前类型的路径进行匹配和偏移计算。如果这个“图”很大遍历耗时就会显著上升。尤其是跨动态库边界时还可能触发类型信息和虚表信息的重复或引用折叠的差异问题甚至导致 dynamic_cast 失败。这在插件式架构、跨模块反射系统里是特别危险的雷区。5.2 如何手写一个轻量 RTTI 等价物不少人会问如果我特别在意性能不用 RTTI 行不行当然可以。你可以自己做一套很轻量的“类型标签”系统思路是给每个类定义一个static constexpr uint32_t class_id_用一个全局唯一整数表示类型在基类加一个虚函数virtual uint32_t class_id() const { return class_id_; }想要类型检查时就if (obj-class_id() Derived::class_id_)想安全转换时可以先检查 id再用static_cast搞定。这种方案的性能接近直接比较一个整数比 dynamic_cast 快得多但代价是你必须手动维护继承体系中的类型编号一致性保证同一条继承链上类型 ID 的唯一性。它也做不到完整的“动态类型层次遍历”比如“不知道完整类型时判断能否转换成某个中间基类”这种场景下还是得靠真 RTTI。5.3 编译器开关RTTI 能关掉吗有些编译器、有些工程为了减小二进制体积或调性能会关闭 RTTI。关闭后编译期不生成 typeinfovtable 没有对应的 RTTI 字段或变短了代码里任何 typeid、dynamic_cast 的调用都会直接编译报错。这样会导致异常处理里的 catch 匹配、协变返回类型、partial 库实现等行为也受到影响。而多态带虚函数的调用本身时可以不受影响的vtable 里只要不放入 typeinfo 即可。所以如果你确定不用 dynamic_cast/typeid关闭 RTTI 是可行的但如果你依赖 C 异常机制关闭 RTTI 可能会导致 catch 崩溃因为在某些 ABI 下会依赖 typeinfo 的地址匹配。这类开关我一般建议不到万不得已别关。现代编译器对 RTTI 开销已经优化得足够好并且 dynamic_cast 不是性能瓶颈的主角。6. 常见动态类型问题与实操排查技巧6.1 dynamic_cast 返回 null 有哪些隐藏原因有人会写出一种特别隐蔽的错误场景class Base { public: virtual void f() {} }; class DerivedA : public Base {}; class DerivedB : public Base {}; void check(Base* b) { auto* d dynamic_castDerivedA*(b); if (d) { ... } }表面看起来没什么问题但当 b 指向 DerivedB 时d 为 null 是合理的。可有时候 b 明明就是指向正确的类型却得到 null这时候常见原因包括跨 DLL/SO 边界每个模块可能各自生成了同一类对应的 typeinfo两边的 vptr 信息不能严格等价未定义虚函数表的可见性在隐藏 visibility 的类中符号可能未导出导致动态链接时运行时能看到的类型信息和编译期不一致部分编译单元开启 RTTI、部分关闭编译器会生成长度不同的 vtable而程序照旧调用结果不仅是 dynamic_cast 失败整个内存布局都已经不对了。这种问题常见于大型插件项目。直接的对策是尽量保证同一个多态类及其 vtable、typeinfo 都由同一个模块导出并保证所有模块的 RTTI 选项一致或者在各模块边界使用接口类隔离。6.2 调试技巧直接看 vtable 前两槽如果你真想看看对象的 RTTI 信息在 GDB 里可以直接set $vptr *(void**)obj info symbol *(void**)($vptr - 16)通常能看到typeinfo for ClassName这样的符号。这个操作能很快确认某个对象实际类型是什么也能验证跨模块时的 typeinfo 是否一致。在 Release 构建中如果编译器把虚函数调用改写成了直接调用比如最终类型已确定且无覆盖可能那 vptr 可能不会被访问但是只要对象是多态的vptr 一定在对象里。如果在 gdb 中读取失败重点检查指针偏移和对齐。6.3 小心虚析构函数与 RTTI 的关系很多人会忽略这一点一个类如果带有虚析构函数它也是“多态类”会有 vptr。但实际上虚析构函数在 vtable 里也占用一个槽位而且它在 Itanium ABI 里往往有两个入口完整对象析构和删除析构/deleting destructor。如果你手写模拟虚表记得把析构相关的槽位考虑进去否则你自定义的虚表很容易跑飞。同时dynamic_cast是否安全与虚析构无关关键是有没有虚函数即使析构不是虚的只要其它虚函数存在动态类型转换就可用。6.4 常见性能与正确性速查表为了方便日常写代码时对照我整理了一个简单的表格操作开销量级适合场景主要风险虚函数调用低一次间接跳转分支预测多态分发分支预测失败/无法内联typeid极低一次取址比较精确类型判断跨模块 typeinfo 不等价dynamic_cast单继承低-中类层次遍历类图简单时的安全向下转换类图变大、分支复杂dynamic_cast多继承/虚继承中-高复杂层级下安全转换遍历开销跨模块问题自己实现的 class_id 对比极低整数比较性能敏感、类型树可控无法处理完整继承网络看到这个表“最优解”往往不是“完全不用 RTTI”而是“分清使用场景精准选择恰当的工具”。7. 这些底层知识到底在哪几类项目里最有用掌握这部分不只是为了面试装点门面在实际工程里它至少能在四种场景下直接产生价值。第一类是通用组件库/基础库开发者。你设计的事件系统、插件系统、序列化系统只要涉及到跨模块传递多态对象就一定会和 RTTI 打交道理解了 vtable 布局后能预判某些类型信息在哪个模块被剥离从而提前规避。第二类是游戏引擎/编辑器工具链开发。这类项目对对象类型识别和属性反射的需求特别高往往需要逃离编译器的 RTTI 去实现自定义反射。你想把类的字段名、字段偏移、类型哈希管理起来就必须先正确理解系统 RTTI 是怎么设计的否则手写的那套很容易跟编译器的动态类型信息打架。第三类是编译器/解释器/静态分析相关工具开发。一旦你要解析 C 源码、模拟 ABI、生成代码或分析虚函数调用RTTI 与 vtable 的底层实现就是绕不开的核心知识点。你可能得自己重建多态模型这时理解越深越不容易跑偏。第四类是高频交易、嵌入式或实时系统这类性能敏感环境。这些项目常常为了性能而考虑关闭 RTTI、禁用异常、手动控制虚函数调用但关闭之前必须搞清楚类型安全、多态安全边界在哪里。盲目关闭 RTTI 一旦引发未定义行为代价不是一点半点的。大多数普通业务代码其实用不到这些细节但一旦遇到极端问题比如“为什么跨 so 后 dynamic_cast 失效”、“为什么虚继承对象大小比你预期大”、“为什么 O2 后 typeid 还变快了”这类没有底层认知排查起来真的会抓瞎。8. 额外聊聊实现差异不同平台上的 RTTI 表现8.1 gcc/clang 所用的 Itanium ABI 布局实际上大部分类 Unix 平台Linux、macOS、某些嵌入式 BSD采用的 Itanium C ABI在 RTTI 上有统一的格式约定每个多态类对应的 typeinfo 是固定的__class_type_info派生体系typeinfo 内部会记录类名、作用域以及继承信息typeinfo 通过 vtable 负偏移被对象引用。这个统一格式也方便了跨编译器调用只要两边都是 Itanium ABI动态库接口里传递的多态对象在很多情况下是兼容的。具体 typeinfo 对象会有类名字符串指针、状态标志、基类描述、虚函数表描述等。对不同继承关系编译器会选用不同的 typeinfo 子类__si_class_type_info单一继承__vmi_class_type_info多继承/虚继承__class_type_info本身是根类没有任何基类信息。typeinfo 中基类数组还会带访问控制标志public/protected/private、虚基类标志。这些信息和 vtable 里的 offset 一起提供完整转换路径。8.2 MSVC 的 RTTI 布局又不一样Windows 上 MSVC 的 RTTI 使用_RTTICompleteObjectLocator、_RTTITypeDescriptor和_s_RTTIBaseClassDescriptor等结构。它同样挂在虚函数表前边结构与 Itanium ABI 差异很大。这里提这个是想说明一个实用结论如果你做跨平台开发千万不要手写依赖“某种固定 vtable 偏移”的二进制分析代码更不要自己解析对象布局。直接使用编译器提供的 RTTI 接口才是可移植的路径。只有在固定平台、固定编译器的封闭场景下尝试直接读取 vptr 和 typeinfo 才是可控的。9. 我踩过的 RTTI 相关的坑与总结心得文章写到这分享一点我在实际开发里踩过的坑。第一次在项目里大面积用 dynamic_cast 是在一个图形引擎消息系统里。所有 UI 事件对象都继承自一个 Event 基类然后我写了个事件分发器收到消息后逐个 dynamic_cast 判断是否为某类事件。功能一切正常但后来 Profile 一看消息密集帧里 dynamic_cast 居然占了不小比例原因就是继承层次深、每个事件类型又带有若干接口基类最终遍历路径比想象的长。后来我把“事件类型 ID”这个字段提升为所有事件基类的虚函数用整数比较替代 dynamic_cast 链性能立刻改善了一个档次。另一个坑是跨动态库的 RTTI。当时某个第三方插件库和主程序各编译了一次头文件里的同名多态类结果主程序 dynamic_cast 一个插件传出的对象时总是失败。查了一圈终于发现两边由于可见性和 RTTI 开关不一样typeinfo 地址不一致vtable 也不等价。最后解决办法是将这些共享类型统一抽到一个公共头文件并让插件库不再导出具名符号而是通过接口工厂回传。如果你只是写业务逻辑用 dynamic_cast 完全没问题性能影响微乎其微但如果你在做性能敏感的引擎层、库边界、反射机制请一定记住能用静态多态CRTP、模板或普通多态解决的问题不要硬上 dynamic_cast即使真用 dynamic_cast尽量保持继承层次扁平避免菱形虚继承所有编译单元都要保持相同的 RTTI 开关尽量让多态类型符号在同一个模块可见类型判断优先用 typeid 或自定义类型 ID只有需要“跨层级安全转换”时才使用 dynamic_cast在对象构造/析构期间最好避免进行可能触达 Runtime Type Identification 的操作因为此时 vptr 可能尚未指向最终类型如果你要手动实现反射/RTTI底层布局与编译器 ABI 不一定兼容最好通过虚函数接口封装而不是直接操作 vptr。最后再补一句实操层面的体会当年我花了挺长时间研究 C 的 vtable 与 RTTI 的汇编代码很多网上资料讲得零零散散有的很久没更新了。后来我把每个平台、每个编译器生成的虚表、typeinfo 符号打印出来对着看才彻底弄明白这些抽象机制背后的真实组织方式。只要你能熟练使用gdb去查看 vptr 和 typeinfo配合readelf或objdump检查相关符号段那对 C 对象模型的理解基本就超过市面上大多数开发者了。真希望每个 C 学习者都能亲手做一次这样的实验那收获比读十篇博客都大。
阅读完成 · 觉得有帮助?