【C++进阶系列 (七)】C++ RTTI 深度剖析:从 typeid 到 dynamic_cast 的底层之旅

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :YYYing.

⭐️C++编程系列专栏:C++编程系列

系列上期内容:【C++进阶系列 (六)】C++ 内存对象模型:从内存块到多态的底层之旅

系列下期内容:【C++进阶系列 (八)】C++ 多态实现原理:一次讲透 vptr、vtable 与动态绑定


目录

[1. 为什么需要 RTTI?](#1. 为什么需要 RTTI?)

[2. 核心数据结构:std::type_info](#2. 核心数据结构:std::type_info)

[2.1 type_info 长什么样?](#2.1 type_info 长什么样?)

[2.2 type_info 对象什么时候创建?](#2.2 type_info 对象什么时候创建?)

[2.3 type_info 存在哪里?](#2.3 type_info 存在哪里?)

[3. vtable 里的秘密:type_info* 藏在哪里?](#3. vtable 里的秘密:type_info* 藏在哪里?)

[3.1 Itanium ABI 的 vtable 布局](#3.1 Itanium ABI 的 vtable 布局)

[3.2 为什么放在 vtable 里?](#3.2 为什么放在 vtable 里?)

[3.3 MSVC 的差异](#3.3 MSVC 的差异)

[4. typeid 的完整剖析](#4. typeid 的完整剖析)

[4.1 两种行为模式](#4.1 两种行为模式)

[4.2 typeid 的底层实现](#4.2 typeid 的底层实现)

[4.3 空指针的陷阱](#4.3 空指针的陷阱)

[4.4 type_info 的比较](#4.4 type_info 的比较)

[5. dynamic_cast 的完整剖析](#5. dynamic_cast 的完整剖析)

[5.1 它到底在做什么?](#5.1 它到底在做什么?)

[5.2 向下转型(Downcast)](#5.2 向下转型(Downcast))

[5.3 横向转型(Crosscast)](#5.3 横向转型(Crosscast))

[5.4 type_info 里到底存了什么?](#5.4 type_info 里到底存了什么?)

[5.5 失败与异常](#5.5 失败与异常)

[5.6 为什么 dynamic_cast 慢?](#5.6 为什么 dynamic_cast 慢?)

[6. RTTI 的成本与禁用](#6. RTTI 的成本与禁用)

[6.1 内存成本](#6.1 内存成本)

[6.2 性能成本](#6.2 性能成本)

[6.3 -fno-rtti 禁用](#6.3 -fno-rtti 禁用)

[7. 宏观总结](#7. 宏观总结)

[8. 面试回答模板(直接可用)](#8. 面试回答模板(直接可用))

结语

---⭐️封面自取⭐️---



1. 为什么需要 RTTI?

C++ 的多态是静态类型系统 + 运行时派发 的混合体。你写 Base* p = new Derived; 时,编译期只知道 p 的静态类型是 Base*,不知道它实际指向 Derived。虚函数解决了"调用哪个函数"的问题,但没解决"这个对象到底是什么类型"的问题。

typeiddynamic_cast 就是为这个问题准备的。它们让程序在运行时能"问"一个对象:"你究竟是谁?"

生动比喻 :虚函数像是物业的"标准服务流程",你按门铃(调用虚函数),物业按你的房号(vtable)派人。RTTI 则是你直接问物业:"这栋楼里现在住的是谁?"物业翻出登记簿(type_info),告诉你真实住户。


2. 核心数据结构:std::type_info

2.1 type_info 长什么样?

std::type_info 是标准库定义的一个类,核心内容极其简单:

cpp 复制代码
class type_info {
public:
    virtual ~type_info();
    const char* name() const noexcept;
    bool operator==(const type_info&) const noexcept;
    bool operator!=(const type_info&) const noexcept;
    bool before(const type_info&) const noexcept;
    size_t hash_code() const noexcept;
    // 拷贝和赋值被禁用
    type_info(const type_info&) = delete;
    type_info& operator=(const type_info&) = delete;
protected:
    const char* __name;  // 类型名称(mangled)
};

__name 存的是修饰后的类型名 (mangled name),比如 Derived 可能存成 7Derivedname() 返回的就是这个字符串。

2.2 type_info 对象什么时候创建?

编译期 。编译器在编译时就在静态存储区为每个多态类生成了一个 type_info 对象,并初始化好 __name 等内容。它不是运行时 new 出来的。

2.3 type_info 存在哪里?

放在只读数据段.rodata 或类似段)。每个多态类只有一份 ,所有该类的对象共享同一个 type_info 实例。


3. vtable 里的秘密:type_info* 藏在哪里?

3.1 Itanium ABI 的 vtable 布局

在 GCC/Clang 使用的 Itanium C++ ABI 下,vtable 的内存布局大致是这样:

bash 复制代码
vtable 指针指向这里 ──┐
                      ↓
        +-----------------------+
        | offset-to-top         |  (ptrdiff_t)
        +-----------------------+
        | type_info*            |  ← RTTI 指针在这里
        +-----------------------+
        | &Base::f              |  ← 虚函数地址
        +-----------------------+
        | &Base::g              |
        +-----------------------+

offset-to-top 用于 dynamic_cast<void*> 找到对象完整起始地址;type_info* 就是 RTTI 的入口。

3.2 为什么放在 vtable 里?

因为对象里只有一个 vptr。要找到 type_info,最经济的做法就是让 vptr 指向的 vtable 里包含这个指针。这样:

  • typeid(*p) → 通过 p->vptr 找到 vtable → 取 -1 槽 → 拿到 type_info*

  • dynamic_cast → 同样先拿到 type_info*,再沿继承链搜索。

3.3 MSVC 的差异

MSVC 把 type_info 指针放在 vtable 的第 0 个槽位(虚函数地址从第 1 个开始)。所以"位置"是实现定义的,但"vtable 里有 RTTI 指针"这一点是共通的。


4. typeid 的完整剖析

4.1 两种行为模式

typeid 的行为取决于操作数是不是多态类型

场景 typeid(expr) 的结果
expr 是非多态类型 返回静态类型type_info,编译期就能确定
expr 是多态类型的左值(对象或引用) 返回动态类型type_info,运行时通过 vtable 查询
expr 是多态类型的指针 返回指针的静态类型 (即 Base*),不解引用
cpp 复制代码
struct Base { virtual ~Base() {} };
struct Derived : Base {};

Derived d;
Base& ref = d;
Base* ptr = &d;

typeid(d) == typeid(Derived);    // true,动态类型
typeid(ref) == typeid(Derived);  // true,动态类型
typeid(ptr) == typeid(Base*);    // true,指针的静态类型
typeid(*ptr) == typeid(Derived); // true,解引用后动态类型

关键细节typeid 对多态类型的指针本身 不会去解引用查动态类型,它只看指针的静态类型。只有对指针解引用*ptr)或多态引用/对象才查动态类型。

4.2 typeid 的底层实现

伪代码逻辑如下:

cpp 复制代码
// typeid(obj) 大致等价于:
const std::type_info& typeid_impl(const Base& obj) {
    // obj 是多态类型,编译器插入隐藏调用
    return obj.__vptr[-1];  // 从 vtable 的 -1 槽取 type_info*
}

对于非多态类型,编译器直接返回一个编译期确定的 type_info 对象的引用,没有任何运行时开销。

4.3 空指针的陷阱

cpp 复制代码
Base* p = nullptr;
typeid(*p);  // 抛出 std::bad_typeid

对空指针解引用后做 typeid 会抛异常,因为没法从 nullptr 的"vtable"里取 type_info

4.4 type_info 的比较

type_info::operator== 在 Itanium ABI 下通常比较指针 即可(同一个类的 type_info 全局唯一)。但标准不保证这一点,跨 DSO(动态库)时可能出问题。


5. dynamic_cast 的完整剖析

5.1 它到底在做什么?

dynamic_cast 要做三件事:

  1. 确认转换是否合法:目标类型是否在对象的动态类型继承链上。

  2. 计算指针调整量:多重继承下,基类子对象和派生类对象地址不同。

  3. 返回调整后的指针或失败信号。

5.2 向下转型(Downcast)

cpp 复制代码
Base* p = new Derived;
Derived* d = dynamic_cast<Derived*>(p);  // 成功

底层步骤:

  1. p 的 vtable 拿到 type_info*(假设是 Derived 的)。

  2. 检查 Derived 是否继承自 Base(目标类型)。

  3. 如果是,计算 Derived* 相对 Base* 的偏移量,返回调整后的指针。

5.3 横向转型(Crosscast)

cpp 复制代码
struct B1 { virtual ~B1() {} };
struct B2 { virtual ~B2() {} };
struct D : B1, B2 {};

D d;
B1* p1 = &d;
B2* p2 = dynamic_cast<B2*>(p1);  // 横向转型,成功

B1*B2* 需要重新计算地址 ,因为 B2 子对象在 D 中的偏移和 B1 不同。dynamic_cast 会通过 type_info 里的继承关系信息找到正确的偏移。

5.4 type_info 里到底存了什么?

Itanium ABI 的 __class_type_info 派生结构里,__base_info 数组存储了基类的 type_info* 和偏移量信息dynamic_cast 的核心算法就是:

  1. 从源类型的 type_info 开始。

  2. 遍历基类列表,递归查找目标类型。

  3. 找到后,沿路径累加偏移量,得到最终指针。

5.5 失败与异常

cpp 复制代码
Base* p = new Base;
Derived* d = dynamic_cast<Derived*>(p);  // 返回 nullptr

Base& r = *p;
try {
    Derived& d2 = dynamic_cast<Derived&>(r);  // 抛 std::bad_cast
} catch (const std::bad_cast&) { ... }

指针版本返回 nullptr,引用版本抛 std::bad_cast

5.6 为什么 dynamic_cast 慢?

  • 线性搜索:在复杂继承层次中,需要遍历继承树。

  • 指针调整:多重继承和虚继承需要计算偏移量。

  • 无法内联:调用运行时库函数,而非编译期展开。

  • 实测数据 :比 static_cast60~200 倍


6. RTTI 的成本与禁用

6.1 内存成本

每个多态类多一份 type_info 对象,里面存着类型名字符串。大型项目中继承层次复杂,RTTI 数据可能显著膨胀。

6.2 性能成本

  • 对象大小:多态类多一个 vptr(64 位下 8 字节)。

  • dynamic_cast:运行时库调用 + 继承链搜索。

  • 虚函数:间接调用,无法内联。

6.3 -fno-rtti 禁用

编译时加 -fno-rtti,编译器不再生成 RTTI 数据。代价:

  • typeiddynamic_cast 不可用

  • 多态异常(catch 基类引用捕获派生类异常)不可用

  • 必须全程序一致,否则 ABI 不兼容。

替代方案:手动实现 TypeId 系统,每个类定义一个静态 ID,通过虚函数返回。


7. 宏观总结

一句话哲学 :RTTI 是 C++ 在"零开销抽象"原则下做的有条件妥协 ------你为多态付出了 vptr 的代价,RTTI 只是顺便利用了这块 vptr,把类型信息挂在 vtable 里。你用不用它,vtable 都已经在那里了;但一旦你用 dynamic_cast,就触发了运行时搜索的代价。


8. 面试回答模板(直接可用)

面试官问:谈谈 C++ 的 RTTI。

您好,我从四个层面回答。

第一,本质。 RTTI 是运行时类型识别,核心工具是 typeiddynamic_cast。它只对多态类型真正生效------也就是至少有一个虚函数的类。非多态类型的 typeid 在编译期就确定了。

第二,底层实现。 编译器为每个多态类生成一个 std::type_info 对象,放在只读数据段,每个类一份。这个对象的指针被塞进 vtable 的固定槽位------Itanium ABI 下是 -1 槽,MSVC 是第 0 槽。typeid 通过 vptr 找到 vtable,再取 type_info*dynamic_cast 同样先拿 type_info*,然后遍历继承链搜索目标类型。

第三,dynamic_cast 的细节。 它要确认转换合法性并计算指针偏移量。单继承下偏移简单;多重继承下需要从一个基类子对象地址调整到另一个基类子对象地址;虚继承下偏移是运行时计算的。失败时指针返回 nullptr,引用抛 bad_cast。性能上比 static_cast 慢 60~200 倍,因为它是运行时库调用加线性搜索。

第四,代价与边界。 RTTI 会增加二进制体积,dynamic_cast 有性能开销。很多项目用 -fno-rtti 禁用,但会同时禁用 dynamic_cast 和多态异常。替代方案是手动实现 TypeId 系统。我的总结是:RTTI 是 C++ 用少量内存和运行时开销换取类型安全向下转型的机制,理解它的 vtable 存储方式,就能在需要时做出正确的性能取舍。


结语

RTTI的学习暂时就到这里了,这个东西的用处可以说其实就是识别对象实际类型 的机制,从而可以基于这个信息做安全转换。 但它不只服务于转换,异常匹配、类型映射、调试日志都会用到它。代价是 vptr 和 RTTI 数据带来的内存与性能开销,所以是否启用、是否用 dynamic_cast,是一个需要权衡的工程决策。

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!


---⭐️ 封面自取 ⭐️---

相关推荐
nvvas2 小时前
Java 入门(四):数组——数据的集合管理
java·开发语言
钓鱼的肝2 小时前
csp-j-s总结(2)
c++·经验分享·笔记·算法·青少年编程
重生的黑客2 小时前
Qt 常用控件精讲(1):QWidget 核心属性全解 —— 从 geometry 到 qrc 与 QSS
c++·qt·qwidget·qss·常用控件
Hespethorn3 小时前
取 `X-Forwarded-For` 首段做频控 key,等于把限流开关交给了调用方
c++
米糕闯编程3 小时前
鱼香ros2(七)功能包组织C++节点
c++·ros2·cmakelists
Y3815326623 小时前
SERP 数据清洗实战:字段标准化、日期解析与去重键
开发语言·数据库·python·python数据库
汉克老师3 小时前
GESP2026年9月认证C++四级( 第三部分编程题(1、新汉诺塔))精讲
c++·gesp·小学生·学c++编程
君顾13 小时前
本地城市低空经济管理系统定制:城市级低空运营平台架构与落地实战
java·开发语言·飞手
多多鼠4 小时前
System Prompt 的“版本漂移”问题:从变更管理到 A/B 测试体系
开发语言·网络·人工智能·python·langchain