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

C++ 有四把"转换的扳手",每一把只干一类活、只担一类风险:static_cast(编译期转换,不给检查)、dynamic_cast(运行期检查,属于 RTTI)、const_cast(只动 const)、reinterpret_cast(按比特重解释,最危险)。 本节的核心结论是一条命令:把 C 风格的强转 (T)x 全部换成这四把带名字的扳手。 因为 C 风格强转是"依次试这四把,哪把能用用哪把" ——你不知道它最后用了哪一把,代码一改它还会悄悄换一把。 四把扳手的性格一句话:static 最快但不管对错;dynamic 最稳但要花时间(还必须有虚函数);const 只管 const;reinterpret 不管死活。

📝 本节和前面几节的关系

14.1.1~14.1.3 把 typeid 和 dynamic_cast 都讲过了,本节把它们放进"四种转换"的坐标系里,做一次总收。 它同时收了三笔旧账: - 11.2.4 的转换运算符(自己写 operator double())——那是"自定义类型参与的隐式转换",本节讲的是"显式命令编译器转换",两者是"自动挡"和"手动挡"; - 7.4.4 的 void * ——它是 static_cast 和 dynamic_cast 都能处理的特殊目标; - 2.4(类型与转换) ——static_cast<int>(3.14159) 就是我们那时说的"截断",只不过现在写得更明确。 往回想:为什么需要 dynamic_cast? 因为第十一章的继承在编译期把"子类身份"丢掉了(静态类型只记得 Animal *)——RTTI 就是把这个信息捡回来的工具。

一、先说说 C 风格强转的"三宗罪"

C 风格强转就是那个老写法:

int n = (int)3.14;
Dog *d = (Dog *)p;
char *s = (char *)constMsg;

编译器对它的处理方式是:按顺序试着当 const_cast、static_cast、reinterpret_cast 用,第一个能编译过的就算数。

于是有了三宗罪:

罪 说明
① 不透明 写的人看不出它到底想干哪件事(是"类型转换"还是"重解释比特"?),读的人更看不出
② 会悄悄改含义 今天 (Dog *)p 走的是 static_cast(因为是父子关系),明天你把继承关系改了,同一句话就变成了 reinterpret_cast ——代码没动,含义变了、还编得过
③ 搜不到 (T)x 这种写法没法用 static_cast 去搜。在一堆 (int)、(char *) 里想找出"危险的那几个",基本靠眼力

从今天起,改掉这个习惯:写四把扳手里的某一把,让"你想干什么"写在代码上。 (编译器的帮忙:g++ 加 -Wold-style-cast 就能把 C 风格强转全报出来,项目里值得打开。)

二、四把扳手全景表

static_cast dynamic_cast const_cast reinterpret_cast
什么时候算 编译期 运行期(沿继承链查) 编译期 编译期
能干什么 数值互转、上行/下行转换、void * 中转、枚举↔整数 只做继承体系的转换(上行/下行/交叉/void*) 只删或加 const / volatile 按比特重新解释:指针↔整数、无关指针互转
安全性检查 完全没有(错了就是未定义行为) 有(失败给 nullptr / 抛 bad_cast) 没有 没有
前提 两个类型之间有合理的转换关系 源和目标都得是多态类型 类型本身一样,只差 const 无(什么都能转)
能不能去掉 const ❌ ❌ ✅ 只有它能 ❌
开销 零开销(编译期算好,可能做一次偏移加减) 有(运行期查表/遍历) 零 零
危险程度 中(用错不报错) 低(有检查) 中(UB 风险在"原对象真是 const") 最高

一句口诀先记住:

"数值与继承用 static,安全的继承用 dynamic,const 的事归 const,看比特找 reinterpret,实在不用 C 强转。"

三、static_cast:编译期转换(最常用)

它管四类活:

// ① 数值之间的转换(这就是"截断"的正式写法)
int    n = static_cast<int>(3.99);          // 3(截断,不四舍五入)
double d = static_cast<double>(7) / 2;      // 3.5(3.3.2 那个经典坑的正式解法)

// ② 继承体系里的转换(上行安全;下行不检查!)
Animal *a = static_cast<Animal *>(dogPtr);  // 子 → 父(安全)
Dog    *d2 = static_cast<Dog *>(a);         // ⚠️ 父 → 子(不检查!错了就是 UB)

// ③ void* 与具体指针互转(malloc 返回值那种场景)
void *raw = static_cast<void *>(p);
int  *ip  = static_cast<int *>(raw);

// ④ 枚举 ↔ 整数、显式调用构造函数
int   code = static_cast<int>(WED);
Weekday w = static_cast<Weekday>(3);

它做不到的(会被编译器拦下):

const int ci = 5;
int *p = static_cast<int *>(&ci);        // ❌ 不能去掉 const
Swimmer *s = static_cast<Swimmer *>(pf); // ❌ 不能在无关类型间转(那是 14.1.3 的交叉转换)

两条要点:

四、dynamic_cast:运行期检查(RTTI 的主角)

语法上唯一要注意的:尖括号里写"指针类型"或"引用类型"。

Dog *d  = dynamic_cast<Dog *>(animalPtr);      // 指针版:失败给 nullptr
Dog &dr = dynamic_cast<Dog &>(animalRef);      // 引用版:失败抛 std::bad_cast
void *v = dynamic_cast<void *>(animalPtr);     // 特殊版:给"最派生对象"的地址

它能做的四件事(回顾 14.1.2 / 14.1.3):

方向 例子 说明
上行(子 → 父) Dog * → Animal * 能做,但没必要(隐式转换或 static_cast 就行,还更快)
下行(父 → 子) Animal * → Dog * 主要用途:检查 + 转换
交叉(父 → 兄弟父) Flyer * → Swimmer * 独门绝技(14.1.3)
取对象头地址 任意多态指针 → void * 判断"是不是同一个对象"的正解(14.1.3)

三条硬要求:

  1. 源类型必须是多态类型(有虚函数)——否则编译不过;
  2. 目标也必须写在继承体系里(不能转到毫不相干的类型);
  3. dynamic_cast 不能去掉 const ——const Dog * 只能转到 const Dog * / const Animal *(要去 const 得先用 const_cast,这本身就该让你警惕)。

五、const_cast:只动 const

它是四把扳手里"最专一"的:只负责删掉(或加上)const / volatile,类型本身必须一模一样。

const char *msg = buf;
char *p = const_cast<char *>(msg);         // 去掉 const
const int &cr = x;
int &r = const_cast<int &>(cr);            // 去掉 const(引用版)

必须记住的安全红线:

只有当"原对象本身不是 const"时,用 const_cast 去改它才是安全的。

int x = 5;                      // 原对象不是 const
const int &cr = x;
const_cast<int &>(cr) = 99;     // ✅ 安全:改的其实是 x

const int y = 5;                // 原对象就是 const
const_cast<int &>(y) = 99;      // ☠️ 未定义行为(y 可能在只读内存里)

它的正当用途几乎只有一个:对接"参数忘了写 const 的老接口"(C 库、遗留代码):

size_t old_strlen(char *s);              // 老接口:没写 const
const char *msg = buf;
old_strlen(const_cast<char *>(msg));     // 明知道它不改,只好替它去掉 const

注意:这是在"替别人的错误买单" ——写的时候必须有把握"那个函数确实不会改",否则就是给自己埋雷。

六、reinterpret_cast:按比特重新解释(最危险)

它不做任何"转换",只做一件事:把这段比特原样交给另一个类型去看。

三类典型用法:

// ① 指针 ↔ 整数(看地址的数值)
uintptr_t addr = reinterpret_cast<uintptr_t>(p);

// ② 无关类型的指针互转(协议解析、硬件寄存器)
char *bytes = reinterpret_cast<char *>(&someStruct);   // 按字节看一个结构体

// ③ 二进制层面的"类型双关"(看浮点数的比特长什么样)
unsigned long long bits = reinterpret_cast<unsigned long long &>(d);

它的性格:

用它的心态应该像"开保险箱用炸药":技术上可行,但你要清楚自己在炸什么。

七、该用哪一把:决策表

你想干的事 用哪把 备注
数值互转(int ↔ double)、枚举 ↔ 整数 static_cast 会截断,小心
子类指针 → 父类指针 不用写转换(隐式就会)或 static_cast 安全
父类指针 → 子类指针 dynamic_cast 除非你能保证类型,才用 static_cast
从一个基类转到另一个不相关的基类 dynamic_cast 唯一能做交叉转换的
判断"两个不同基类指针是不是同一个对象" dynamic_cast<void *> 14.1.3
去掉/加上 const const_cast 只在"原对象不是 const"时安全
指针 ↔ 整数、按字节看数据、协议解析 reinterpret_cast 最危险,先想清楚有没有别的路
什么都不确定 先停下来想清楚 不要用 C 风格强转

最后一句工程建议(也是 12.4 / 13.5 的老调):"能不用转换就不用转换" ——大多数 dynamic_cast 出现的地方,本来可以用一个虚函数解决(9.2.3 的判据)。转换是"设计上有一点没说清"的信号。

八、动手 Demo:四把扳手各来一次

任务:四种转换各用一遍,特别看一眼"static_cast 骗过编译器、却骗不过 typeid"这一幕。

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

class Animal {
public:
    virtual ~Animal() {}
    virtual string name() const { return "动物"; }
};

class Dog : public Animal {
public:
    string name() const { return "狗"; }
    void fetch() const { cout << "(叼球)" << endl; }
};

class Cat : public Animal {
public:
    string name() const { return "猫"; }
};

// ③ 一个"参数忘了写 const"的老接口
void oldPrint(char *s) { cout << "③ 老接口收到:" << s << endl; }

int main() {
    // ── ① static_cast:数值转换(截断)────────
    double pi = 3.14159;
    cout << "① static_cast<int>(3.14159) = " << static_cast<int>(pi) << endl;

    // ── ② dynamic_cast:向下转换 + 检查 ───────
    Dog dog;
    Cat cat;
    Animal *pets[2] = { &dog, &cat };
    for (int i = 0; i < 2; i++) {
        if (Dog *d = dynamic_cast<Dog *>(pets[i])) {
            cout << "② 第 " << i << " 只是狗:";
            d->fetch();
        } else {
            cout << "② 第 " << i << " 只不是狗,跳过" << endl;
        }
    }

    // ── ③ const_cast:对接老接口 ──────────────
    char buf[] = "hello";                  // ★ 数组可改,安全
    const char *msg = buf;                 // 从"只读视角"看它
    oldPrint(const_cast<char *>(msg));
    cout << "   (如果 msg 指向的是字符串字面量,这样做就危险了)" << endl;

    // ── ④ reinterpret_cast:看比特 ────────────
    double one = 1.0;
    unsigned long long bits = reinterpret_cast<unsigned long long &>(one);
    cout << "④ 1.0 的比特 = 0x" << hex << bits << dec << endl;

    // ── ⑤ 危险示范:static_cast 骗得过编译器 ───
    Animal *a = &cat;
    Dog *bad = static_cast<Dog *>(a);       // 编过了!但它其实是猫
    cout << "⑤ static_cast 把猫当狗:typeid 仍然报 "
         << typeid(*bad).name() << "(骗不过 RTTI)" << endl;

    return 0;
}

看到什么算成功:

① static_cast<int>(3.14159) = 3
② 第 0 只是狗:(叼球)
② 第 1 只不是狗,跳过
③ 老接口收到:hello
   (如果 msg 指向的是字符串字面量,这样做就危险了)
④ 1.0 的比特 = 0x3ff0000000000000
⑤ static_cast 把猫当狗:typeid 仍然报 4Cat(骗不过 RTTI)

读结果:

30 秒实验

  1. 把 ⑤ 那句加一行 bad->fetch(); → 大概率会崩或者乱跳 ——这就是"不做检查的向下转换"的下场(它的严重性在于"可能这次没事",下一次就出事了)。
  2. 把 ⑤ 的 static_cast 改成 dynamic_cast → bad 变成 nullptr,加个判断就能安全处理。同一个意图,多打几个字,换回安全。
  3. 把 ④ 的 double one = 1.0; 改成 2.0 → 比特变成 0x4000000000000000(指数位加 1)。再看 0.5 → 0x3fe0000000000000。三个数连起来看,就能体会"指数位"在干什么。
  4. 把 ③ 的 char buf[] = "hello"; 改成 const char *msg = "hello"; 再 const_cast → 可能看起来也没事(因为 oldPrint 不改它)。但如果 oldPrint 里写了一个字符,就可能崩 ——"看起来没事"是这类 UB 最坏的地方。
  5. 加编译选项 -Wold-style-cast(Dev-C++:工具 → 编译选项 → 在命令行里加),把自己写过的一个 .cpp 扫一遍 ——看看用了几次 C 风格强转。

常见误区

  1. "static_cast 和 C 风格强转一样" ——不一样:static_cast 只做"合理的那一类转换",做不到的会直接编译错误(比如去掉 const、无关类型互转)。C 风格强转是"四把都试一遍",你根本不知道最后用了哪把。
  2. "static_cast 向下转换是安全的,因为它能编过" ——能编过只代表"类型之间有关系"(父子),不代表"这个对象真的是子类"。 错了就是未定义行为(Demo ⑤)。
  3. "dynamic_cast 比 static_cast 慢,所以能不用就不用" ——看场合:已经保证过类型的地方(比如刚判过 typeid),用 static_cast 没问题;但在"探测"的场合,用 static_cast 相当于拆了安全带。性能焦虑应该留给热点路径,不是留给初次写对的代码。
  4. "const_cast 是"改 const 变量"的合法方式" ——它只是"合法地去掉 const 这个标签"。对真正 const 的对象动手仍是未定义行为(可能崩、可能改了没效果)。
  5. "reinterpret_cast 是万能转换,什么都能转" ——是"什么都能编过",不是"什么都能对"。它不做任何调整,所以:多重继承会错、平台一换就错、对齐不对会崩。
  6. "reinterpret_cast 可以用来做父子转换" ——大错:它不做偏移调整,而多重继承下基类子对象就在别的偏移处(14.1.3)。父子转换老老实实用 static_cast / dynamic_cast。
  7. "箭头只能有一个方向" ——四把扳手不是替代关系,是分工:static_cast 管数值与继承,dynamic_cast 管安全的继承,const_cast 管 const,reinterpret_cast 管比特。一次转换要同时做两件事(比如"去 const + 向下转"),必须分两步写(dynamic_cast<Dog *>(const_cast<Animal *>(cp)))——而"需要写两步"本身就是一个"这里很可疑"的信号。
自测三问(点开对答案) 1. **四把转换扳手各自"只干什么"?** ——**`static_cast`:编译期的合理转换(数值、继承、`void*`、枚举);`dynamic_cast`:运行期检查的继承转换(下行/交叉/取对象头地址);`const_cast`:只动 `const`/`volatile`;`reinterpret_cast`:按比特重新解释(指针↔整数、无关类型)。** 2. **为什么说要用它们替换 C 风格强转?** ——**因为 C 风格强转是"依次尝试这四把、哪把能用用哪把"**:**① 看不出意图;② 类型关系一变它就会悄悄换一把(还编得过);③ 搜不到、审不掉。** 而带名字的转换**把"我想干什么"写在了代码上,编译器也就知道该拦谁。** 3. **"父类指针转成子类指针"该怎么写?为什么不能随便用 `static_cast`?** ——**默认用 `dynamic_cast`**(**失败给 `nullptr` / 抛 `bad_cast`,你能处理**)。**`static_cast` 在编译期不做任何检查** ——**对象其实不是子类时,你会拿到一个"指向错误位置"的指针,此后任何使用都是未定义行为**(Demo ⑤ 的 `4Cat` 就是现场)。

相关笔记