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 的交叉转换)
两条要点:
static_cast最容易被误用的一处就是"向下转换":它编得过,但完全不检查。 父类指针不是子类时就转,等于拿到一个"指错地方"的指针 ——这是 C++ 里最经典的未定义行为来源之一。(要从父到子,老实用dynamic_cast,除非你已经有了别的保证。)static_cast是"零开销"的 ——它不是"没有动作",而是"动作在编译期就算好了"(比如把地址加上偏移,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) |
三条硬要求:
- 源类型必须是多态类型(有虚函数)——否则编译不过;
- 目标也必须写在继承体系里(不能转到毫不相干的类型);
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);
它的性格:
- 不会做任何偏移调整(不像
static_cast会算基类偏移)——所以"它在多重继承下一定会出错"(14.1.3 那个坑,用reinterpret_cast只会更糟); - 保证"转回来还是原值"(
reinterpret_cast<T*>(reinterpret_cast<U*>(p)) == p),但不保证"转过去能用"; - 平台相关:指针和整数的大小关系、字节序、对齐,全都得你自己负责;
- 不能去掉
const。
用它的心态应该像"开保险箱用炸药":技术上可行,但你要清楚自己在炸什么。
七、该用哪一把:决策表
| 你想干的事 | 用哪把 | 备注 |
|---|---|---|
数值互转(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)
读结果:
- ① "3" 不是 4:
static_cast是截断,不是四舍五入(想四舍五入得自己+0.5或用<cmath>的round)。 - ② 是
dynamic_cast的标准姿势:if (Dog *d = dynamic_cast<Dog *>(p))一行完成"声明 + 检查 + 能用"(nullptr当条件是false)。 - ③ 那句提醒很重要:
const_cast本身不危险,"对真正const的对象动手"才危险。 这里buf是数组(不是字面量),所以安全。 - ④
0x3ff0000000000000是 IEEE 754 双精度里1.0的比特(符号位 0、指数0x3FF、尾数全 0)——十六进制输出默认小写。看它的意义在于体会"比特是比特,怎么解释靠类型"。 (更严谨的写法:用memcpy把double拷进unsigned long long。reinterpret_cast那种"类型双关"在严格别名规则下是灰色地带 ——认得它、能用它看数据,但别在关键代码里依赖它。) - ⑤ 是本节最有价值的一幕:
static_cast骗过了编译器(编译期检查只看静态类型),但typeid拿到了真相(4Cat) ——"RTTI 是照妖镜"。不过照出来也没用:bad已经是个错误的指针了,你只能靠"别这么写"来避免这个坑。 4Cat还是 g++/MinGW 的修饰名(MSVC 会给class Cat);不要依赖这个字符串。
30 秒实验
- 把 ⑤ 那句加一行
bad->fetch();→ 大概率会崩或者乱跳 ——这就是"不做检查的向下转换"的下场(它的严重性在于"可能这次没事",下一次就出事了)。 - 把 ⑤ 的
static_cast改成dynamic_cast→bad变成nullptr,加个判断就能安全处理。同一个意图,多打几个字,换回安全。 - 把 ④ 的
double one = 1.0;改成2.0→ 比特变成0x4000000000000000(指数位加 1)。再看0.5→0x3fe0000000000000。三个数连起来看,就能体会"指数位"在干什么。 - 把 ③ 的
char buf[] = "hello";改成const char *msg = "hello";再const_cast→ 可能看起来也没事(因为oldPrint不改它)。但如果oldPrint里写了一个字符,就可能崩 ——"看起来没事"是这类 UB 最坏的地方。 - 加编译选项
-Wold-style-cast(Dev-C++:工具 → 编译选项 → 在命令行里加),把自己写过的一个.cpp扫一遍 ——看看用了几次 C 风格强转。
常见误区
- "
static_cast和 C 风格强转一样" ——不一样:static_cast只做"合理的那一类转换",做不到的会直接编译错误(比如去掉const、无关类型互转)。C 风格强转是"四把都试一遍",你根本不知道最后用了哪把。 - "
static_cast向下转换是安全的,因为它能编过" ——能编过只代表"类型之间有关系"(父子),不代表"这个对象真的是子类"。 错了就是未定义行为(Demo ⑤)。 - "
dynamic_cast比static_cast慢,所以能不用就不用" ——看场合:已经保证过类型的地方(比如刚判过typeid),用static_cast没问题;但在"探测"的场合,用static_cast相当于拆了安全带。性能焦虑应该留给热点路径,不是留给初次写对的代码。 - "
const_cast是"改 const 变量"的合法方式" ——它只是"合法地去掉 const 这个标签"。对真正const的对象动手仍是未定义行为(可能崩、可能改了没效果)。 - "
reinterpret_cast是万能转换,什么都能转" ——是"什么都能编过",不是"什么都能对"。它不做任何调整,所以:多重继承会错、平台一换就错、对齐不对会崩。 - "
reinterpret_cast可以用来做父子转换" ——大错:它不做偏移调整,而多重继承下基类子对象就在别的偏移处(14.1.3)。父子转换老老实实用static_cast/dynamic_cast。 - "箭头只能有一个方向" ——四把扳手不是替代关系,是分工:
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` 就是现场)。- ☐ 下一节(14.2 异常处理)
相关笔记
- RTTI与多重继承 —— 上一节:为什么
reinterpret_cast在多继承下必错 - 异常处理 —— 下一节:本章的另一半
- 转换运算符 —— 隐式转换(自动挡)与本节(手动挡)的区别
- 表达式中的类型转换 ——
(double)a/b与(double)(a/b)的那个坑 - 指向空的指针与空类型指针 ——
void *是万能口袋 - 🔷 C++基础 · 索引 —— 全课路线图