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

引用上照样能做 RTTI:typeid(r) 给你对象的真实类型,dynamic_cast<Dog &>(r) 也能把引用安全地"转下去"。 但引用有一个和指针完全不同的地方,也是本节唯一要记的重点:引用没有"空引用"这个东西,所以 dynamic_cast 失败时它没法返回一个"没有" ——它只能抛异常 std::bad_cast。 一句话:指针版失败给 nullptr(你能当场判断),引用版失败抛 bad_cast(必须 try/catch 接住)。

📝 本节和前面几节的关系

14.1.1 讲的 typeid 在引用上"行为一样",所以本节的真正新东西是 dynamic_cast 到引用时的那条异常路线。 它顺便给 14.2 提前铺了路:这是本章里第一次出现"标准库自己往外抛异常"的场景 ——bad_cast 是 std::exception 家族的一员(14.2.4 会把它归位)。你看,RTTI 和异常处理本来就是缠在一起的(本章把两者放一章的又一个理由)。 往回想:7.8 讲引用的三条铁律(必须初始化、不能改绑、不能为空)——本节的麻烦全是从"不能为空"这一条来的。

一、引用上怎么用 RTTI

两件事都能做,语法几乎照抄指针版:

Device &r = printer;                    // 引用(不是指针)

typeid(r)                    // ① 问类型 → 给真实身份(Printer)
dynamic_cast<Printer &>(r)   // ② 安全转换引用

typeid(r) 的行为:标准规定"如果表达式是多态类型的左值,结果就是动态类型" ——引用在这一步会被"剥掉",所以拿到的是真实身份,不是 Device。

dynamic_cast<Printer &>(r):这是新东西 ——尖括号里写的是"引用类型"(Printer &),返回的也就是一个 Printer &。

二、本节唯一的关键差别:失败时它抛异常

先想清楚"为什么引用版必须抛异常":

失败时能给什么?
指针版 dynamic_cast<Dog *>(p) 给 nullptr ——"没有"这个状态,指针有地方装
引用版 dynamic_cast<Dog &>(r) 引用不能"是空的" ——没有任何一个值能表示"失败了"

既然没有"失败值"可返回,标准就只剩一个选择:抛异常。

try {
    Printer &p = dynamic_cast<Printer &>(dev);   // 不是打印机 → 抛 bad_cast
    p.print();
} catch (const bad_cast &e) {
    cout << "转引用失败了:" << e.what() << endl;
}

std::bad_cast 定义在 <typeinfo>(和 typeid 同一个头文件),它继承自 std::exception(14.2.4)。

指针版 vs 引用版对照表(本节的核心表):

dynamic_cast<T *>(p) dynamic_cast<T &>(r)
尖括号里写 指针类型 T * 引用类型 T &
成功时 返回 T * 返回 T &
失败时 返回 nullptr 抛 std::bad_cast
能不能"就地判断失败" 能(if (p != nullptr) / if (p)) 不能,必须 try/catch
使用场合 "有就用,没有就算了"(可选能力) "它必须是这个类型"(前置条件、契约)
写起来的味道 探测 断言

三、什么时候该用引用版

判据很简单,问一句:"转不过去,算不算错?"

场景 该用哪个 为什么
"遍历一堆设备,支持打印的就顺手打印一下" 指针版 不支持是常态,不是错误
"我要做序列化:这个对象支持 Serializable 就顺手续上" 指针版 探测接口(RTTI 最正当的用法)
"这个函数要求传进来的必须是打印机(调用方保证)" 引用版 转不过去说明调用方违约 ——异常就是"违约报告"
"我已经用 typeid 确认过类型了" 引用版(甚至 static_cast) 既然确定,就不用再走一遍安全检查

引用版还有一个"写法上的好处":调用方不需要写 &,函数签名看起来更自然(void print(Printer &p) 比 void print(Printer *p) 少一类空指针问题)。

代价也很实在:它把"检查"换成了"异常" ——而异常是一条"昂贵且容易忘掉"的路(忘了 catch 就直接终止程序)。

四、动手 Demo:同一个需求,两种接失败的方式

任务:写两个函数 ——tryPrint(Device *)(失败跳过)和 print(Device &)(失败抛 bad_cast),对照它们的性格。

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

class Device {
public:
    virtual ~Device() {}
    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; }
};

// ① 指针版:转不了就算了
void tryPrint(Device *d) {
    Printer *p = dynamic_cast<Printer *>(d);
    if (p != nullptr) p->print();
    else              cout << "(不是打印机,跳过)" << endl;
}

// ② 引用版:转不了就抛 bad_cast
void mustPrint(Device &d) {
    try {
        Printer &p = dynamic_cast<Printer &>(d);
        p.print();
    } catch (const bad_cast &e) {
        cout << "(转引用失败:" << e.what() << ")" << endl;
    }
}

int main() {
    Printer pr;
    Scanner sc;
    Device &r1 = pr;
    Device &r2 = sc;

    cout << "-- (1) 引用上的 typeid --" << endl;
    cout << "r1 的动态类型:" << typeid(r1).name() << endl;   // Printer
    cout << "r2 的动态类型:" << typeid(r2).name() << endl;   // Scanner
    cout << "(两个引用的静态类型都是 Device&,typeid 给的是真实身份)" << endl;

    cout << "-- (2) 指针版:失败给 nullptr --" << endl;
    tryPrint(&pr);
    tryPrint(&sc);

    cout << "-- (3) 引用版:失败抛 bad_cast --" << endl;
    mustPrint(pr);
    mustPrint(sc);

    cout << "-- (4) 引用版如果不接住 --" << endl;
    cout << "(bad_cast 没被 catch 就会一路上抛,最后 terminate)" << endl;
    return 0;
}

看到什么算成功:

-- (1) 引用上的 typeid --
r1 的动态类型:7Printer
r2 的动态类型:7Scanner
(两个引用的静态类型都是 Device&,typeid 给的是真实身份)
-- (2) 指针版:失败给 nullptr --
(打印中……)
(不是打印机,跳过)
-- (3) 引用版:失败抛 bad_cast --
(打印中……)
(转引用失败:std::bad_cast)
-- (4) 引用版如果不接住 --
(bad_cast 没被 catch 就会一路上抛,最后 terminate)

几点说明:

30 秒实验

  1. 把 mustPrint 里的 catch 整段删掉,再调 mustPrint(sc); → 程序会直接终止(打印 terminate called after throwing an instance of 'std::bad_cast')。这就是"没人接的异常"的下场。
  2. 把 dynamic_cast<Printer &>(d) 的 & 改成 *,其余不动 → 编译不过("用指针类型初始化引用")。尖括号里的 & / * 必须和你要的返回类型对上。
  3. typeid(r1) == typeid(Device) → 0;typeid(r1) == typeid(Printer) → 1。引用上比较同样好用。
  4. 把 Device &r1 = pr; 改成 Device r1 = pr;(去掉 &)→ 再跑 typeid(r1).name(),会变成 6Device ——因为这一句不是引用,是"对象切片"(11.4.2),子类部分被切掉了。

常见误区

  1. "引用上的 dynamic_cast 失败会给个空引用" ——没有"空引用"这回事。失败就是抛 std::bad_cast。
  2. "引用版和指针版差不多,随便挑一个" ——它们的性格完全不同:指针版是"探测"(失败是正常情况),引用版是"断言"(失败是违约)。用错了会让"正常的可选项"变成"异常流程",或者反过来让"该报错的事"被悄悄忽略。
  3. "typeid(r) 给的是引用的类型" ——引用会被剥掉,给的是真实身份(14.1.1 那张表里就有这一行)。只有 typeid(p)(指针不带星)才给指针类型。
  4. "bad_cast 是编译错误" ——是运行期异常。它能不能发生,编译期看不出来(只有跑到那一句、类型又不对才抛)。所以 catch 写不写、写在哪,全靠你判断——这也是它容易被忽略的原因。
  5. "bad_cast 得自己定义" ——标准库已经给了(<typeinfo> 里)。自己定义同名类型只会造成混淆。
  6. "dynamic_cast 到引用一定比到指针安全" ——不是"更安全",是"更严格"。它逼你面对失败(要么处理异常、要么程序终止),但代价是引入了异常路径 ——在不想引入异常的地方(比如 -fno-exceptions 的嵌入式代码),指针版才是唯一选择。
自测三问(点开对答案) 1. **`dynamic_cast` 转到引用时,失败会发生什么?为什么不能像指针那样返回"空"?** ——**抛 `std::bad_cast`**。因为 **C++ 里引用不能为空** ——**没有任何一个值能表示"转换失败",所以只能用异常报告**。 2. **什么时候该用引用版、什么时候该用指针版?** ——**问一句"转不过去算不算错"**:**不算错(可选能力、探测接口)→ 指针版**(失败给 `nullptr`,就地判断);**算错(前置条件、调用方违约)→ 引用版**(失败抛 `bad_cast`)。 3. **`typeid(r)` 里 `r` 是 `Device &`,为什么不返回 `Device`?** ——因为标准规定:**表达式是"多态类型的左值"时,`typeid` 给动态类型**,**引用在这一步被剥掉**,所以给的是真实身份(如 `Printer`)。**顺带记住:如果写成 `Device r = printer;`(不是引用,是对象),就真的被切片成 `Device` 了。**

相关笔记