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 到底什么时候用? 真正合理的场景是:
- 框架/库要对"外来对象"做分类处理(比如调试打印、日志、序列化);
- 无法修改类的时候(第三方库的类里加不了虚函数);
- 少数"补丁式"的分支,而且你很确定它不会再扩张;
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 ——这不是错:
- g++ / MinGW(Dev-C++ 内置的就是它) 用的是修饰名(mangled name):
3Dog里的3表示"接下来这个名字有 3 个字符";Cat就是3Cat。 - MSVC(Visual Studio) 会输出
class Dog。 - 所以这一行在不同编译器上长得不一样 ——这正是本节第四点说的"
name()是实现定义的"。看它一眼可以,写代码依赖它绝对不行。
30 秒实验
- 把
Animal里的virtual ~Animal()与virtual void speak()都删掉(变成非多态类型),再跑一遍:输出会从3Dog/3Cat全变成6Animal——亲眼确认"没有虚函数 = 没有运行期信息"。 - 把
typeid(*zoo[i])的星号去掉,写成typeid(zoo[i]):输出变成指针类型本身(P6Animal,P表示 pointer)——typeid对指针问的是"指针的类型",不是"指向对象的类型"。 - 在
main开头加一行Animal a; cout << typeid(a).name();:a是对象而不是指针,照样能问(因为Animal是多态类型)。 - 取消注释那句
((Dog *)zoo[1])->fetch();——大概率不会立刻崩(因为fetch()没碰成员数据),但这是运气,不是安全。 这就是dynamic_cast存在的理由。
常见误区
- "RTTI 就是
typeid" ——是两个工具:typeid(问)、dynamic_cast(转)。实际写业务代码时,dynamic_cast用得比typeid多得多。 - "任何类都能用 RTTI" ——必须多态类型(有虚函数)。对没有虚函数的类,
typeid只看静态类型、dynamic_cast根本编不过。 - "
dynamic_cast是万能的,什么都能转" ——它只能沿继承关系转(向上、向下、横向)。转到毫无关系的类型上是编译错误(那是reinterpret_cast的领域,见 14.1.4)。 - "
name()的输出可以拿来比较" ——绝对不可以。要比较就用typeid(a) == typeid(b)(比较的是类型本身,不是字符串)。 - "有了 RTTI 就方便了,我到处用
typeid分支" ——这是在用"运行时开关"替代"虚函数的多态",加一个子类就要动调度代码。RTTI 是补丁,不是主路。 - "RTTI 有运行开销,所以是坏东西" ——开销很小(一次指针查表 + 比较)。它真正的代价是"设计上的代价" ——用它替代多态,会让代码越来越难改。该用的时候不用,是另一种损失(比如写调试/日志/序列化时)。
- "
((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()`)**。头文件 `- ☐ 下一节(14.1.1 什么是 RTTI)
相关笔记
- 什么是RTTI —— 下一节:
typeid与type_info的细节 - 虚函数概述 —— RTTI 的地基:动态绑定三条件
- 利用虚函数实现动态绑定 —— 父类指针数组(本章 Demo 的原型)
- 多重继承 —— 14.1.3 要处理的复杂情况
- 🔷 C++基础 · 索引 —— 全课路线图