(一)C语言中的类型转换
在学习C++的类型转换之前,我们先来回顾下有关C语言中类型转换的相关知识!!!
发生的时机:
- 在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化。
而C语言中总共有两种形式的类型转换:
- 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
- 显式类型转化:需要用户自己处理
接下来,我分别对上述两种类型转换进行代码层面的解释说明:
1.1 隐式类型转换
隐式类型转换: 这种转换是由编译器根据上下文进行的:
解释说明:
- 在
Test()
函数中,定义了一个整型变量a
并赋值为1。然后,将整型变量a
隐式地转换为浮点型变量b
,这是因为在表达式中涉及到不同类型的操作数时,编译器会自动进行隐式类型转换。 - 因为整型变量
a
被隐式转换为浮点型变量b
,所以b
的值为1.00。
💨💨但是此时如果你想将一个`double`类型的变量转换为`int`,由于计算机中整数存储方式的不同,可能会丢失一些小数部分的信息,这就会造成精度损失:
- 例如以下代码:
解释说明:
- 上述例子中,a 是一个
double
类型的浮点数,但将其隐式转换为int
类型时,小数部分将被截断,导致精度损失。
💨💨 除了上述情况之外,可能还会碰到以下情况。例如以下示例:
解释说明:
- 加法的两个运算对象类型不同: 3. 14的类型是double,10的类型是int;
- 编译器不会直接把两个数据相加,而是先根据类型转换规则设法将运算对象的类型统一后在进行相加,此时就会发生精度缺失
💨💨 隐式类型转换可能导致一些意外的行为,特别是在混合使用不同类型的操作时:
解释说明:
- 上述例子中,b是一个字符,但它可以被隐式转换为其ASCII码值(整数),然后与a相加。这可能导致程序行为不符合预期。
【小结】
- 根据上述描述,大家需要注意的是,过度使用类型转换可能会导致代码难以理解和维护;
- 例如,如果你在代码中看到大量的类型转换,那么这可能意味着你的代码应该被重构以减少这种不透明性;
- 此外,使用类型转换可能会违反一些编程原则,如“不要直接操作值”,因为这可能会导致代码的可读性和可维护性下降。
1.2 显示类型转换
显式类型转换:程序员明确地使用类型转换运算符进行类型转换。这有助于确保程序员了解可能发生的数据丢失,并提供更精细的控制。
- 例如以下代码:
解释说明:
- 上述代码的关键在于将浮点数
num1 + num2
的结果强制转换为整数; - 这种强制类型转换会导致浮点数的小数部分被截断,只保留整数部分;
- 在这个例子中,
num1 + num2
的结果是13.14
,但由于强制转换为整数,最终的result
变量的值为13
。
💨💨 其次可能还有遇到将指针的值转换为整数类型的情况:
解释说明:
- 使用显式的强制类型转换将指针变量
p
转换为整数类型,并将结果赋值给整型变量address; - 需要注意的是,将指针转换为整数类型只是将地址值从指针类型转换为整数类型,而不是将指针所指的内容进行转换
1.3 类型转换函数
C语言还提供了一些类型转换的库函数,例如atoi()、atof()等,用于将字符串转换为整数、浮点数等。这些函数执行严格的转换,并提供错误处理机制。
- 例如以下代码:
【小结】
- 总体而言,C语言的类型转换是一项强大但需要小心使用的功能;
- 程序员应该确保了解数据转换的可能影响,并在需要时采取适当的预防措施;
- 其次,面临着转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换等等缺陷
(二)C++中的类型转换
2.1 隐式类型转换
2.1.1 算术转换
当我们像下面这样把一个算术类型的值赋给另一种类型时:
bool a = 10; //a为真 int i = a; //i的值为1 i = 3.1415; //i的值为3 double pi = i; //pi的值为3.0 unsigned char b = -1; //假设char占8比特,则b为255 signed char c = 256; //假设char占8比特,c的值是未定义的
类型所能表示的值的范围决定了转换的过程:
- 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为 false,否则结果为true;
- 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为 0,初始值为true则结果为 1;
- 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分;
- 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失;
- 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char 所得的结果是255;
- 当我们赋给带符号类型-一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
🔥 🔥 含有无符号类型的表达式
💨💨尽管我们不会故意给无符号对象赋--个负值,但是保不齐大家会写出这么做的代码。例如,当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数。把int转换成无符号数的过程和把int直接赋给无符号变量一样:
【解释说明】
- 在第一个输出表达式里,两个(负)整数相加并得到了期望的结果;
- 在第二个输出表达式里,相加前首先把整数-42转换成无符号数;
- 把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。
💨💨当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负数:
2.1.2 数组转换成指针
在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:
int arr[10]; //含有10个整数的数组 int* num = arr; //arr转换成指向数组首元素的指针
- 当数组被用作decltype关键字的参数,或者作为取地址符(&)、 sizeof 及typeid等运算符的运算对象时,上述转换不会发生;
- 同样的,如果用一个引用来初始化数组,上述转换也不会发生;
- 当在表达式中使用函数类型时会发生类似的指针转换。
2.1.3 指针的转换
指针的转换: C++还规定 了几种其他的指针转换方式:
- 包括常量整数值0或者字面值nullptr能转换成任意指针类型;
使用整数值 0
:
//整数值 0 可以隐式转换为指针类型,因此 arry 被初始化为空指针。 int* arry= 0; // 或 int* arry= NULL;
使用字面值 nullptr
:
//nullptr 是C++11引入的空指针字面值,它可以被隐式转换为任意指针类型 //相较于使用整数值 0,使用 nullptr 更为类型安全,因为它是一个特殊的空指针值,而不是整数。 int* arry= nullptr;
- 指向任意非常量的指针能转换成void*;
👉 这种转换的目的是为了提供一种通用的指针类型,可以指向任何非常量的数据 👈
【解释说名】:
- 在上面的例子中,it_Ptr 和 db_Ptr 是指向整数和双精度浮点数的指针,分别被隐式转换为
void*;
void*
是一种通用的指针类型,可以指向任何非常量的数据;- 然而,需要注意的是,使用
void*
会失去指针指向对象的具体类型信息,因此在使用时需要谨慎。
- 指向任意对象的指针能转换成const void*:
2.1.4 转换成bool类型
- 存在一种从算术类型或指针类型向布尔类型自动转换的机制;
- 如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:
1️⃣整数类型到bool
2️⃣指针类型到bool
3️⃣其他类型到bool
2.1.5 转换成常量
- 允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样;
- 也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用
int i; const int& j = i; //非常量转换成const int的引用 const int* p = &i;//非常量的地址转换成 const的地址
👉 但是需要注意一点的是不允许将const 转换为非常量,编译器会发生报错 👈
2.2 显示类型转换
2.2.1 为什么C++需要四种类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
- static_cast、reinterpret_cast、const_cast、dynamic_cast
C风格的转换格式很简单,但是有不少缺点的:
- 1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
- 2. 显式类型转换将所有情况混合在一起,代码不够清晰
因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
2.2.2 C++强制类型转换
有时我们希望显式地将对象强制转换成另外一种类型。例如,如果想在下面的代码中执行浮点数除法:
int i,j; double res = i/j;
- 就要使用某种方法将i和 / 或 j 显式地转换成double,这种方法称作强制类型转换(cast)。
接下来我将对每一种进行简要的说明:
static_cast
- static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_ cast。编译器隐式执行的任何类型转换都可用static_cast
💨💨接下来介绍static_cast
的几个常见用法和特点:
- 基本数据类型转换:可以在数值类型之间进行转换,例如将整数类型转换为浮点型或将浮点型转换为整数类型
示例如下:
- 指针之间的转换: 可以将指针类型转换为其他指针类型,或者将void指针转换为其他指针类型
示例如下:
- 注意:static_cast在指针之间的转换时,只能在相关类型之间进行安全的转换。
- 父子类指针或引用转换:static_cast 可以进行父子类之间的指针或引用转换,前提是这两个类之间必须存在继承关系
示例如下:
class Arry{ public: virtual void foo() { cout << "Arry::foo()" << endl; } }; class Num: public Arry{ public: virtual void foo() { cout << "Num::foo()" << endl; } }; Arry* pBase = new Num(); // 创建一个派生类对象,并将其赋值给基类指针 Num* pDerived = static_cast<Num*>(pBase); // 将基类指针pBase转换为派生类指针pDerived
- static_cast对于编译器无法自动执行的类型转换也非常有用:
示例如下:
【注意】
- 当我们把指针存放在void*中,并且使用static_ cast 将其强制转换回原来的类型时,应该确保指针的值保持不变;
- 也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型;
- 类型一旦不符,将产生未定义的后果。
reinterpret_cast
- reinterpret_cast:操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型
💨💨举个例子,现在有以下类型的转换:
int *num; char *pc = reinterpret_ cast<char*>(num) ;
我们必须牢记pc所指的真实对象是一一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。例如:
string str(pc) ;
- 可能导致异常的运行时行为。
注意事项:
- 使用reinterpret_ _cast 是非常危险的,用pc初始化str的例子很好地证明了这一.点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。
- 当我们用一个int的地址初始化pc时,由于显式地声称这种转换合法,所以编译器不会发出任何警告或错误信息。
- 接下来再使用pc时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int的指针。最终的结果就是,在上面的例子中虽然用pc初始化str没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。
💨💨再给大家验证reinterpret_cast是将一种类型转换为另一种不同的类型:
【解释说明】
- 因为
static_cast
在进行指针类型转换时,只能用于具有继承关系的类层次结构之间的指针类型转换,或者用于指针和整数类型之间的转换。但它不能用于两个不相关的类型进行转换
const_cast
- const_cast:是用于在编译时去除对象的
const
或volatile
修饰
💨💨 以下是const_cast
的常见应用场景:
- 去除
const
修饰:
示例如下:
【解释说明】
- 这里是因为 const的原因,加了const之后编译器默认会进行优化操作;
- 因为编译器默认认为const是不能直接被修改的,如果想去直接修改是无法完成的,这里只能是间接的发生了修改操作;
- 有些地方会把这里的 num 存到寄存器,此时不会去内存中取 num,而是去寄存器当中取数据;而有些编译器则把这里当作"宏” 的效果一样,编译的时候直接替换即可。
而在vs下,我通过调试可以发现这里发生的是替换:
此时,当我们可以加上 volatile 关键字之后,则可以防止这种优化操作:
- 再去调试看编译器此时的处理操作:
此外,上述操作我们还可以使用 C语言的强制类型转换机制.具体如下:
dynamic_cast
- dynamic_cast:用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。它主要用于类层次结构中的向上转换和向下转换,并且只能用于包含虚函数的类。
💨💨 dynamic_cast
的主要特点和应用场景如下:
- 用于向下转型:当基类指针或引用指向派生类对象时,可以使用
dynamic_cast
将其转换为派生类指针或引用。
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的),而对象则不可以
而对于安全或者不安全这个话题我们可以通过代码来进行理解:
class A { public : virtual void f(){} }; class B : public A {}; void fun (A* pa) { // dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回 B* pb1 = (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; }
【解释说明】
因为大家看(A *pa)这是一个父类的指针,对于上转下,一般情况我们是没有上转下的需求的,只有一种情况有上转下的需求。此时有没有这样一种可能,这是一个父类的指针,这个父类的指针如果本身就是指向父类,你给他转回子类可不可以呢?可以,但是有风险存在。
强制类型转换或者static这种转换,它都是很暴力的,它不管你指父类或者指向子类,都给你转成功:
而dynamic_cast在这个地方的好处就是:它是先检查你这个父类的指针,如果你指向父类,此时就转换失败,如果你指向子类呢,我才会转换成功:
注意
- 强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会;
- 强烈建议:避免使用强制类型转换
- 向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
(三)小结
以上便是本文的主要内容了。接下来,简单总结一下本文内容!!!
- 隐式类型转换:当编译器自动将一种类型转换为另一种类型时,称为隐式类型转换。例如,将一个整数赋值给一个浮点数变量时,编译器会自动将整数转换为浮点数。
- 显式类型转换:当程序员明确指定将一种类型转换为另一种类型时,称为显式类型转换。在C++中,有四种显式类型转换运算符:static_cast、dynamic_cast、const_cast和reinterpret_cast。
- static_cast:用于基本的类型转换,如将整数转换为浮点数,指针转换为整数等。它可以在编译期进行类型检查,但不能用于没有继承关系的类之间的转换。
- dynamic_cast:用于在继承关系中的类型转换。它会在运行时进行类型检查,如果转换不安全,则返回nullptr或抛出异常。
- const_cast:用于去除变量的const或volatile属性。它可以用于修改常量对象的值,但需要谨慎使用,因为违反了const的语义。
- reinterpret_cast:用于将一个指针或引用转换为另一个不相关的类型。它没有类型检查,因此非常危险,可能导致未定义的行为。
总的来说,类型转换是将一个类型的值转换为另一个类型的过程。在C++中,可以通过隐式类型转换和显式类型转换来实现不同类型之间的转换。在进行类型转换时,需要注意类型的兼容性和安全性,避免出现错误或未定义的行为。
本文的内容讲解便到此结束,感谢大家的观看与支持!!!