13 【C++】C++的类型转换
文章目录
- 前言
- 1.为什么C++需要四种类型转换
- 2.C++的类型转换
- 2.1 static_cast
- **static_cast的安全检查:**
- **允许相关的/安全的类型转换**
- **阻止不相关的/不安全的类型转换**
- 2.2 reinterpret_cast
- 2.3 const_cast
- const_cast的安全检查:
- 只允许修改 const/volatile属性,且必须是指针/引用类型
- 不允许其他的类型转换和非指针引用的转换
- 2.4 dynamic_cast
- dynamic_cast的运行时类型转换检查 (以指针向下转型为例)
- 总结
前言
C语言中的类型转换
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。
- 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
- 显式类型转化:需要用户自己处理
到目前为止我们的所有代码,使用的也都是C语言的类型转换,
void Test()
{int i = 1;// 隐式类型转换double d = i;printf("%d, %.2f\n", i, d);int* p = &i;// 显示的强制类型转换int address = (int)p;printf("%p, %d\n", p, address);
}
缺陷:
转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换
1.为什么C++需要四种类型转换
C 风格转换过于“强大”且危险: (type)expression这种写法可以执行多种本质上不同的转换操作(如数值转换、指针类型转换、去除 const、多态类型转换等),编译器不会检查转换是否合理或安全。程序员需要自己承担风险。
如以下类型转换的坑:
场景1:
在while比较时,如果pos是0,end就算减到了负数,整个程序也不会停下,因为运算符比较的时候发生了隐式类型转换,编译器用end创造了一个临时的size_t类的变量,而我们的size_t变量的是没有负数的,所以这个end(实际是负的,但是转换成的size_t类临时是极大的)永远会>=0,所以这个循环永远不会停止.
场景2:
以下代码不可以:
int n = 10;
int arr[n]; //表达式的计算结果不是常数
这种定义叫变长数组,是C99中的一个特性之一,但是我们的vs系列中一直都没有支持.
下面代码可以吗?
const int n = 10;
int arr[n];
可以,但是其实它被替换成了常量,但是n它其实是一个常变量,只是经过了编译器的隐式类型转换。
它不能直接修改,但是可以间接修改:
int main()
{const int n = 10;int* p = (int*)&n;*p++;cout << n << endl;cout << *p << endl;return 0;
}
以上代码有几个问题:
-
编译器可能将n直接替换为字面量10,当读取n时,实际读取的是编译器缓存的常量值(而非内存中的值),导致cout << n输出10,而*p实际指向的内存值已被改为11。
对此可以使用volatile关键字修饰变量:
volatile const int n = 10;
这样可以让编译器强制去内存取,禁用了宏替换,于是打印出来的就只能是改过的值。
-
内存模型欺骗:
const int n = 10; // 编译器可能将n放入只读内存段 int* p = (int*)&n; // 通过指针强制访问 *p = 20; // 试图修改只读内存 -> 程序崩溃!
可能触发段错误
-
修改声明为const的对象是未定义行为(UB)
- 具体表现取决于编译器实现:
- 可能正常修改(如您的示例)
- 可能输出旧值(编译器优化)
- 可能程序崩溃(内存保护)
- 可能引发其他任意行为
- 具体表现取决于编译器实现:
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast、reinterpret_cast、const_cast、dynamic_cast
C++的新操作符限制转换范围,读者(包括未来的你和其他开发者)能立即理解程序员进行该转换的目的是什么。
static_cast: “我知道这个数值转换/相关类层次转换应该是安全的,或者是我需要的标准转换。”
dynamic_cast: “我想在运行时安全地检查这个对象是否属于某个派生类并转换。”
const_cast: “我需要临时移除这个对象的 const或 volatile属性(通常用于与旧 API 交互,需谨慎)。”
reinterpret_cast: “我需要把这块内存当作完全不同的类型来解释(例如处理硬件寄存器、序列化),我知道这很危险且不可移植。”
注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
2.C++的类型转换
类型之间的转换是需要我们的数据之间有一点关联的(相近类型的),正如一般自定义类型是不能转换的,除了两类在单参数的构造函数有关联,如:
如果想禁用这个转换,我们可以在构造函数这里加上explicit。
2.1 static_cast
static_cast是最常用的类型转换,它是静态转换,用于两个相关类型之间的装换,不可用于多态类型的转换(比C风格的转换更安全),编译器隐式执行的任何类型转换都可用static_cast。
int main()
{double d = 12.34;int a = static_cast<int>(d);cout<<a<<endl;return 0;
}
-
静态类型转换,即static_cast的所有类型检查、转换规则和最终生成的转换代码,都是在编译阶段由编译器确定和完成的。
因为它是静态类型转换,所以在相关类型转换中特殊转换情况(继承体系向下转换)下,它不进行运行时检查,依赖程序员保证正确性。所以我们在做继承体系下的类型转换时,还是使用dynamic_cast,因为安全的继承体系向下转型是 dynamic_cast的工作。 -
相比与C风格的转换它更安全,因为编译器会根据源代码中提供的类型信息(源类型和目标类型),应用 C++ 标准中定义的规则来判断这个转换是否合法。
-
它用于编译器已知且允许的转换,所以编译器隐式执行的任何类型转换都可用static_cast。
static_cast的安全检查:
允许相关的/安全的类型转换
static_cast是用于相关类型之间的转换的,编译器会在编译阶段检查使用static_cast转换的合法性和安全性,然后才生成对应的转换
- 基本类型转换(数值类型):
int main()
{int a = 42;double b = static_cast<double>(i); // 安全:int转double(无精度损失)double c = 42.01; int d = static_cast<double>(c); //waring: 从“double”转换到“int”,可能丢失数据return 0;
}
安全检查:编译器允许数值类型间的转换(如int→double),但会警告可能丢失精度的反向转换(如double→int)。
- 继承体系中的向上转换(派生类→基类)
我们知道继承体系,子类和父类是密切相关的(相关类型),且子类本来就有父类的成员(安全的转换),所以,向上转换,是安全的:
class Base {};
class Derived : public Base {};int main()
{ Derived d;Base* b = static_cast<Base*>(&d); // 安全:向上转换始终合法
}
安全检查:编译器验证继承关系,若Derived不是Base的子类,则报错。
- 枚举与整数类型转换:
enum Color { RED, GREEN };
int main()
{Color c = static_cast<Color>(1); // 安全:整数转枚举(GREEN)
}
安全检查:编译器确保整数在枚举定义的取值范围内(此处1是有效的)。
阻止不相关的/不安全的类型转换
- 不相关的指针类型转换:
int main()
{int x = 10;double* p = static_cast<double*>(&x); // 错误:int*与double*无关
}
安全检查:编译器直接报错,因为两种指针类型无继承关系。
- 去除const限定符:
int main()
{const int ci = 100;int* p = static_cast<int*>(&ci); // 错误:static_cast不能移除const
}
安全检查:必须使用const_cast移除const,static_cast会拒绝。
- 向下转换(基类→派生类)无检查:
class Base {};
class Derived : public Base {};
int main()
{Base base;Derived* d = static_cast<Derived*>(&base); // 编译通过,但运行时行为未定义!
}
安全漏洞:static_cast允许向下转换,但不验证对象实际类型 (若base非Derived对象,会引发未定义行为)。此时应使用dynamic_cast(需多态类)。
其实面对继承体系,不应该使用static_cast,上面例子我们可以得知,继承体系的向上、向下转换,static_cast就像是我们C式的类型转换一样,并没有安全检查,所以我们在面对继承体系的类型转换的时候,应该使用专门的dynamic_cast,static_cast只是对类型之间的相关性检查,对精度损失做告警。
2.2 reinterpret_cast
reinterpret_cast是 C++ 中最强大但也最危险的类型转换运算符,它允许在任意类型之间进行低级别的重新解释(reinterpretation)。它不进行任何编译时类型检查或运行时安全检查,直接操作底层比特模式,所以它可以进行任意不相关类型之间的转换。仅在完全理解后果且无其他替代方案时使用。
int main()
{double d = 12.34;int a = static_cast<int>(d); //相关类型的转换用:static_castcout << a << endl;// 这里使用static_cast会报错,应该使用reinterpret_cast//int *p = static_cast<int*>(a);int *p = reinterpret_cast<int*>(a); //这时不相关类型的转换就要用:reinterpret_castreturn 0;
}
注意: reinterpret_cast也是静态转换,也是在编译阶段就完成的。
reinterpret_cast类型转换,其实和我们C式类型转换在生成的机器码层面通常是完全相同的,之所以要引入它,是因为它相比 C 风格的(SomeType*)0xDEADBEEF
,C++ 版本的reinterpret_cast像危险警示标签。
reinterpret_cast本质是 C++ 为 C 风格转换中危险的那部分操作设计的“语法隔离区”。
在 C++ 中永远优先使用 static_cast/const_cast,把 reinterpret_cast当作和 C 风格强制转换一样的“最后手段”。
2.3 const_cast
用于对const常变量的类型转换,它也是静态转换,用于添加或者删除变量的const或volatile属性,但是只对最初非 const 定义的对象使用 const_cast移除const属性。
void main()
{int a = 2;const int* b = const_cast<const int*>(&a);int* p = const_cast<int*>(b);*p = 3;cout << a << endl;
}
-
它也是静态转换,也是在编译阶段就完成的。
-
不改变底层类型
const_cast不能用于不同类型之间的转换(如 int转 char)。如需此类转换,请用 reinterpret_cast。 -
仅限指针/引用
它只适用于指针、引用或成员指针类型。 -
只对最初非 const 定义的对象使用 const_cast移除const属性,否则是未定义的行为。
const_cast的安全检查:
只允许修改 const/volatile属性,且必须是指针/引用类型
const_cast不能改变底层数据类型。它只允许添加或移除 const和 volatile限定符。
//合法:
const int* → int* // 移除 const
int* → const int* // 添加 const
volatile char* → char* // 移除 volatile
//合法:
int* → const int* // 指针类型一致
const int& → int& // 引用类型一致
不允许其他的类型转换和非指针引用的转换
源类型和目标类型必须是相同的底层类型,仅 const/volatile限定符不同。
//非法:
const int* → char* // 错误:改变了底层类型 (int → char)
double* → const float* // 错误:改变了底层类型 (double → float)//非法:
int** → const int** // 错误:指向指针的指针(多级指针需逐级修改)
int[10] → const int* // 错误:数组与指针类型不同
2.4 dynamic_cast
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)
向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
注意:
- dynamic_cast只能用于父类含有虚函数的类
- dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回,如果是指针转换失败返回nullptr,引用转换失败抛出 std::bad_cast异常(后面讲)。
class A
{
public:virtual void f() {}
};
class B : public A
{
};
void fun(A* pa)
{// dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回B* pb1 = static_cast<B*>(pa);B* pb2 = dynamic_cast<B*>(pa);cout << "pb1:" << pb1 << endl;cout << "pb2:" << pb2 << endl;
}
int main()
{A a;B b;fun(&a);fun(&b);return 0;
}
dynamic_cast的运行时类型转换检查 (以指针向下转型为例)
假设我们有 Base* basePtr指向一个实际类型可能是 Derived的对象。我们执行 Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);。
- 检查源类型是否多态:
- 编译器首先检查 Base是否是多态类型(是否有虚函数)。如果不是,dynamic_cast要么编译失败(如果目标类型与源类型无关),要么行为未定义(如果编译器允许编译通过但转换无效)。dynamic_cast要求源类型是多态的!
- 获取对象的实际类型信息:
- 通过 basePtr找到它指向的对象。
- 通过该对象的 vptr找到其实际类型对应的 vtable。
- 从 vtable 中预先设定的位置,获取指向该对象实际类型(比如 ActualDerived)的 std::type_info对象的指针。
- 检查转换是否合法:
- 编译器生成的代码会利用 std::type_info对象中存储的继承关系信息。
- 它需要检查两个条件:
- Derived是否是 ActualDerived的基类? (或者 ActualDerived就是 Derived,或者 ActualDerived继承自 Derived)
- ActualDerived的继承路径中是否包含唯一的、可访问的 Derived? (避免歧义转换)
- 这个检查本质上是在运行时遍历 ActualDerived的继承树,看 Derived是否在其中。
- 执行转换 (如果合法):
- 如果转换合法:
- 如果 Derived是 ActualDerived的一个非虚基类(且是单继承或非第一个基类等情况),对象内存中 Derived子对象的地址可能与 ActualDerived对象的起始地址不同(存在偏移)。
- dynamic_cast不仅检查类型,还需要计算正确的地址偏移量。这个偏移量信息也存储在 RTTI 数据中(通常与 std::type_info关联或在 vtable 的附加结构中)。
- dynamic_cast返回一个指针,该指针是 basePtr的值加上计算出的偏移量(如果需要调整)。这个指针现在正确地指向了对象内部的 Derived子对象部分。
- 如果转换不合法:
- 返回 nullptr。
疑问:dynamic_cast执行转换是在运行时的,此时我们的运行代码已经固定,如何去执行我们类型的转换呢?
依赖存储在对象虚函数表 (vtable) 中的 运行时类型信息 (RTTI)。编译器不是在运行时生成的转换代码,而是一段由编译器预先生成的、通用的“查询逻辑代码”(查询 RTTI 并执行条件逻辑)。
总结
- static_cast,静态类型转换,检查相关类型之间的转换。
- reinterpret_cast,静态类型转换,不做类型安全检查,直接操作底层比特模式,和我们C式类型转换在生成的机器码层面通常是完全相同。
- constcast,静态类型转换,只用于指针和引用,且只用于添加或者消除变量最初非 const 定义的对象的const或者volatile属性,注意:对最终为const定义的对象消除const属性是未定义的行为。
- dynamiccast,动态转换,用于多态体系的父子类指针或引用之间的安全类型转换,会在代码运行时:做是否有虚函数(虚表)的检查、通过虚表做 实际类型 和 转换目标类型 之间的类型继承关系检查(检查其是否满足向上转换,或者一开始就指向子类)、然后根据检查的结果,返回指定的结果(转换成功的指针或者是转换失败的空指针、异常)。
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点 😃。