【C++】—— C++的类型转换

简介: 【C++】—— C++的类型转换



(一)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. 例如,如果你在代码中看到大量的类型转换,那么这可能意味着你的代码应该被重构以减少这种不透明性;
  3. 此外,使用类型转换可能会违反一些编程原则,如“不要直接操作值”,因为这可能会导致代码的可读性和可维护性下降。

1.2 显示类型转换

显式类型转换:程序员明确地使用类型转换运算符进行类型转换。这有助于确保程序员了解可能发生的数据丢失,并提供更精细的控制。

  • 例如以下代码:

 解释说明:

  • 上述代码的关键在于将浮点数 num1 + num2 的结果强制转换为整数;
  • 这种强制类型转换会导致浮点数的小数部分被截断,只保留整数部分;
  • 在这个例子中,num1 + num2 的结果是 13.14,但由于强制转换为整数,最终的 result 变量的值为 13

💨💨 其次可能还有遇到将指针的值转换为整数类型的情况:

 解释说明:

  • 使用显式的强制类型转换将指针变量p转换为整数类型,并将结果赋值给整型变量address;
  • 需要注意的是,将指针转换为整数类型只是将地址值从指针类型转换为整数类型,而不是将指针所指的内容进行转换

1.3 类型转换函数

C语言还提供了一些类型转换的库函数,例如atoi()、atof()等,用于将字符串转换为整数、浮点数等。这些函数执行严格的转换,并提供错误处理机制。

  • 例如以下代码:


【小结】

  1. 总体而言,C语言的类型转换是一项强大但需要小心使用的功能;
  2. 程序员应该确保了解数据转换的可能影响,并在需要时采取适当的预防措施;
  3. 其次,面临着转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换等等缺陷

(二)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在这个地方的好处就是:它是先检查你这个父类的指针,如果你指向父类,此时就转换失败,如果你指向子类呢,我才会转换成功:

注意

  • 强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会;
  • 强烈建议:避免使用强制类型转换

  • 向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)

(三)小结

以上便是本文的主要内容了。接下来,简单总结一下本文内容!!!

  1. 隐式类型转换:当编译器自动将一种类型转换为另一种类型时,称为隐式类型转换。例如,将一个整数赋值给一个浮点数变量时,编译器会自动将整数转换为浮点数。
  2. 显式类型转换:当程序员明确指定将一种类型转换为另一种类型时,称为显式类型转换。在C++中,有四种显式类型转换运算符:static_cast、dynamic_cast、const_cast和reinterpret_cast。
  3. static_cast:用于基本的类型转换,如将整数转换为浮点数,指针转换为整数等。它可以在编译期进行类型检查,但不能用于没有继承关系的类之间的转换。
  4. dynamic_cast:用于在继承关系中的类型转换。它会在运行时进行类型检查,如果转换不安全,则返回nullptr或抛出异常。
  5. const_cast:用于去除变量的const或volatile属性。它可以用于修改常量对象的值,但需要谨慎使用,因为违反了const的语义。
  6. reinterpret_cast:用于将一个指针或引用转换为另一个不相关的类型。它没有类型检查,因此非常危险,可能导致未定义的行为。

总的来说,类型转换是将一个类型的值转换为另一个类型的过程。在C++中,可以通过隐式类型转换和显式类型转换来实现不同类型之间的转换。在进行类型转换时,需要注意类型的兼容性和安全性,避免出现错误或未定义的行为。

本文的内容讲解便到此结束,感谢大家的观看与支持!!!

相关文章
|
7月前
|
安全 编译器 程序员
【C++】C++的类型转换
【C++】C++的类型转换
|
7月前
|
设计模式 安全 算法
【C/C++ 类型转换 】深入理解C++向上转型:从基础到应用
【C/C++ 类型转换 】深入理解C++向上转型:从基础到应用
228 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
2月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
29 1
|
2月前
|
存储 编译器 数据安全/隐私保护
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解2
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
40 3
|
2月前
|
编译器 C++
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解1
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
53 3
|
7月前
|
存储 安全 编译器
C++:现代类型转换
C++:现代类型转换
54 5
|
2月前
|
C++
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
29 0
|
5月前
|
存储 安全 编译器
【C++11】类型转换
【C++11】类型转换
36 0
|
5月前
|
安全 程序员 编译器
C++一分钟之-C++中的类型转换
【7月更文挑战第8天】C++中的类型转换涉及隐式和显式操作,隐式转换如从`int`到`double`是自动的,但可能导致数据丢失。显式转换包括`static_cast`, `dynamic_cast`, `const_cast`, `reinterpret_cast`,以及转换构造函数。要避免数据丢失、类型不匹配和运行时错误,需谨慎使用显式转换并检查结果。过度使用`reinterpret_cast`应避免。理解这些转换有助于编写更安全的代码。
47 0