C++类型转换
本节解决什么问题
C++ 中经常会遇到“把一种类型当成另一种类型使用”的场景,例如:
- 把
double转成int。 - 把
enum class转成整数。 - 把基类指针安全地转成派生类指针。
- 临时去掉
const限定,适配旧接口。 - 在底层代码里把指针、整数、字节视图互相转换。
C 风格强转写法 (目标类型)表达式 能做很多事,但问题也在这里:它太“万能”,读代码的人很难一眼看出这次转换到底是普通数值转换、继承层级转换、去掉 const,还是危险的底层重解释。
C++ 提供了四个更明确的类型转换运算符,让转换意图写在代码里。
隐式转换和显式转换
类型转换可以分为隐式转换和显式转换。
隐式转换
隐式转换是由编译器自动完成的转换,代码中不需要写出转换操作。例如:
int count = 10;
double result = count;
这里 count 是 int,而 result 是 double。编译器会自动把整数 10 转换成浮点数 10.0,这就是隐式转换。
隐式转换写起来方便,但有些转换可能造成数据丢失:
double price = 19.8;
int whole_price = price;
这段代码通常可以通过编译,但 whole_price 得到的是 19,小数部分会被丢弃。因为转换没有明确写出来,阅读代码时也容易忽略它。
显式转换
显式转换是程序员在代码中明确要求进行的转换。例如:
double price = 19.8;
int whole_price = static_cast<int>(price);
这里的 static_cast<int> 明确表示:程序员知道 price 会被转换成 int,也接受小数部分被丢弃。
两者的区别可以简单理解为:
- 隐式转换:编译器根据上下文自动转换。
- 显式转换:程序员在代码中明确写出要转换成什么类型。
隐式转换并不一定不安全,显式转换也不代表一定安全。显式写出转换的主要价值,是让转换意图更清楚,并让编译器按照指定的转换方式进行检查。
下面介绍的 static_cast、dynamic_cast、const_cast 和 reinterpret_cast,都是 C++ 提供的显式类型转换。
这个特性是什么
C++ 的四种显式类型转换是:
| 转换方式 | 主要用途 | 安全程度 |
|---|---|---|
static_cast<T>(expr) | 常规类型转换,如数值、枚举、父子类上行转换 | 常用,编译期检查 |
dynamic_cast<T>(expr) | 多态类型的安全向下转换 | 运行期检查,失败返回 nullptr 或抛异常 |
const_cast<T>(expr) | 添加或移除 const / volatile 限定 | 要非常克制 |
reinterpret_cast<T>(expr) | 底层二进制重解释,如指针和整数互转 | 最危险,少用 |
原则:能不用强转就不用;必须强转时,优先选择语义最窄、最能说明意图的转换。
C++ 标准版本
C++98 就已经提供这四种转换写法,实际工程中依然推荐使用它们替代 C 风格强转。
需要的头文件
类型转换本身不需要额外头文件。示例代码使用到的库需要对应头文件,例如:
#include <cstdint>
#include <iostream>
运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。
基本语法
目标类型 value = static_cast<目标类型>(表达式);
目标类型 value = dynamic_cast<目标类型>(表达式);
目标类型 value = const_cast<目标类型>(表达式);
目标类型 value = reinterpret_cast<目标类型>(表达式);
运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。
示例代码
示例 1:static_cast 处理常规转换
static_cast 适合明确、常规、编译期能检查的转换。比如数值转换、enum class 转整数、父类指针指向子类对象等。
#include <iostream>
enum class Status
{
ok = 200,
not_found = 404
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
double score = 89.7;
int integer_score = static_cast<int>(score);
Status status = Status::not_found;
int status_code = static_cast<int>(status);
std::cout << "integer_score = " << integer_score << "\n";
std::cout << "status_code = " << status_code << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
运行结果:
integer_score = 89
status_code = 404
示例 2:dynamic_cast 安全判断真实对象类型
dynamic_cast 常用于多态基类。向下转换时,它会在运行期检查对象真实类型。
#include <iostream>
struct Animal
{
virtual ~Animal() = default;
};
struct Cat : Animal
{
void meow() const
{
std::cout << "cat: meow\n";
}
};
struct Dog : Animal
{
};
void try_meow(Animal* animal)
{
Cat* cat = dynamic_cast<Cat*>(animal);
if (cat != nullptr)
{
cat->meow();
}
else
{
std::cout << "not a cat\n";
}
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
Cat cat;
Dog dog;
try_meow(&cat);
try_meow(&dog);
// 返回 0 表示程序正常结束。
return 0;
}
运行结果:
cat: meow
not a cat
注意:
dynamic_cast用在指针上,转换失败会得到nullptr;用在引用上,转换失败会抛出std::bad_cast。它要求基类至少有一个虚函数,通常是虚析构函数。
示例 3:const_cast 只改变 const 限定
const_cast 只能添加或移除 const / volatile 限定,不能把 int 转成 double,也不能把两个不相关的类型互相转换。
#include <iostream>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
int value = 10;
const int& readonly_ref = value;
int& writable_ref = const_cast<int&>(readonly_ref);
writable_ref = 20;
std::cout << "value = " << value << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
运行结果:
value = 20
这个例子里,value 本身是一个普通的 int 变量,并不是 const int。
int value = 10;
const int& readonly_ref = value;
这两行代码的意思是:readonly_ref 是 value 的一个“只读引用”。它并没有创建一个新的变量,而是给 value 起了一个只能读取、不能修改的别名。
也就是说:
readonly_ref
本质上还是指向原来的 value。
接下来:
int& writable_ref = const_cast<int&>(readonly_ref);
这行代码使用 const_cast 去掉了 readonly_ref 上的 const 限定,让它重新变成一个可以修改的引用。由于它引用的还是原来的 value,所以:
writable_ref = 20;
本质上就是在修改:
value = 20;
因此最后输出的是:
value = 20
需要注意的是,这个例子能够修改成功,是因为原始对象 value 本身不是 const,只是我们临时用 const int& 这种只读方式去引用它。
如果原始对象本身就是 const,再用 const_cast 强行修改就是未定义行为,例如:
const int value = 10;
const int& readonly_ref = value;
int& writable_ref = const_cast<int&>(readonly_ref);
writable_ref = 20; // 未定义行为
所以可以简单记住:
const_cast 只是把“只读视角”改回“可写视角”,它不会复制出新变量,也不会真的把一个天生的常量安全地变成普通变量。
示例 4:reinterpret_cast 做底层重解释
reinterpret_cast 表示“把这段二进制数据按另一种类型解释”。它绕过了很多类型系统保护,通常只应该出现在非常底层的代码里。
比如把一个指针转换成整数形式的地址,再把这个整数地址转换回指针。
reinterpret_cast 可以用来做一些比较“底层”的类型转换,比如把一个指针转换成整数形式的地址,再把这个整数地址转换回指针。
需要注意的是,这里转换的是地址,不是变量里面保存的值。
#include <cstdint>
#include <iomanip>
#include <iostream>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
int value = 42;
int* p = &value;
std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);
int* again = reinterpret_cast<int*>(raw);
std::cout << "value = " << value << "\n";
std::cout << "p = " << p << "\n";
std::cout << "raw = " << raw << "\n";
std::cout << "raw(hex) = 0x" << std::hex << raw << std::dec << "\n";
std::cout << "again = " << again << "\n";
std::cout << "*again = " << *again << "\n";
std::cout << std::boolalpha;
std::cout << "same pointer = " << (p == again) << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
运行结果示例:
value = 42
p = 0x7ffd60a576d4
raw = 140726224910036
raw(hex) = 0x7ffd60a576d4
again = 0x7ffd60a576d4
*again = 42
same pointer = true
其中,具体的地址值每次运行都可能不同,所以你的运行结果不一定和上面完全一样。
这段代码里:
int value = 42;
int* p = &value;
value 是一个普通的 int 变量,里面保存的值是 42。
p 是一个 int* 指针,里面保存的是 value 的地址,而不是 value 的值。
也就是说,p 里面存的不是:
42
而是类似这样的地址:
0x7ffd60a576d4
接着:
std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);
这行代码把指针 p 转换成了一个整数。
std::uintptr_t 是一种无符号整数类型,它的作用是尽可能安全地保存一个指针地址。
所以如果:
p = 0x7ffd60a576d4
那么 raw 保存的就是这个地址对应的整数形式。
例如:
raw = 140726224910036
这只是同一个地址的十进制写法。
下面这行代码:
std::cout << "raw(hex) = 0x" << std::hex << raw << std::dec << "\n";
是把 raw 按十六进制打印出来。这样就可以看到,raw 的十六进制形式和指针 p 打印出来的地址基本是一样的。
然后:
int* again = reinterpret_cast<int*>(raw);
这行代码又把整数形式的地址 raw 转回了 int* 指针。
也就是说:
p 指向 value
again 也指向 value
所以:
std::cout << "*again = " << *again << "\n";
这里的 *again 就是在访问 again 指向的变量。
因为 again 重新指向了原来的 value,所以:
*again
等价于:
value
因此输出结果是:
*again = 42
最后:
std::cout << "same pointer = " << (p == again) << "\n";
这行代码比较的是 p 和 again 这两个指针保存的地址是否相同。
由于它们都指向 value,所以结果是:
same pointer = true
可以把整个过程理解成:
value 是一个变量,值是 42
p 保存 value 的地址
raw 把这个地址用整数形式保存下来
again 又把这个整数地址转换回指针
所以 again 重新指向 value
所以这段代码的重点不是把 42 转成整数,而是把 value 的地址转成整数。
简单记忆:
int* p = &value;
表示:
拿到 value 的地址
std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);
表示:
把地址当成整数保存
int* again = reinterpret_cast<int*>(raw);
表示:
把整数地址再变回指针
*again
表示:
通过这个地址找到原来的 value
一句话总结:
reinterpret_cast 在这个例子里转换的是“地址的表示形式”,不是变量里面的值。value 的值是 42,而 raw 保存的是 value 的地址。
四种转换怎么选
| 需求 | 推荐写法 | 说明 |
|---|---|---|
double 转 int | static_cast<int>(x) | 明确可能丢失小数 |
enum class 转整数 | static_cast<int>(e) | 枚举类不会隐式转整数 |
| 基类指针转派生类指针,并且不确定真实类型 | dynamic_cast<Derived*>(p) | 转换失败可判断 nullptr |
去掉 const 适配旧接口 | const_cast<T*>(p) | 前提是旧接口不会修改真正的 const 对象 |
| 指针和整数互转、底层字节解释 | reinterpret_cast<T>(x) | 非常底层,优先避免 |
类型转换 对比 C风格强转 会增加运行开销吗
static_cast<T>(expr) 中虽然使用了尖括号,看起来有些像模板,但它不是模板,而是 C++ 语言内置的类型转换运算符。这里的 T 表示要转换成的目标类型。
与功能相同的 C 风格强转相比,C++ 的显式类型转换通常不会产生额外的运行开销:
| 转换方式 | 通常的运行开销 |
|---|---|
static_cast | 不会因为使用这种写法而增加额外开销 |
dynamic_cast | 需要在运行时检查对象的真实类型,可能产生时间和程序体积开销 |
const_cast | 通常只改变编译器进行类型检查的方式,没有运行时操作 |
reinterpret_cast | 通常只改变编译器解释数据的方式,没有额外转换步骤 |
例如,下面两种写法表达的是同一种数值转换:
double value = 3.14;
int a = (int)value;
int b = static_cast<int>(value);
编译器通常会为它们生成相同或等价的机器指令。static_cast 的优势不是运行得更快,而是转换意图更加明确,并且能够得到更严格的编译期检查。
不过,需要区分“转换写法的开销”和“转换操作本身的开销”。例如,把浮点数转换成整数可能需要执行实际的处理器指令:
double value = 3.14;
int result = static_cast<int>(value);
这里可能产生的运行开销来自“把 double 转换成 int”这个操作,而不是来自 static_cast 语法。即使改成隐式转换或 C 风格强转,这个转换操作仍然存在。
因此可以简单记住:选择 C++ 的显式类型转换,主要是为了让类型检查更严格、代码意图更清楚;除了需要运行时检查的 dynamic_cast,它通常不会因为写法不同而比 C 风格强转更慢。
C风格强转为什么不推荐
C 风格强转写起来短:
int n = (int)3.14;
运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。
更推荐写成:
int n = static_cast<int>(3.14);
运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。
C 风格强转可能在背后组合多种转换能力,既可能像 static_cast,也可能像 const_cast,甚至接近 reinterpret_cast。代码越底层、类型越复杂,这种“不说明白”的风险就越大。
常见错误
用 static_cast 做不安全向下转换
Animal* animal = new Dog;
Cat* cat = static_cast<Cat*>(animal); // 危险:编译可能通过,但真实对象不是 Cat
运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。
如果不确定真实类型,应该使用 dynamic_cast。
修改真正的 const 对象
const int value = 10;
int& ref = const_cast<int&>(value);
ref = 20; // 未定义行为
运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。
滥用 reinterpret_cast
double d = 3.14;
int* p = reinterpret_cast<int*>(&d); // 危险:把 double 对象当 int 对象访问
运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。
使用建议
- 普通转换优先用
static_cast。 - 多态向下转换优先用
dynamic_cast,并检查失败情况。 const_cast只用于适配旧接口,不要用它修改真正的const对象。reinterpret_cast只放在非常底层、边界清晰的代码里,并集中封装。- 不要为了少打几个字使用 C 风格强转。
小结
static_cast:常规转换,最常用。dynamic_cast:多态类型安全向下转换。const_cast:只改变const/volatile限定。reinterpret_cast:底层重解释,风险最高。
一句话记忆:能不用就不用;必须用时,把转换意图写清楚。