文章目录
C++的类型转换
C语言中的类型转换
C语言在赋值、函数传参、返回值接收等场景中,如果左右两侧类型不一致,就可能发生类型转换。C语言把类型转换分为两类:隐式类型转换 和显式类型转换 。
隐式转换由编译器在编译阶段自动完成,显式转换由程序员通过强制转换语法完成。 C风格转换的核心问题是"转换形式统一但语义不明确",容易把精度丢失、指针截断、去除 const 等风险混在一起,导致代码难以审查和排错。
c
#include <iostream>
#include <cstdio>
int main()
{
int i = 42;
double d1 = i; // 隐式类型转换:int -> double
int x = 12.98; // 隐式类型转换:double -> int,小数部分被截断
unsigned int u = 10;
int y = -1;
if (y < u)
{ // 有符号与无符号比较,y 可能被转换为很大的无符号数
std::cout << "y < u 成立" << std::endl;
}
else
{
std::cout << "y < u 不成立" << std::endl;
}
std::cout << "d = " << d1 << ", x = " << x << std::endl;
double d2 = 12.34;
int a = (int)d2; // C风格显式转换:double -> int
int value = 7;
int *p = &value;
int address = (int)p; // C风格显式转换:指针 -> int,可能发生截断
std::cout << "a = " << a << ", address = " << address << std::endl;
return 0;
}
我们看上述代码,我们能发现C语言的隐式类型转换问题其实挺大的。
那么为什么全部要求显示类型转换不就行了。
- 确实全部使用显示类型转换可以解决上述隐式类型转换的问题,但是C语言的显示类型转换语法太简单了,
(类型)变量;就构成类型转换,这就会导致我们有时漏写或忘写的情况。通常这种忘写和漏写影响不是很大因为显示类型转换是要经过思考过后才决定的事,但是这还是会对后续代码进行维护留下了隐患。
为什么C++需要四种类型转换
C++ 在兼容 C风格转换的同时,引入了四种类型转换分别是:static_cast、reinterpret_cast、const_cast、dynamic_cast 。
那么为什么C++要将强制类型转换搞的这么复杂呢?
- 因为C++ 需要四种命名转换,是因为类型转换的语义差异很大:数值转换 、位模式重解释 、去除 const 、继承体系中的安全向下转型,风险和检查规则完全不同。如果继续使用 C风格统一语法,代码审查者无法快速判断转换意图,编译器也难以针对不同转换做不同检查。让用户将意图写出来。
C风格转换的问题与C++设计动机
C风格转换主要有两个问题:第一,隐式转换可能悄悄丢精度或引发符号问题; 第二,显式转换把所有情况混在一起,代码不够清晰 。C++ 命名转换的设计动机是"按语义分类",例如 static_cast 表示相关类型间的静态转换,reinterpret_cast 表示低层位模式重解释,const_cast 表示只处理 const/volatile 属性,dynamic_cast 表示运行时检查的安全向下转型。这样写出来的代码更容易搜索、审查和定位错误。
cpp
int main()
{
double d = 12.34;
int a = (int)d; // C风格:看不出是普通数值转换
int b = static_cast<int>(d); // C++风格:明确是静态数值转换
const int c = 10;
int* p = (int*)&c; // C风格:看不出去了 const
int* q = const_cast<int*>(&c); // C++风格:明确只去 const
std::cout << a << " " << b << " " << *p << " " << *q << std::endl;
return 0;
}
注意:C++的强制类型转换并不是"万能药",它只是让意图更清晰;例如 reinterpret_cast 和 const_cast 仍然可能产生未定义行为。
C++强制类型转换
static_cast
static_cast 用于相关类型之间的静态转换,例如基本数值类型转换、void* 与具体指针类型转换、继承体系中基类与派生类指针/引用的相关转换等。它由编译器在编译期根据类型关系进行检查,不做运行时类型检查。它不能用于两个完全不相关的指针类型互转,也不能去除 const 属性 。它的优点是语义比 C风格转换清晰,但向下转型时如果实际对象不是目标派生类,仍然可能产生未定义行为。
cpp
int main()
{
double d = 12.34;
int a = static_cast<int>(d); // 基本类型转换,小数部分截断
std::cout << "a = " << a << std::endl;
void *vp = &a;
int *ip = static_cast<int *>(vp); // void* -> 具体指针类型
std::cout << "*ip = " << *ip << std::endl;
char *cp = new char('c');
short *sp = static_cast<short*>(cp); // 报错:类型转换无效
return 0;
}
static_cast 的问题就是不做运行时检查,尤其用于继承体系向下转型时,如果父类指针实际指向父类对象,强转为子类指针后使用可能崩溃或产生未定义行为。
reinterpret_cast
reinterpret_cast 用于低层次的位模式重新解释,通常把一种类型的位模式解释成另一种类型,例如指针与足够宽的整数互转、不同指针类型之间的重解释。它不进行类型安全检查,也不保证转换后的对象可合法访问。它的使用场景主要是底层系统编程、序列化、硬件地址操作等。普通业务代码应尽量避免使用。
cpp
#include <cstdint>
int main()
{
int a = 42;
int* p = &a;
uintptr_t addr = reinterpret_cast<uintptr_t>(p); // 指针 -> 足够宽的整数
int* q = reinterpret_cast<int*>(addr); // 整数 -> 指针
std::cout << "*q = " << *q << std::endl;
double d = 3.14;
// int* bad = reinterpret_cast<int*>(&d); // 可以编译,但通过 bad 访问违反严格别名规则
return 0;
}
reinterpret_cast的缺陷是不检查对齐、不检查对象真实类型,把整数随便转成指针再解引用很可能崩溃。
这个缺陷就导致用了reinterpret_cast的代码移植到其他平台时是一件很危险的事情,因为其结果依赖平台、指针宽度、对齐和对象表示,跨平台代码要非常谨慎。
const_cast
const_cast 最常用的用途是去除变量的 const 或 volatile 属性,方便在特定接口中传递指针或引用。它只改变类型系统中的 const/volatile 限定,不改变对象本身是否真的是 const。也就是说,如果原对象本身定义为 const,通过 const_cast 去掉 const 后修改它,仍然属于未定义行为。它适合用于"对象本身不是 const,只是当前引用/指针是 const"的场景。
错误用法:
cpp
int main()
{
const int a = 2;
int* p = const_cast<int*>(&a); // 去掉 const
*p = 3; // 未定义行为:a 本身是 const 对象
std::cout << "a = " << a << ", *p = " << *p << std::endl;
return 0;
}
正确用法:
cpp
void oldApi(int* p)
{
*p = 100;
}
int main()
{
int x = 2;
const int& cr = x; // x 本身不是 const 对象
oldApi(const_cast<int*>(&cr)); // 去掉的是引用带来的 const 限定
std::cout << "x = " << x << std::endl;
return 0;
}
为什么const_cast移除了const属性,对其修改仍是未定义行为?
- 因为const属性是从该变量定义的时候就注定了它不能修改,const_cast只是去掉了const这个属性。比如:const int* 使用const_cast就变成了int* ,但是能否修改就看最初的变量是否是const属性的,如果不是就可以修改。因为const从定义开始对const属性变量修改操作就是未定义行为。
为什么C++要设计const_cast这个可以说没什么用的类型转换关键字?
- const_cast,设计的初衷是对接缺 const 的老 API;就是以前的接口是有const属性的但是没写;
- 第二个用法就是复用 const/非 const 重载的实现。
dynamic_cast
dynamic_cast 用于继承体系中的安全向下转型:把父类指针/引用转换为子类指针/引用,并在运行时检查实际对象类型是否匹配。向上转型(子类到父类)本来就可以赋值兼容,不需要 dynamic_cast;向下转型(父类到子类)使用 dynamic_cast 更安全。它要求父类至少含有一个虚函数,因为运行时类型识别依赖虚表等机制。指针转换失败返回 nullptr,引用转换失败抛出 std::bad_cast。
cpp
class A
{
public:
virtual ~A() {} // 有虚函数,支持多态和 dynamic_cast
};
class B : public A
{
public:
void hello()
{
std::cout << "B::hello" << std::endl;
}
};
void fun(A* pa)
{
B* pb1 = static_cast<B*>(pa); // 不做运行时检查,可能得到非法指针
B* pb2 = dynamic_cast<B*>(pa); // 运行时检查,失败返回 nullptr
std::cout << "pb1 = " << pb1 << ", pb2 = " << pb2 << std::endl;
if (pb2 != nullptr) {
pb2->hello();
}
}
int main()
{
A a;
B b;
fun(&a); // dynamic_cast 失败,pb2 为 nullptr
fun(&b); // dynamic_cast 成功
return 0;
}
注意:
dynamic_cast只能用于父类含有虚函数的类;如果父类没有虚函数,编译会失败。static_cast向下转型不做运行时检查,如果父类指针实际指向父类对象,强转后使用是未定义行为;正确做法是使用dynamic_cast并检查结果。dynamic_cast有运行时开销,不应滥用;如果设计上可以避免向下转型,优先使用虚函数和多态接口。
RTTI
RTTI 是 Run-Time Type Identification 的简称,即运行时类型识别。
C++通过以下方式来支持RTTI:
- typeid运算符
- dynamic_cast运算符
- decltype
typeid 运算符、dynamic_cast 运算符等机制支持运行时类型识别;
typeid 可以获取表达式的动态类型信息;
dynamic_cast 可以在运行时检查继承体系中的转换是否安全。需要注意;
decltype 是编译期类型推导工具,严格来说不属于运行时类型识别。
RTTI了解即可
总结:
| 需求 | 推荐方式 | 说明 |
|---|---|---|
| 基本数值类型转换 | static_cast |
明确表达静态数值转换意图,比C风格更清晰 |
void*转具体指针 |
static_cast |
相关指针类型转换,编译期检查 |
| 指针与整数互转、底层位模式重解释 | reinterpret_cast |
仅在底层场景使用,必须确保地址有效、对齐正确 |
去除const/volatile限定 |
const_cast |
确认原对象本身不是const,否则修改属于未定义行为 |
| 多态继承体系安全向下转型 | dynamic_cast |
父类需有虚函数;必须检查nullptr或捕获std::bad_cast |
| 普通业务代码中的强制转换 | 优先避免,必要时用C++命名转换 | C风格转换语义混合、可读性差,不推荐在新C++代码中使用 |
| 兼容C语言或旧代码 | 可保留C风格转换 | C++兼容C风格,但新代码应优先使用命名转换 |
完