🔷 第十四章 RTTI与异常处理
📄 一句话概括

RTTI 是 Run-Time Type Information(运行期类型信息)的缩写 ——它让程序在"跑起来之后"还能问一句:你这个对象到底是什么类型? 编译期我们只看得见指针的静态类型(Animal *);RTTI 让我们能拿到对象的动态类型(Dog)。 它由两个工具 + 一个类组成:typeid(问类型)、dynamic_cast(安全地转换类型)、std::type_info(装类型信息的那个类)。 本章把它和"异常处理"放在同一章,是因为它们是同一个母题的两面:RTTI 是"运行期按类型做判断",异常处理是"运行期按类型做匹配" ——两者都是"编译期定不了、必须等跑起来才知道"的事。

📝 本节和前面几节的关系

RTTI 是第十一章"多态"的补集。 11.4 讲的是虚函数 ——同一句话、不同反应(你说 speak(),狗汪汪、猫喵喵)。虚函数解决的是"做事"。 但有些需求是"问身份":比如"报一下你是哪种动物"、"把所有狗挑出来去叼球"。这种"按类型分支"的需求,光靠虚函数很不顺手 ——要么在基类给每一种操作都开一个虚函数(基类被越改越脏),要么就得靠 RTTI 问一句:"你是什么?" 往回想:第十一章的虚函数(11.4.1)是 RTTI 的地基 ——只有"多态类型"的对象身上才挂着运行期类型信息;11.3 的多重继承是 14.1.3 的主角;11.2.4 的转换运算符和 7.4 的指针是 14.1.4 的底子。 往前看:14.2 的异常处理是"另一半" ——异常的匹配规则(14.2.3)本质上就是一次"运行期的类型匹配",和 dynamic_cast 是同一套逻辑。

一、先看一个"虚函数解决不了"的尴尬

先回忆 11.4 干成了什么:

Animal *p = new Dog();
p->speak();      // 输出"汪汪" —— 编译器只知道 p 是 Animal*,但跑起来它按 Dog 办事

这叫动态绑定:你下达的命令(speak)是"对谁都能下达"的,具体怎么办由对象的真实身份决定。 这是面向对象最漂亮的地方。

现在换一个需求,你立刻会卡住:

"把动物们中的狗都挑出来,让它们去叼球。"

fetch()(叼球)是 Dog 独有的,Animal 里没有。于是:

zoo[i]->fetch();               // ❌ 编译不过:Animal 里没有 fetch
((Dog *)zoo[i])->fetch();      // ⚠️ 编过了!但它可能是猫 → 未定义行为,随时崩
写法 结果
zoo[i]->fetch() 编译期就被拦下(Animal 没有这个成员)——安全但没用
((Dog *)zoo[i])->fetch() 编译期放行、运行期乱来(对猫执行狗的代码 = 未定义行为)——能用但危险

所以需要第三种东西:让程序在"跑起来之后"能自己判断一次类型,判断对了再转。

这就是 RTTI。

二、RTTI 是什么:三个零件的总称

零件 一句话 出现在哪一小节
typeid(表达式) 问一句"你是什么类型" → 返回一个 const std::type_info & 14.1.1
dynamic_cast<目标类型>(表达式) 安全地"往下/"横着"转类型;转不了就报失败(指针给 nullptr、引用抛 bad_cast) 14.1.2 / 14.1.4
std::type_info 装类型信息的小盒子;能比较(== !=)、能取名字(name()) 14.1.1

头文件是 <typeinfo>。

#include <typeinfo>

if (typeid(*zoo[i]) == typeid(Dog)) {       // 问身份
    Dog *d = dynamic_cast<Dog *>(zoo[i]);   // 安全转换
    d->fetch();
}

这两句合起来,就实现了"先问、再转、然后干" ——上一个节点的尴尬解决了。

三、RTTI 的地基:为什么"必须有多态类型"

这是本节最重要的一条硬规矩:

只有"多态类型"(polymorphic type)才能正确地做 RTTI。 所谓多态类型,就是"至少有一个虚函数"的类。

为什么?因为运行期类型信息是挂在"虚函数表"(vtable)上的:

Dog 对象
┌──────────────┐
│ vptr ────────┼──► Dog 的虚函数表
├──────────────┤    ┌────────────────┐
│ 成员数据…     │    │ speak()  ──────┼──► Dog::speak
└──────────────┘    │ 析构      ────┼──► Dog::~Dog
                    │ type_info ────┼──► 「我是 Dog」   ← RTTI 就藏在这
                    └────────────────┘

11.4.1 说"动态绑定靠指针/引用 + 虚函数",RTTI 就搭在这同一张表上 ——表里除了函数地址,还多了一格记录"这个类的身份"。

推论(一定要记住):

情况 typeid(*p) 的结果
Animal 有虚函数,p 指向 Dog 对象 Dog(动态类型,正确)
Animal 没有虚函数,p 指向子类对象 Animal(静态类型 —— 因为根本没有运行期信息可查)
p 本身就是子类指针(Dog *p = new Dog;) Dog(静态类型就已经是 Dog,没悬念)

一句话判据:类里有没有 virtual,决定 RTTI 是"看真的"还是"看单子上写的"。

顺带回收一笔账:11.4.1 强调过"基类析构必须是 virtual" ——它在这里又多加一条好处:虚析构本身就是让类成为"多态类型"的最省事手段(哪怕你没有别的虚函数)。

四、RTTI 的代价:它不是免费的

代价 具体是什么
空间 每个多态类型的 vtable 里多一个 type_info 指针(每个类一份,不是每个对象一份)
时间 dynamic_cast 要在运行期沿继承链去找,比一次静态转换慢得多;static_cast 则是零开销(编译期就算好了)
可移植性 编译器可以关掉 RTTI(-fno-rtti);也可以关掉异常(-fno-exceptions)——嵌入式里的常见做法,此时 typeid / dynamic_cast 用不了
名字 type_info::name() 的输出是"实现定义"的 ——g++/MinGW(Dev-C++ 用的就是这个)给 3Dog 这种修饰名,MSVC 给 class Dog。同一份代码换个编译器,打印的不一样。

所以有一条铁律:

判断类型永远用 typeid(a) == typeid(b) 或 dynamic_cast,永远不要拿 name() 的字符串去做比较或分支。

五、分寸:能用虚函数,就别用 RTTI

RTTI 是"合法的,但不是首选的"。 看两种写法:

// ❌ 用 RTTI 做分支:每加一种动物,这里就要改
if (typeid(*a) == typeid(Dog))        a->fetch();
else if (typeid(*a) == typeid(Cat))   a->climb();

// ✅ 用虚函数:每加一种动物,只需要新类自己实现
a->doYourJob();      // 基类里声明成 virtual,各自去实现

为什么后者更好:前者违背 9.2.3 那条判据 ——"这件事主要改谁的数据,就写在谁身上"。加一只鸟,却要回去改动物园的调度代码,这就是改动扩散。

那 RTTI 到底什么时候用? 真正合理的场景是:

  1. 框架/库要对"外来对象"做分类处理(比如调试打印、日志、序列化);
  2. 无法修改类的时候(第三方库的类里加不了虚函数);
  3. 少数"补丁式"的分支,而且你很确定它不会再扩张;
  4. dynamic_cast 做"多态接口探测" ——"如果这个对象支持 Serializable 接口,就顺手把它存一下;不支持就跳过。"(这是 dynamic_cast 最正当的用法)

一句话:RTTI 用来"问一句、顺手做点附加的事";一旦发现自己在写一长串 if (typeid…),就该回去改设计了。

六、本章地图

小节 讲什么
14.1.1 什么是 RTTI 静态类型 vs 动态类型;typeid 与 std::type_info 的用法
14.1.2 RTTI 与引用 引用上的 typeid 与 dynamic_cast;失败时抛 std::bad_cast(因为引用不能是空)
14.1.3 RTTI 与多重继承 多重继承下"一个对象有好几个地址";指针比较的坑;交叉转换(cross-cast)只有 dynamic_cast 能做
14.1.4 RTTI 转换语法 四种转换运算符的完整对照:static_cast / dynamic_cast / const_cast / reinterpret_cast
14.2 异常处理 抛(14.2.1)→ 捕(14.2.2)→ 匹配(14.2.3)→ 标准异常库(14.2.4)

七、动手 Demo:一个迷你动物园

任务:把本节开头的尴尬完整走一遍 ——用虚函数"做事",用 RTTI"问身份 + 安全转换"。

#include <iostream>
#include <typeinfo>
using namespace std;

class Animal {
public:
    virtual ~Animal() {}          // ★ 有虚函数 = 多态类型,RTTI 才有效
    virtual void speak() const { cout << "……(某种叫声)" << endl; }
};

class Dog : public Animal {
public:
    void speak() const { cout << "汪汪" << endl; }
    void fetch() const { cout << "(把球叼回来了)" << endl; }   // 狗独有
};

class Cat : public Animal {
public:
    void speak() const { cout << "喵喵" << endl; }
    void climb() const { cout << "(爬上了树)" << endl; }       // 猫独有
};

int main() {
    Animal *zoo[3];
    zoo[0] = new Dog();
    zoo[1] = new Cat();
    zoo[2] = new Dog();

    cout << "-- (1) 虚函数:按真实身份做事(11.4)--" << endl;
    for (int i = 0; i < 3; i++) {
        cout << "第 " << i << " 只:";
        zoo[i]->speak();
    }

    cout << "-- (2) RTTI 第一招:typeid 问身份 --" << endl;
    for (int i = 0; i < 3; i++)
        cout << "第 " << i << " 只的真实类型是 " << typeid(*zoo[i]).name() << endl;

    cout << "-- (3) RTTI 第二招:dynamic_cast 安全转换 --" << endl;
    for (int i = 0; i < 3; i++) {
        Dog *d = dynamic_cast<Dog *>(zoo[i]);      // 不是狗就给 nullptr
        if (d != nullptr) {
            cout << "第 " << i << " 只是狗:";
            d->fetch();
        } else {
            cout << "第 " << i << " 只不是狗,跳过" << endl;
        }
    }

    // 对比:不做检查的强转有多危险(取消注释可以自己试)
    // ((Dog *)zoo[1])->fetch();   // zoo[1] 是猫 → 未定义行为,可能崩

    for (int i = 0; i < 3; i++) delete zoo[i];
    return 0;
}

看到什么算成功:

-- (1) 虚函数:按真实身份做事(11.4)--
第 0 只:汪汪
第 1 只:喵喵
第 2 只:汪汪
-- (2) RTTI 第一招:typeid 问身份 --
第 0 只的真实类型是 3Dog
第 1 只的真实类型是 3Cat
第 2 只的真实类型是 3Dog
-- (3) RTTI 第二招:dynamic_cast 安全转换 --
第 0 只是狗:(把球叼回来了)
第 1 只不是狗,跳过
第 2 只是狗:(把球叼回来了)

关于输出里的 3Dog ——这不是错:

30 秒实验

  1. 把 Animal 里的 virtual ~Animal() 与 virtual void speak() 都删掉(变成非多态类型),再跑一遍:输出会从 3Dog / 3Cat 全变成 6Animal ——亲眼确认"没有虚函数 = 没有运行期信息"。
  2. 把 typeid(*zoo[i]) 的星号去掉,写成 typeid(zoo[i]):输出变成指针类型本身(P6Animal,P 表示 pointer)——typeid 对指针问的是"指针的类型",不是"指向对象的类型"。
  3. 在 main 开头加一行Animal a; cout << typeid(a).name();:a 是对象而不是指针,照样能问(因为 Animal 是多态类型)。
  4. 取消注释那句 ((Dog *)zoo[1])->fetch(); ——大概率不会立刻崩(因为 fetch() 没碰成员数据),但这是运气,不是安全。 这就是 dynamic_cast 存在的理由。

常见误区

  1. "RTTI 就是 typeid" ——是两个工具:typeid(问)、dynamic_cast(转)。实际写业务代码时,dynamic_cast 用得比 typeid 多得多。
  2. "任何类都能用 RTTI" ——必须多态类型(有虚函数)。对没有虚函数的类,typeid 只看静态类型、dynamic_cast 根本编不过。
  3. "dynamic_cast 是万能的,什么都能转" ——它只能沿继承关系转(向上、向下、横向)。转到毫无关系的类型上是编译错误(那是 reinterpret_cast 的领域,见 14.1.4)。
  4. "name() 的输出可以拿来比较" ——绝对不可以。要比较就用 typeid(a) == typeid(b)(比较的是类型本身,不是字符串)。
  5. "有了 RTTI 就方便了,我到处用 typeid 分支" ——这是在用"运行时开关"替代"虚函数的多态",加一个子类就要动调度代码。RTTI 是补丁,不是主路。
  6. "RTTI 有运行开销,所以是坏东西" ——开销很小(一次指针查表 + 比较)。它真正的代价是"设计上的代价" ——用它替代多态,会让代码越来越难改。该用的时候不用,是另一种损失(比如写调试/日志/序列化时)。
  7. "((Dog *)p)->fetch() 和 dynamic_cast<Dog *>(p)->fetch() 差不多,还更快" ——差得远:前者错了就是未定义行为(可能崩、可能算出垃圾结果、可能看起来正常);后者错了给 nullptr,你能处理。 用一点点性能换"错了也有兜底",这笔账永远划算(除非它真在热点路径上、而且你已经用别的方式保证了类型正确)。
自测三问(点开对答案) 1. **RTTI 是什么的缩写?它包含哪三个零件?** ——**Run-Time Type Information(运行期类型信息)**。三个零件:**`typeid`(问类型,返回 `const std::type_info &`)、`dynamic_cast`(安全转换)、`std::type_info`(装类型信息的类,可 `==` 比较、可 `name()`)**。头文件 ``。 2. **为什么说"RTTI 必须有多态类型"?** ——因为**运行期类型信息挂在虚函数表(vtable)上**,而 vtable 只有**有虚函数的类**才有。**对非多态类型,`typeid` 只能给你静态类型**(换句话说:它骗你,但它没有别的信息可给)。**给基类加一个 `virtual` 析构,就顺手让它成了多态类型**。 3. **什么时候不该用 RTTI?** ——**当"按类型分支"这件事会不断扩张时**(每加一个子类都要回来改这段 `if`)。**这种情况应该改设计:把"该做的事"写成基类的虚函数,让每个子类自己去实现**(9.2.3 的判据:这件事主要改谁的数据,就写在谁身上)。**RTTI 的正当用途是"问一句、顺手做点附加的事"**(调试打印、接口探测、无法修改第三方类时的分类处理)。

相关笔记