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

RTTI 就是"运行期问类型"这件事的官方名字:程序在跑的时候,拿着一个基类指针/引用,能问出它背后那个对象的真实类型。 本节要把两个术语钉死:静态类型(写代码时声明的类型,编译期就知道)和动态类型(对象在运行时的真实类型)。正常编译只看前者,RTTI 是唯一能拿到后者的正规通道。 工具就一个 typeid,加一个类 std::type_info:typeid(表达式) 返回一个 const type_info &,能 == 比较、能 name() 取名字。

📝 本节和前面几节的关系

14.1 讲了 RTTI 是"两个工具 + 一个类"的总称,本节把其中的 typeid 这一个工具展开讲透(dynamic_cast 在 14.1.2 / 14.1.4)。 它和 11.4 是"正反面":11.4 的虚函数让你不用关心类型就能正确做事(这是首选);本节让你在必须关心类型时有个正规办法(这是补充)。 它也是 14.2.3"异常匹配"的预演 ——异常的匹配规则本质上就是"拿 typeid 去比"(只不过匹配允许"派生类 → 基类"这一种宽松)。

一、先分清两个词:静态类型与动态类型

这两个词是本章的通用货币,必须钉死。

class Animal { public: virtual ~Animal() {} };
class Dog : public Animal { };

Animal *p = new Dog();
说法 关于 p 关于 *p
静态类型(编译期看) Animal * Animal
动态类型(运行期看) Animal *(指针自己的类型永远是它) Dog

一句话:静态类型是"单子上写的",动态类型是"实际是谁"。 虚函数(11.4)是"不查单子、直接让它自己干";RTTI 是"停下来查一次单子"。

二、typeid 的用法

语法极其简单:

#include <typeinfo>

typeid(表达式)      // 也可以写 typeid(类型名)

返回类型是 const std::type_info & ——一个引用(type_info 不能拷贝,见第三节)。

最常用的两个写法:

typeid(*p) == typeid(Dog)       // ① 比较:它到底是不是 Dog?
typeid(*p).name()               // ② 取名字:看看它叫什么(仅供调试打印)

typeid 到底给"静态"还是"动态",看这张表:

表达式 静态类型是多态的吗 typeid 给什么
*p,p 是 Animal * 且 Animal 有虚函数,实际指向 Dog ✅ Dog(动态类型)
*p,Animal 没有虚函数,实际指向 Dog ❌ Animal(静态类型,骗你)
r,r 是 Animal & 绑到 Dog 上 ✅ Dog(引用也一样剥掉,看动态)
p(注意没有 *) — Animal *(指针自己的类型)
a,a 是 Animal 类型对象(不是指针/引用) — Animal(早就切片了,11.4.2)
x,x 是 int / double — 类型本身(内置类型没有多态一说)

三条要记住的:

  1. typeid(*p) 和 typeid(p) 是两回事:一个问"它指着谁",一个问"它自己是什么"。 差一个星号,答案完全不同。
  2. typeid 不能"凭空问一个不存在的对象":如果 p 是空指针,typeid(*p) 会抛 std::bad_typeid 异常(这也是本章后面要学的"异常"的一个标准成员)。typeid(p)(不带星)对空指针是安全的(问的是指针类型)。
  3. typeid 对同一个类型,一辈子给同一个答案 ——所以比较用 ==,稳、快、不用字符串。

三、std::type_info 这个类

它就是"类型名片"。 你在 <typeinfo> 里能找到它,但基本不用自己创建(typeid 给你)。

成员 说明
operator== / operator!= 判断是不是同一个类型(这是最有用的)
name() 返回 const char *:类型的名字,格式由编译器决定(g++ 给 3Dog,MSVC 给 class Dog)
before(const type_info &) 排序用的比较(顺序由实现定义,一般用不上)
hash_code() C++11 起可用,给类型算一个哈希值,可放进哈希表

一条必须知道的限制:

type_info 是"不可拷贝、不可赋值"的(它的拷贝构造和 = 被标记为删除)。 所以你只能"拿着引用看",不能"存一份带走"。

type_info t = typeid(x);            // ❌ 编译不过(拷贝构造被删)
const type_info &t = typeid(x);     // ✅ 只能拿引用

这条限制有个很实际的后果:type_info 不能当容器的元素(vector<type_info> 不行)。要"按类型归类",标准做法是用 type_index(C++11,<typeindex>)或者干脆用字符串/枚举做 key。 教材没展开这一层,认得就行。

四、三种"判断类型"的写法,怎么选

同一个需求("是打印机就打印"),有三种写法:

写法 代码 评价
① 虚函数(首选) d->doJob(); 基类声明虚函数,各自实现。加新类型不用改老代码(11.4 讲的,这是正路)
② dynamic_cast if (Printer *p = dynamic_cast<Printer *>(d)) p->print(); "顺手做点附加的事",比如"支持某接口就顺便存一下"。能用,且比 ① 更灵活
③ typeid 比较 if (typeid(*d) == typeid(Printer)) … 只用来"分类统计/调试打印";拿它做业务分支 = 加一个子类就要回来改这里

三者的关系一句话:

能用 ① 就用 ①;需要"探测能力"用 ②;typeid 留在"调试、日志、统计"这类地方。

另外对比一下 dynamic_cast 与 typeid 的性格:

typeid dynamic_cast
干什么 问(只读信息,不改指针) 转(真要拿到那个子类类型的指针)
失败怎么办 不会失败(问题不合法的类型时编译期就报错;空指针抛 bad_typeid) 指针给 nullptr;引用抛 bad_cast
能不能"横着转" 不能(只回答类型) 能(交叉转换,14.1.3)

五、动手 Demo:办公室设备清单

任务:一个 vector<Device *> 里装着打印机和扫描仪,分别用 typeid 和 dynamic_cast 数一遍、干一遍活,再用虚函数做一遍对照。

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

class Device {
public:
    virtual ~Device() {}                       // ★ 多态类型(RTTI 的前提)
    virtual string kind() const { return "设备"; }
};

class Printer : public Device {
public:
    string kind() const { return "打印机"; }
    void print() const { cout << "(打印中……)" << endl; }
};

class Scanner : public Device {
public:
    string kind() const { return "扫描仪"; }
    void scan() const { cout << "(扫描中……)" << endl; }
};

int main() {
    vector<Device *> office;
    office.push_back(new Printer());
    office.push_back(new Scanner());
    office.push_back(new Printer());

    // ── ① typeid 比较:分类统计 ────────────────
    int nPrinter = 0, nScanner = 0;
    for (size_t i = 0; i < office.size(); i++) {
        if      (typeid(*office[i]) == typeid(Printer)) nPrinter++;
        else if (typeid(*office[i]) == typeid(Scanner)) nScanner++;
    }
    cout << "① 打印机 " << nPrinter << " 台,扫描仪 " << nScanner << " 台" << endl;

    // ── ② dynamic_cast:让打印机干活 ───────────
    cout << "② " ;
    for (size_t i = 0; i < office.size(); i++) {
        Printer *p = dynamic_cast<Printer *>(office[i]);
        if (p != nullptr) { cout << "第 " << i << " 台干上了"; p->print(); }
        else                cout << "第 " << i << " 台不是打印机,跳过" << endl;
    }

    // ── ③ 虚函数版:同样的"报身份",不需要 RTTI ──
    for (size_t i = 0; i < office.size(); i++)
        cout << "③ 第 " << i << " 台是:" << office[i]->kind() << endl;

    // ── ④ 空指针上做 typeid(*p) 会抛异常 ────────
    Device *bad = nullptr;
    try {
        cout << typeid(*bad).name() << endl;       // ✗ 抛 bad_typeid
    } catch (const bad_typeid &e) {
        cout << "④ 对空指针做 typeid(*p) 抛了异常:" << e.what() << endl;
    }

    for (size_t i = 0; i < office.size(); i++) delete office[i];
    return 0;
}

看到什么算成功:

① 打印机 2 台,扫描仪 1 台
② 第 0 台干上了(打印中……)
第 1 台不是打印机,跳过
第 2 台干上了(打印中……)
③ 第 0 台是:打印机
③ 第 1 台是:扫描仪
③ 第 2 台是:打印机
④ 对空指针做 typeid(*p) 抛了异常:std::bad_typeid

这一关要体会的东西:

30 秒实验

  1. typeid(*office[i]).name() 打印出来看一眼 ——Dev-C++(MinGW)下是 7Printer / 7Scanner(7 是名字长度),MSVC 下是 class Printer。确认它"能用来看、不能用来判断"。
  2. 把 Device 里的两个 virtual 都删掉再跑 ①:nPrinter 会变成 0 ——因为 typeid(*office[i]) 现在给的全是 Device。(顺带:dynamic_cast 那一段会直接编译不过。)
  3. 把 ① 里的 typeid(*office[i]) 改成 typeid(office[i]) ——两个计数都变 0(比的是"指针类型")。
  4. cout << (typeid(int) == typeid(long)) → 0;typeid(int) == typeid(int) → 1。比较的是类型本身,不是字符串。

常见误区

  1. "typeid 是编译期的(像 sizeof)" ——不是。sizeof / decltype 是编译期的;typeid 对多态类型是运行期的(要去对象里查 vtable)。这也是它能在"编译期不知道类型"时起作用的原因。
  2. "typeid 一定返回真实类型" ——只在多态类型上。基类没有虚函数时,它给你静态类型,而且不报错。 这条最容易骗人。
  3. "typeid(p) 和 typeid(*p) 一样" ——差一个星号,答案完全不同:前者问指针类型(P6Animal),后者问指向对象的类型。
  4. "name() 可以拿来比较字符串" ——不可以(实现定义)。要比较就 typeid(a) == typeid(b)。
  5. "type_info 可以存起来" ——不能拷贝。要拿它做容器 key,用 std::type_index(C++11)。
  6. "typeid 不会出问题,随便用" ——空指针上写 typeid(*p) 会抛 std::bad_typeid。判个空再问,或者问 typeid(p)。
  7. "模板里也能靠 typeid 判断类型" ——技术上可以,但不是好习惯。编译期能决定的事应该用模板特化(12.3.1)或 if constexpr(C++17)来解决 ——"编译期的事编译期办,运行期的事运行期办",这是 C++ 的一贯态度(1.1.3 的零开销哲学)。
自测三问(点开对答案) 1. **静态类型和动态类型分别指什么?编译器按哪个走?** ——**静态类型 = 代码里声明的类型(编译期就知道);动态类型 = 对象运行时的真实类型。** **编译器一律按静态类型做检查**(所以"基类指针调不了子类独有函数")。**RTTI 是唯一能正规拿到动态类型的通道。** 2. **`typeid(*p)` 什么时候给动态类型、什么时候给静态类型?** ——当 `*p` 的静态类型是**多态类型**(有虚函数)时,给它**指向对象的动态类型**;**否则给静态类型**。**另外注意 `typeid(p)`(不带星)问的是指针自己的类型。** 3. **为什么说"判断类型不要用 `name()`"?那该用什么?** ——因为 **`name()` 的返回值是"实现定义"的**(g++ 给 `3Dog`,MSVC 给 `class Dog`),**换编译器就变**。**该用 `typeid(a) == typeid(b)` 比较类型本身**,或者用 `dynamic_cast` 直接转(转成功就是,失败就是不是)。

相关笔记