• C++学习之路-类型转换


    C语言中的类型转换

    通常,C语言的类型转换风格是下面两种

    • (type)expression
    • type (expression)

    类似于我们理解的强制转换

    int a = 10;
    double b = (double) a;
    double c = double(a);
    
    • 1
    • 2
    • 3

    当前,就算不写强制转换,只有变量声明的类型和赋值的类型不同。C语言也会默认进行隐式转换

    int a = 10;
    double b = a; //b = 10.00000
    
    • 1
    • 2

    C++中的类型转换

    C++中有4个类型转换符

    • static_cast
    • dynamic_cast
    • reinterpret_cast
    • const_cast

    具体使用时的格式为:

    xxxx_cast<type>(expression)
    
    • 1

    const_cast:一般用于去除const属性(将const转换为非const)

    比如说定义了一个const类型的指针a,想要将a赋值给非const指针b,就会出错。
    在这里插入图片描述

    于是,可以这样解决:这样就完成了const类型到非const类型的转换

    在这里插入图片描述

    const_cast中的类型必须是指针,引用,或者指向对象类型成员的指针

    在这个过程中,我遇到了一个细节问题。当我转换基本数据类型int时,不可以转换。

    在这里插入图片描述
    所以,const_cast只能用在指针或引用类型之间的转换上。

    比如说我想要重新定义一个对象指针p2,指向已经存在的const对象指针p1,这是不被允许的。

    在这里插入图片描述
    但是,我就是有这样的需求,需要另外一个指针指向这块区域。那我就可以采用const_cast强制转换为非const的:

    const Person *p1 = new Person();
    Person *p2 = const_cast<Person*>(p1);
    
    • 1
    • 2

    但是!!!!并不是说const转换为非const就必须使用const_cast,而是const_cast可以做这件事而已!!!

    这样也可以办到const到非const的转换。只不过const_cast更加清晰明了,让开发人员直接就能看出来这是const转为非const的过程。程序的可读性强而已。

    const Person *p1 = new Person();
    Person *p3 = (Person *) p1;
    
    • 1
    • 2

    这两种转换本质上没有任何区别,只是可读性更强而已。

    在这里插入图片描述

    所以,const_cast,我们只需要能看懂别人写的代码就行,可有可无的存在,没有什么优秀特性,唯一的优点就是可读性强。

    dynamic_cast:一般用于多态类型的转换(有安全检测功能)

    一般用于多态类型的转换,额外有类型之间的安全检测过程。这个我们需要额外重视

    什么叫多态,可以看之前的博文。第一部分就是说明父类指针可以指向子类对象,而子类指针不可以指向父类对象

    • 首先定义两个类,实现多态。相信可以看明白(因为只有虚函数才有多态)
    class Person
    {
    	virtual void run(){}
    public:
    	int m_age;
    
    };
    
    class Student : public Person {
    public:
    	int m_score;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 声明两个父类Person指针对象,分别指向不同的对象
    Person *p1 = new Person();  //父类指针指向父类对象
    Person *p2 = new Student(); //父类指针指向子类对象
    
    • 1
    • 2

    类指针为什么不可以指向父类对象

    这里多说一句:为什么子类指针不允许指向父类对象?

    因为某种类型的指针,只可以访问其本身的成员。比如:Person类型的指针,只可以访问 m_age,而不可以访问m_score;Student类型的指针就可以访问m_age和m_score。

    在这里插入图片描述
    在这里插入图片描述

    指向什么类型的对象,也就意味着指针可以访问这个对象的内存区域。比如:指向Person对象,那就可以访问Person对象里的内存;指向Student对象,那就可以访问Student对象里的内存。

    现在说说为什么父类指针可以指向子类对象,子类指针不可以指向父类对象!!

    因为这里面涉及到安全问题。也就是说父类指针指向子类对象没有安全问题,而子类指针指向父类对象就存在安全问题

    下面举一个例子,就明白了了!我们定义了两个指针,分别指向不同的对象,可以看明白吧

    Person *p1 = new Student ();  //父类指针指向子类对象 (安全)
    Student *s1 = new Person ();  //子类指针指向父类对象 (不安全)
    
    • 1
    • 2

    p1可以访问m_age,同时p1指向的区域是Student(有m_age和m_score),结果就是p1访问不到子类Student里的m_score。但这又何妨呢?访问不到就访问不到呗,起码不会乱访问。

    p2可以访问m_age和m_score,但是p2指向的区域是Person (只有m_age),也就意味着p2可以访问到m_age,但是p2->m_score在程序中并不会报错,而是指向Person对象内存中m_age的下四个字节。但是,这四个字节不是Person对象的内存,很可能是别的对象的内存,这就乱访问了,p2->m_score的值就不是我们想要访问的值了,就会出现安全问题。

    所以C++不允许这种情况出现,直接报错!!!
    在这里插入图片描述

    但是,如果我非要这么做,可以用强制转换,达到编译器不报错的目的。但是安全问题依然存在!!!

    在这里插入图片描述

    为了避免开发人员强制转换所带来的安全问题。我们可以使用dynamic_cast进行强制转换,并进行安全检测!如果发现是子类指针指向父类对象,那返回的指针为nullptr,就不会产生安全问题(不会乱访问未知的内存区域)

    Person *p1 = new Person();  //指向父类
    Person *p2 = new Student();	//指向子类
    
    cout << "p1 = " << p1 << endl; //p1 = 000001EE091900D0
    cout << "p2 = " << p2 << endl; //p2 = 000001EE091894B0
    
    Student *stu1 = (Student *)p1;                  //stu1 = 000001EE091900D0
    Student *stu2 = dynamic_cast<Student *>(p1);	//stu2 = 0000000000000000
    Student *stu3 = dynamic_cast<Student *>(p2);	//stu3 = 000001EE091894B0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到stu1和p1的值是一样的,说明这种强转直接将p1的内容给了stu1,也就是说stu1依然指向了Person对象,而且编译器还没报错;

    在使用dynamic_cast强转之后,发现stu2 虽然接受的是p1的值,但是stu2通过安全检测,发现存在子类指针指向父类对象的操作,于是dynamic_cast将stu2清零,这样就是一个nullptr,就不会出现乱访问的状况了。

    由于stu3指向的也是本身类的对象,是子类指针指向子类对象,所以不存在子类指针指向父类对象的操作。因此不会被安全监测,也就不会被清零了。

    交叉转换也会被dynamic_cast检测出来

    不只是,子类指针指向父类对象不被允许。只要是不合乎逻辑的操作(比如交叉转换),都是可以被dynamic_cast检测出来

    假设我定义了一个Car类,跟Person和Student 一点关系都没有

    class Person
    {
    	virtual void run(){}
    public:
    	int m_age;
    
    };
    
    class Student : public Person {
    public:
    	int m_score;
    };
    
    class Car
    {
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    一点关系没有,我们也可以通过强转让其赋值。如果是直接强转,就是把值赋过来拉倒。如果是采用dynamic_cast强转,则会进行安全监测(由于Car指针不可以随意访问与其一点关系没有的Person对象),所以就会将赋值过来的p1置为nullptr

    Person *p1 = new Person(); 
    cout << "p1 = " << p1 << endl;//p1 = 000001FB10922770
    
    Car *c1 = (Car *)p1;
    Car *c2 = dynamic_cast<Car *>(p1);
    
    cout << "c1 = " << c1 << endl; //c1 = 000001FB10922770
    cout << "c2 = " << c2 << endl; //c2 = 0000000000000000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    总结:我们几乎不会犯子类指针指向父类对象(不合乎逻辑)的操作,但是要能看懂别人写这样的代码的含义。

    static_cast:常用于基本数据类型转换,比如非const转换为const

    • 对比dynamic_cast,缺乏安全监测

    上面的代码要这样写,stu2就不会被置为nullptr了

    Student *stu2 = static_cast<Student *>(p1);	//stu2 = 000001EE091900D0
    
    • 1
    • 不能交叉转换(不是统一继承体系的,无法转换)

    什么叫交叉转换?Person和Car没有任何联系,强行转换叫做交叉转换。

    Person *p1 = new Person(); 
    
    Car *c1 = (Car *)p1;
    Car *c2 = dynamic_cast<Car *>(p1);
    
    • 1
    • 2
    • 3
    • 4

    而static_cast是不可以完成这样的操作的,编译器会直接报错:类型转换无效
    在这里插入图片描述

    对比完与dynamic_cast的区别,我们说一下static_cast的特点:

    • 两个代码没有任何区别,static_cast反而更麻烦
    int a = 10;
    double b = static_cast<double>(a);
    
    • 1
    • 2
    int a = 10;
    double b = a;
    
    • 1
    • 2
    • 非const转为const
    int *a = new int();
    const int *b = static_cast<const int*>(a);
    
    Person *p1 = new Person();
    const Person *p2 = static_cast<const Person *>(p1);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    基本没啥用,不写static_cast,跟隐式转换的效果是一模一样的。也就是写不写程序执行的效果是一样的。

    reinterpret_cast:没有任何类型检查和格式转换,仅仅是简单的二进制数据的拷贝

    这个有些不太一样,需要注意一下。

    reinterpret_cast属于比较底层的强制转换,没有任何类型检查和格式转换,仅仅是简单的二进制数据的拷贝。

    比如:我们定义一个int型的变量,赋值10。

    int a = 10;
    
    • 1

    我们通过监视,输入&a,就可以查看到存储a的内存,并给出了所在的栈空间地址

    在这里插入图片描述

    通过地址值,我们就可以在内存中查找到,该块内存区域的内容。可以看到,是 0a 00 00 00 也就是整数10的16进制表示。还可以看到,其他内存区域都是cc,这就是栈空间的特点,全部初始化为cc。

    在这里插入图片描述

    接下来,我们进行了一个隐式转换的操作。将int转换为double

    int a = 10;
    double d = a;
    
    • 1
    • 2

    获取到double型变量d的地址

    在这里插入图片描述

    查看被赋值的double内容,确实是double类型的数字10.000000…

    在这里插入图片描述

    但是内存中的16进制,我有些看不懂。什么 24 40 我怎么算也算不出是10。

    在这里插入图片描述

    这是因为double、float这种浮点数类型的数据存储方式与int不同(存储尾数和指数这种)。会额外做一些处理,显示的16进制也就不是我们理解的0a这种。具体看这篇博客:浮点数double在内存中的存储方式

    我们了解了int和double内存的存储方式不同,导致二进制数据是不统一含义的。我们解释了这么多,就是为了感受一下reinterpret_cast所谓的仅仅是二进制数据的拷贝是什么意思。

    我们将上述的隐式类型转换改为reinterpret_cast转换,<>为引用类型:不同类型转换需要引用,int* 和int转换就不需要引用。

    int a = 10;
    double d = reinterpret_cast<double&>(a);
    
    • 1
    • 2

    可以看到,d并不是double类型的10.000000…,而是认不出来的数

    在这里插入图片描述

    得到d的地址

    在这里插入图片描述

    查看内存中的数据,我们发现:double类型占8个字节,前4个字节被 int 10 的二进制数据覆盖,而后4个字节依然是栈空间初始化的cc。这就导致 double d 的内存内容是 0a 00 00 00 cc cc cc cc。

    在这里插入图片描述

    这就是简单的二进制赋值,不会考虑类型是啥,reinterpret_cast就是直接赋值。

    下面的图,更能清晰的说明这个过程:

    • 隐式转换,会考虑类型,达到我们想要的转换。由于double存储方式,所以我们看到的二进制代码很奇怪,但是不影响结果

    在这里插入图片描述

    • reinterpret_cast强制转换,只进行简单的二进制代码的复制,不检查左右两边类型。

    在这里插入图片描述

    到这里,并不是告诉开发人员以后采用reinterpret_cast强制转换。我的目的是告诉开发人员,这个reinterpret_cast的功能是什么,在日常开发过程中这么写就出错了。但是,reinterpret_cast依然有他自己的应用场景。

    总结:reinterpret_cast也不会有安全监测,就是把右边的内存中的二进制数据赋值给左边的存储空间,只要二进制存储的方式相同,就不会出现解析不了的问题。

    补充一点,reinterpret_cast可以将指针和整数互相转换

    int *p = reinterpret_cast<int*>(0x0100);  // p是指针,存储的0x0100
    cout << "p = " << p << endl;
    int num = reinterpret_cast<int>(p); //将地址转换为整数,256
    cout << "num = " << num << endl; //256
    
    • 1
    • 2
    • 3
    • 4

    总结

    这些转换类型符可能根本就用不到,但是要知道功能是啥,也就是说要能看懂别人写的代码。仅此而已。

  • 相关阅读:
    深入学习 Redis Sentinel - 基于 DockerCompose 编排哨兵分布式架构,理解工作原理
    单片机C语言实例:13、看门狗
    linux内核启动过程分析
    HCIA自学笔记01-冲突域
    pandas数据分析:十分钟快速入门重点函数速查
    儿童护眼灯什么光源好?亮度柔和的护眼台灯分享
    SpringBoot集成腾讯云云点播服务/视频上传
    基于 Rainbond 的 Pipeline(流水线)插件
    使用Docker部署Hadoop
    Java下打印一个等腰三角型
  • 原文地址:https://blog.csdn.net/weixin_45452278/article/details/126571743