前言

C语言中的类型转换
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。

  1. 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
  2. 显式类型转化:需要用户自己处理

到目前为止我们的所有代码,使用的也都是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;
}

以上代码有几个问题:

  1. 编译器可能将n直接替换为字面量10,当读取n时,实际读取的是编译器缓存的常量值(而非内存中的值),导致cout << n输出10,而*p实际指向的内存值已被改为11。

    对此可以使用volatile关键字修饰变量:

    volatile const int n = 10; 
    

    这样可以让编译器强制去内存取,禁用了宏替换,于是打印出来的就只能是改过的值。

  2. ​内存模型欺骗​:

    const int n = 10;    // 编译器可能将n放入只读内存段
    int* p = (int*)&n;   // 通过指针强制访问
    *p = 20;             // 试图修改只读内存 -> 程序崩溃!
    

    可能触发段错误

  3. 修改声明为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;
}
  1. 静态类型转换,即static_cast的所有类型检查、转换规则和最终生成的转换代码,都是在编译阶段由编译器确定和完成的。
    因为它是静态类型转换,所以在相关类型转换中特殊转换情况(继承体系向下转换)下,它不进行运行时检查,依赖程序员保证正确性。所以我们在做继承体系下的类型转换时,还是使用dynamic_cast,因为安全的继承体系向下转型是 dynamic_cast的工作。

  2. 相比与C风格的转换它更安全,因为编译器会根据源代码中提供的类型信息(源类型和目标类型),应用 C++ 标准中定义的规则来判断这个转换是否合法。

  3. 它用于编译器已知且允许的转换,所以编译器隐式执行的任何类型转换都可用static_cast。

static_cast的安全检查:

允许相关的/安全的类型转换

static_cast是用于相关类型之间的转换的,编译器会在编译阶段检查使用static_cast转换的合法性和安全性,然后才生成对应的转换

  1. ​基本类型转换(数值类型)​​:
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)。

  1. 继承体系中的向上转换(派生类→基类)
    我们知道继承体系,子类和父类是密切相关的(相关类型),且子类本来就有父类的成员(安全的转换),所以,向上转换,是安全的:
class Base {};
class Derived : public Base {};

int main()
{	
	Derived d;
	Base* b = static_cast<Base*>(&d); // 安全:向上转换始终合法
}

​安全检查​:编译器验证继承关系,若Derived不是Base的子类,则报错。

  1. 枚举与整数类型转换​:
enum Color { RED, GREEN };
int main()
{
	Color c = static_cast<Color>(1); // 安全:整数转枚举(GREEN)
}

​安全检查​:编译器确保整数在枚举定义的取值范围内(此处1是有效的)。

阻止不相关的/不安全的类型转换
  1. ​不相关的指针类型转换​:
int main()
{
	int x = 10;
	double* p = static_cast<double*>(&x); // 错误:int*与double*无关
}

安全检查​:编译器直接报错,因为两种指针类型无继承关系。

  1. 去除const限定符​:
int main()
{
	const int ci = 100;
	int* p = static_cast<int*>(&ci); // 错误:static_cast不能移除const
}

安全检查​:必须使用const_cast移除const,static_cast会拒绝。

  1. ​向下转换(基类→派生类)无检查​:
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_cast
	cout << a << endl;

	// 这里使用static_cast会报错,应该使用reinterpret_cast
	//int *p = static_cast<int*>(a);
 
	int *p = reinterpret_cast<int*>(a);    //这时不相关类型的转换就要用:reinterpret_cast
	return 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;
}
  1. 它也是静态转换,也是在编译阶段就完成的。

  2. 不改变底层类型​
    const_cast​不能用于不同类型之间的转换(如 int转 char)。如需此类转换,请用 reinterpret_cast。

  3. 仅限指针/引用​
    它只适用于指针、引用或成员指针类型。

  4. 只对最初非 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转型是安全的)
注意:

  1. dynamic_cast只能用于父类含有虚函数的类
  2. 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);。

  1. 检查源类型是否多态:​​
    • 编译器首先检查 Base是否是多态类型(是否有虚函数)。如果不是,dynamic_cast要么编译失败(如果目标类型与源类型无关),要么行为未定义(如果编译器允许编译通过但转换无效)。​dynamic_cast要求源类型是多态的!​​
  2. ​获取对象的实际类型信息:​​
    • 通过 basePtr找到它指向的对象。
    • 通过该对象的 vptr找到其实际类型对应的 vtable。
    • 从 vtable 中预先设定的位置,获取指向该对象实际类型​(比如 ActualDerived)的 std::type_info对象的指针。
  3. 检查转换是否合法:​​
    • 编译器生成的代码会利用 std::type_info对象中存储的继承关系信息。
    • 它需要检查两个条件:
      • Derived是否是 ActualDerived的基类?​​ (或者 ActualDerived就是 Derived,或者 ActualDerived继承自 Derived)
      • ActualDerived的继承路径中是否包含唯一的、可访问的 Derived?​​ (避免歧义转换)
    • 这个检查本质上是在运行时遍历 ActualDerived的继承树,看 Derived是否在其中。
  4. ​执行转换 (如果合法):​​
    • ​如果转换合法:​​
      • 如果 Derived是 ActualDerived的一个非虚基类(且是单继承或非第一个基类等情况),对象内存中 Derived子对象的地址可能与 ActualDerived对象的起始地址不同(存在偏移)。
      • dynamic_cast不仅检查类型,​还需要计算正确的地址偏移量。这个偏移量信息也存储在 RTTI 数据中(通常与 std::type_info关联或在 vtable 的附加结构中)。
      • dynamic_cast返回一个指针,该指针是 basePtr的值加上计算出的偏移量(如果需要调整)。这个指针现在正确地指向了对象内部的 Derived子对象部分。
    • 如果转换不合法:​​
      • 返回 nullptr。

疑问1:dynamic_cast执行转换是在运行时的,此时我们的运行代码已经固定,如何去执行我们类型的转换呢?

依赖存储在对象虚函数表 (vtable) 中的 ​运行时类型信息 (RTTI)​。编译器不是在运行时生成的转换代码,而是一段由编译器预先生成的、通用的“查询逻辑代码”(查询 RTTI 并执行条件逻辑)。

疑问2:为什么dynamic_cast需要类是多态的呢?如果不是多态,而是简单的继承行不行?

​dynamic_cast要求源类型(即你试图转换的指针或引用的类型)必须是多态的(至少有一个虚函数)。​​ 即使是在“简单的”单继承层次结构中,如果基类没有虚函数,使用 dynamic_cast也会导致编译错误(如果目标类型与源类型无关)或未定义行为(如果编译器允许编译通过但转换无效)。
​为什么有这个要求?核心原因在于 dynamic_cast的机制依赖于虚函数表(vtable)来存储和访问运行时类型信息(RTTI)。​


总结

  1. static_cast,静态类型转换,检查相关类型之间的转换。
  2. reinterpret_cast,静态类型转换,不做类型安全检查,直接操作底层比特模式,和我们C式类型转换在生成的机器码层面通常是完全相同。
  3. const_cast,静态类型转换,只用于指针和引用,且只用于添加或者消除变量最初非 const 定义的对象的const或者volatile属性,注意:对最终为const定义的对象消除const属性是未定义的行为。
  4. dynamic_cast,动态转换,用于多态体系的父子类指针或引用之间的安全类型转换,会在代码运行时:做是否有虚函数(虚表)的检查、通过虚表做 实际类型 和 转换目标类型 之间的类型继承关系检查(检查其是否满足向上转换,或者一开始就指向子类)、然后根据检查的结果,返回指定的结果(转换成功的指针或者是转换失败的空指针、异常)。

本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点 😃。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐