July.cc Blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


C++ 2023 年 7 月 10 日

[C++] C++新的类型转换方式介绍: C语言类型转换介绍、static_cast、reinterpret_cast、const_cast、dynamic_cast、RTTI介绍

C语言中 的类型转换可视性比较差, 所有的转换形式都是以一种相同形式书写, 难以跟踪错误的转换. C++则针对不同场景实现了4种不同的类型转换...

C语言中的类型转换

在C语言中, 有一些情况需要发生类型转化:
  1. 赋值运算符左右两侧类型不同
  2. 比较运算符左右两侧类型不同
  3. 形参与实参类型不匹配
  4. 返回值类型与接收返回值类型不一致
C语言中总共有两种形式的类型转换:隐式类型转换显式类型转换

隐式类型转化

  1. 编译器会在编译阶段自动进行, 比如像这样:

    #include <iostream>
    
    int main() {
        size_t pos = 0;
        int i = 10;
        while (i >= pos) {
            i--;
            std::cout << i << std::endl;
        }
    
        return 0;
    }

    这段代码的执行结果是什么?

    第一眼看去: 好像很简单啊, 不就是 打印 9->0 吗?

    但是, 当程序执行之后:

    很神奇, 明明 i < 0 了 但是循环还在继续.

    原因是: while (i >= pos) 这里发生了隐式类型转换:

    前10次循环, 是没有出什么问题的 也就是 i从10->0的过程.

    不过, 当i==0进入循环时 会执行i—, i成为了负数-1. 按正常思维, 循环就要跳出了.

    但是, 当 i==-1时, while (i >= pos) 的这个条件判断, 在作比较时 i会被隐式类型转换为 size_t 无符号整型

    这意味着什么? 这意味着, 即使 实际上i==-1, 但在比较时 它就是 二进制32位全1的无符号整数, 所以 while (i >= pos) 也就永远不会结束.

  2. 如果允许隐式类型转换, 那编译器就会发生. 如果此处不允许, 那 编译就会报错

    #include <iostream>
    
    int main() {
        char a = 'c';
        int b = a;
        std::cout << a << " : " << b << std::endl;
    
        int* c= a;
    
        return 0;
    }

    这段代码, int* c = a; 会报错. 因为不允许发生隐式类型转换.

    不过, 前面的可以编译通过:

这是C语言的隐式类型转换

显式强制转换

除了上面介绍的 编译器自动做的一些类型的转换. 用户还可以显式的对变量的类型做转换
#include <cstdlib>
#include <iostream>

int main() {
    int* a = (int*)std::malloc(sizeof(int));
    *a = 20;

    std::cout << *a << std::endl;

    std::free(a);

    return 0;
}
malloc()的返回值, 使用(int*)强制类型转换为 int*
即, C语言中 可以使用 (类型) 强制转换变量的类型. 但是 必须是相近的类型, 不然会报错.

C语言中 就只有这两种类型转换的方式, 用户可以使用的也就只有 (类型)
并且还具有明显的缺陷:
可视性比较差, 所有的转换形式都是以一种相同形式书写, 难以跟踪错误的转换
C++则实现了 4种不同的类型转换

C++的4种类型转换

虽然C语言的类型转换很简单, 但是也存在一些问题:
  1. 隐式类型转换可能会导致精度丢失或者判断错误
  2. 显式类型转换则所有情况同一书写方式, 比较混乱 代码不够清晰
所以, C++增添设计了4种新的类型转换: static_cast reinterpret_cast const_cast dynamic_cast
四种类型转换分别用于不同的场景, 看到就可以分辨出来此处的类型转换是什么类型了.

static_cast

static_cast 静态转换, 常用于相关的内置类型之间、以及相关自定义类型之间的转换, 即 相关相近类型之间的转换.
用法很简单, static_cast<需要转换为什么类型>(需要转换类型的变量) 下面三种也是这种用法
#include <iostream>

int main() {
    double d = 123.12398;
    int a = static_cast<int>(d);
    std::cout << d << " : " << a << std::endl;

    return 0;
}

reinterpret_cast

reinterpret_cast 则常用于类似 int->int*, 这种不相近类型之间的转换. 但只能小单位向大单位转换
#include <cstdlib>
#include <iostream>

int main() {
    double d = 123.12398;
    int a = static_cast<int>(d);
    std::cout << d << " : " << a << std::endl;

    // 报错
    //int* c = static_cast<int*>(a);
    int* c = reinterpret_cast<int*>(a);
    std::cout << c << std::endl;
    // int b = reinterpret_cast<int>(c);  // 64位平台 可能会报错
    // 因为 64位平台 指针是8字节, 而int是4字节 可能产生数据丢失
    // 经测试 GCC 会报错, MSVS则不会

    return 0;
}

const_cast

在介绍进程信号的时候, 介绍过一个关键字 volatile, 其作用是 保持内存的可见性
在我们使用 const 修饰变量时, 编译器可能会认为此变量不会被修改 而将此 变量值优化到寄存器 中, 在之后使用此变量时, 可能会 直接从寄存器中获取值. 还有些编译器 会 直接在预处理时其当作值. 原因就是 编译器认为这个变量不会被改变.
但是 实际上, 还是可以 改变此变量的值 的:
int main(){
    const int val = 100;
    int* pV = (int*)&val;
    *pV = 200;

    std::cout << val << std::endl;
    std::cout << *pV << std::endl;

    return 0;
}
这段代码的执行结果是:
通过 对const int val取地址 再 将其强制类型转换为int*, 然后通过int*修改变量的值.
最终打印变量 和 指向变量地址的int*变量的值, 会输出两个不同的值.
原因就是, 直接打印变量 编译器会从寄存器获取数据 或 直接在预处理阶段把变量改为值. 而通过指针访问变量的地址 则是真正的访问了内存中的数据. 想要不让编译器做优化, 就可以使用volatile关键词保持变量的内存可见性, 即使用此变量必须从内存中获取.
不过, 我们这里主要介绍的是 const_cast. const_cast 常用于这种取消const变量的常量属性的场景.
所以, 上面的代码还可以换成:
int main(){
    volatile const int val = 100;
    int* pV = const_cast<int*>(&val);
    *pV = 200;

    std::cout << val << std::endl;
    std::cout << *pV << std::endl;

    return 0;
}

dynamic_cast

dynamic_cast 是功能相对最复杂的一个类型转换方式. 它 用于 将一个父类对象的指针或引用 转换为 子类对象的指针或引用(动态转换)
C++继承体系中:
向上转换: 子类对象的指针或引用 是 默认支持类型转换到 父类对象的指针或引用的(赋值兼容), 比如多态调用.
向下转换: 而 在一般情况下 父类对象的指针或引用 是 不支持直接转换成 子类对象的指针或引用的. 强制转换的话是可以的, 但是不安全.
首先要了解, 什么时候可能会用到向下转换呢?
看一个简单的场景:
class A {
public:
    virtual void f() {}
};

class B : public A {};

void fun(A* pa) {
    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;
}
fun()被调用时, 其实参是否有可能是子类对象的指针?
很明显是有可能的, 因为这是一个简单的多态调用嘛. 也就是说, pa可能指向父类对象也可能指向子类对象.
而, 如果我们需要在fun()函数内部弄明白, pa到底是指向一个父类对象还是子类对象 要怎么实现?
没有dynamic_cast, 可以通过在类内部实现一个支持多态调用的函数, 如果是多态调用返回true, 表示指向子类对象, 否则返回false, 表示返回父类对象.
class A {
public:
    virtual void f() {}
    virtual bool isPoly() {
        return false;
    }
};

class B : public A {
public:
    virtual bool isPoly() {
        return true;
    }
};

void fun(A* pa) {
    if(pa->isPoly()) {
        cout << "多态调用, 指向子类对象" << endl;
    }
    else if(!pa->isPoly()) {
        cout << "指向父类对象" << endl;
    }
}

int main() {
    A a;
    B b;
    fun(&a);
    fun(&b);

    return 0;
}
就像上面这样, 执行结果为:
但是, 有了dynamic_cast之后, 就不需要这样了:
class A {
public:
    virtual void f() {}
};

class B : public A {};

void fun(A* pa) {
    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;
}
这段代码的执行结果是:
前两个输出是传入父类对象指针时的输出, 后两个输出是传入子类对象指针时的输出.
有什么差别?
可以看到:
  1. static_cast, 无论传入父类对象的指针 还是 子类对象的指针, 他都可以成功的向下转换.
  2. dynamic_cast, 传入父类对象的指针时, 向下转换失败, 返回0. 传入子类指针时, 向下转换成功
这意味着什么?
dynamic_cast会动态转换, 如果父类对象的指针或引用 原本就是 子类对象的指针或引用, dynamic_cast才能转换成功. 否则, 转换失败 结果为0. 这说明了, dynamic_cast更加安全
因为, 父类对象的指针强制转化成子类对象的指针, 是不安全的.
不过, 需要注意的是, dynamic_cast只能用于父类含有虚函数的类
如果是下面这种类体系, dynamic_cast无法使用:
class A {};

class B : public A {};

void fun(A* pa) {
    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;
}
GCC会报错: 原类型不支持多态

RTTI

RTTI(Run-Time Type Information,运行时类型信息识别) 是 C++ 的一个特性, 它允许程序在运行时获取对象的类型信息
主要通过两个运算符实现: typeiddynamic_cast
  1. typeid

    使用typeid(val).name()可以获取到当前对对象的类型, 但只是一个字符串. 而且结果根据编译器而定, 不同平台很可能不同.

  2. dynamic_cast

    dynamic_cast则是通过判断是否可以转换为子类对象指针或引用, 来判断父类对象指针或引用原本的内容的.

了解一下就好

感谢阅读~
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

作者: 哈米d1ch 发表日期:2023 年 7 月 10 日