引用上照样能做 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)
几点说明:
7Printer/7Scanner还是 g++/MinGW 的修饰名(数字是名字长度)——MSVC 会打印class Printer。不要依赖它。e.what()返回的具体文字是"实现定义"的 ——GCC/MinGW 是std::bad_cast,别的地方可能略有不同。 只要记得它是给人看的就行。- 第 (4) 点是本节最重要的"警告":引用版
dynamic_cast失败会抛异常,如果你不写catch,程序会打印terminate called after throwing an instance of 'std::bad_cast'然后直接死掉。 这个"死"不是崩溃,是标准规定的行为(异常没人接就调std::terminate,14.2.1 会讲)。
30 秒实验
- 把
mustPrint里的catch整段删掉,再调mustPrint(sc);→ 程序会直接终止(打印terminate called after throwing an instance of 'std::bad_cast')。这就是"没人接的异常"的下场。 - 把
dynamic_cast<Printer &>(d)的&改成*,其余不动 → 编译不过("用指针类型初始化引用")。尖括号里的&/*必须和你要的返回类型对上。 typeid(r1) == typeid(Device)→0;typeid(r1) == typeid(Printer)→1。引用上比较同样好用。- 把
Device &r1 = pr;改成Device r1 = pr;(去掉&)→ 再跑typeid(r1).name(),会变成6Device——因为这一句不是引用,是"对象切片"(11.4.2),子类部分被切掉了。
常见误区
- "引用上的
dynamic_cast失败会给个空引用" ——没有"空引用"这回事。失败就是抛std::bad_cast。 - "引用版和指针版差不多,随便挑一个" ——它们的性格完全不同:指针版是"探测"(失败是正常情况),引用版是"断言"(失败是违约)。用错了会让"正常的可选项"变成"异常流程",或者反过来让"该报错的事"被悄悄忽略。
- "
typeid(r)给的是引用的类型" ——引用会被剥掉,给的是真实身份(14.1.1 那张表里就有这一行)。只有typeid(p)(指针不带星)才给指针类型。 - "
bad_cast是编译错误" ——是运行期异常。它能不能发生,编译期看不出来(只有跑到那一句、类型又不对才抛)。所以catch写不写、写在哪,全靠你判断——这也是它容易被忽略的原因。 - "
bad_cast得自己定义" ——标准库已经给了(<typeinfo>里)。自己定义同名类型只会造成混淆。 - "
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` 了。**- ☐ 下一节(14.1.3 RTTI与多重继承)
相关笔记
- 什么是RTTI —— 上一节:
typeid与type_info - RTTI与多重继承 —— 下一节:一个对象好几个地址
- 引用概述 —— 引用的三条铁律(不能为空是本节的关键)
- 利用虚函数实现动态绑定 —— 对象切片
- 🔷 C++基础 · 索引 —— 全课路线图