C 风格的 (T)expr 一个写法背后可能是四种完全不同的操作:数值转换、类层次导航、去除 const、位模式重解释。编译器遇到它时会按固定顺序依次尝试 const_caststatic_caststatic_castconst_castreinterpret_castreinterpret_castconst_cast,第一个能编译通过的就生效——写的人未必知道实际发生的是哪一种,读的人更无从判断风险,也没法用 grep 找出代码里所有的转换点。

C++ 把这四种语义拆成四个操作符,本质是要求程序员显式声明转换意图。意图不同,安全性、开销、审查方式完全不同:

  • static_cast:类型之间的值如何映射?——编译期的良定义转换
  • dynamic_cast:这个基类指针实际指向谁?——向运行期要答案
  • const_cast:cv 限定符能不能去掉?——唯一的 cv 后门
  • reinterpret_cast:这段内存换个视角看是什么?——位模式重解释

static_cast:编译期的良定义转换

static_cast 执行语言规则本身就允许的转换:数值类型互转、void* 往返、继承体系上溯、调用显式构造函数和转换运算符。全部工作在编译期完成,零运行时开销;编译器只检查转换是否合法,不保证结果是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Meter {
explicit Meter(double v) : v_(v) {}
double v_;
};

struct Base { virtual ~Base() = default; };
struct Derived : Base {};

double pi = 3.14;
int n = static_cast<int>(pi); // 数值截断:3

Derived d;
Base* bp = static_cast<Base*>(&d); // 上溯:恒安全

void* vp = static_cast<void*>(&d);
auto* dp = static_cast<Derived*>(vp); // void* 往返

Meter m = static_cast<Meter>(3.5); // 调用 explicit 构造

需要留意的是 static_cast 也能做下溯(基类指针转派生类指针),前提是程序员自己保证对象确实是目标类型——猜对了零开销,猜错了就是未定义行为。这正是它和 dynamic_cast 的分界线:下溯时类型已知用 static_cast,类型未知必须 dynamic_cast

它做不了的事:去除 const(归 const_cast),无关联指针类型互转(归 reinterpret_cast)。

dynamic_cast:向运行期要答案

四种 cast 中唯一带运行期检查的。它借助 RTTI 回答一个问题:这个基类子对象实际上是不是目标类型?指针版本失败返回 nullptr,引用版本失败抛 std::bad_cast

1
2
3
4
5
6
7
8
9
10
struct Base { virtual ~Base() = default; };
struct Derived : Base {};

void handle(Base* p) {
if (auto* d = dynamic_cast<Derived*>(p)) {
// p 确实指向 Derived(或其派生类)
} else {
// 转换失败:p 指向其他类型
}
}

三个前提与代价:

  • 要求多态类型:类中至少有一个虚函数,否则编译失败——RTTI 挂在虚表上。
  • 有运行时开销:需要在继承图中查询,远比 static_cast 慢。
  • 引用版本必须处理异常std::bad_cast 无法静默忽略。

dynamic_cast 有一项独占能力:在多继承或虚继承体系中做交叉转换(cross-cast),即从一个基类子对象转到另一个无继承关系的基类子对象。这类转换编译期无法静态建立,只能靠运行期查询。

工程上的经验法则:dynamic_cast 零星出现是工具,成片出现是设计信号——本该用虚函数分派或 visitor 的地方,在用类型判断硬分支。

const_cast:唯一的 cv 后门

const_cast 只能做一件事:加、去 constvolatile 限定符,不能改变任何底层类型。它的合法场景基本只有两类。

对接没有标注 const 的旧接口。旧函数承诺只读但签名没写 const,去限定符只为通过编译:

1
2
3
4
5
6
7
8
#include <string>

void legacy_log(char* msg); // 旧接口:只读,但未标 const

void log(const std::string& s) {
// 去掉限定符只为调用,被调方绝不能写
legacy_log(const_cast<char*>(s.c_str()));
}

const 重载之间的委托。经典的 const / 非 const 成员函数去重手法:const 版本把 this 去限定后复用非 const 实现,再把结果加回 const,逻辑只写一份:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <cstddef>
#include <vector>

class Buffer {
std::vector<int> data_;
public:
int& at(std::size_t i) { return data_[i]; } // 真正实现

const int& at(std::size_t i) const {
return const_cast<const int&>(
const_cast<Buffer*>(this)->at(i));
}
};

危险点只有一个,但必须牢记:修改本体为 const 的对象是未定义行为。判断依据是对象的定义,而非指针的类型——const int x = 0; 哪怕骗过编译器拿到 int*,写它就是 UB;反过来,对象本体并非 const,通过 const 指针去限定后再写是合法的。

reinterpret_cast:位模式重解释

最不安全的转换:编译器不做任何检查,无条件按程序员的要求把位模式换个类型解释。它能做指针互转、指针与整数互转、函数指针互转,其余一概不管。

合法用途集中在底层编程:序列化与网络协议中的字节视图char*unsigned char* 别名本就是规则允许的)、用 uintptr_t 暂存指针值、placement new 的原始缓冲区:

1
2
3
4
5
6
7
8
9
10
11
12
#include <cstdint>
#include <new>

alignas(long) unsigned char buf[sizeof(long)];
// placement new 复用既有内存,无需 delete
long* p = new (buf) long{42};

// 以字节视图检查内存布局
auto* bytes = reinterpret_cast<unsigned char*>(p);

// 指针与整数互转(调试输出、句柄设计常用)
auto addr = reinterpret_cast<std::uintptr_t>(p);

使用时记住三条边界:

  • 转出去的指针唯一安全的用法是转回原类型再用;直接以新类型解引用,大多违反别名规则或对齐要求。
  • 函数指针转换同理:可以转,但只能转回原签名后调用,中间形态不可调用。
  • 结果依赖平台(指针宽度、字节序、对齐),可移植性全靠自己保证。

除非在写驱动、协议栈、序列化层,否则看到 reinterpret_cast 的第一反应应该是:有没有别的办法。

如何选择

flowchart TB
  Q{"转换意图"} --> A["去除 const/volatile"]
  Q --> B["无关联类型<br/>位级重解释"]
  Q --> C{"类层次导航"}
  A --> A1["const_cast"]:::cast
  B --> B1["reinterpret_cast"]:::cast
  C -->|"向上,或下溯且类型已知"| C1["static_cast"]:::cast
  C -->|"下溯,需运行期确认"| C2["dynamic_cast"]:::cast
  Q --> D["数值转换 / 显式构造"] --> C1

  classDef cast fill:#e3f2fd,stroke:#1565c0
  classDef intent fill:#fff3e0,stroke:#ef6c00
  class A,B,D intent
cast回答的问题安全性运行时检查典型场景
static_cast值如何映射数值转换、上溯、显式构造
dynamic_cast实际指向谁多态下溯、交叉转换
const_castcv 能否去掉旧接口对接、const 委托
reinterpret_cast位模式怎么读极低字节视图、指针整数互转

工程实践的优先级:

  1. 默认 static_cast:意图明确、零开销,能编译就是良定义。
  2. 多态下溯用 dynamic_cast:为运行时确认付费,别拿 static_cast 赌类型。
  3. const_cast 只用于接口兼容与 const 委托:出现其他用途,多半是设计有问题。
  4. reinterpret_cast 留给底层代码:业务代码里出现就该追问原因。

另外值得单独一提:std::any_cast 用于从 C++17 的 std::any 中取回值,它与这四个 cast 并非一家,而是类型擦除容器的配套工具,详见 C++17 any 与 variant