谈一谈类型转换(以C++为例)

前言

在这里插入图片描述

这两天在看《深度探索C++对象模型》时,又发现了自己的一个语言薄弱点,就是关于C++中类型的转换问题,尤其是涉及到多态时的类型转换:关于 static_cast、typeid、dynamic_cast 问题有些弄得不轻桑,于是,又到了学习总结的时刻了。


一、关于类型转换,我想谈的事

在这里插入图片描述

类型转换,编程语言中再常见不过的事情了。 本质上说,它是把一段内存空间(或者一个对象的内存空间,可能不是连续的一段)换一种解释。于是,一种类型就成了另一种类型。

不过,你说转就转啊,你以为这是风火轮啊? 如果说类型相近的对象之间(牛仔裤和运动裤)可以相互转的话,那么类型相差十万八千里的,转起来确实不太容易(当然,也没这个必要,手机转换成上衣,估计也没人会穿)。

所以说,能不能转、如何转,这就成为了一个问题。

有的语言(比如说C语言)提供了比较松的要求,把权限放在用户(程序员)这儿,能不能转、如何转全看程序员,相当灵活,随你玩,只要你能玩的开(当然,主要是针对C语言的指针的强制类型转换, 对于普通类型的转换还是有一些限制的)。 有的语言(比如说C++)则稍微严格些,它知道不是每个程序员都和编程语言是好朋友,深刻理解“好朋友”的特性,所以在编译期间或者运行期间做了一个限制。

对于C语言来说,其强制类型转换使用的是一对“超级牛逼”的大括号(),然后在这个大括号里写上要转换的类型 ,如

在这里插入图片描述
当然输出的是一个垃圾值。

对于C++来说,它还是稍微要严格些的,虽然它兼容了C语言的风格,使得上面的例子在C++中也能编译的过,但是C++ 之父强烈建议不要使用C风格的强制类型转换,而是使用标准C++的类型转换符static_cast, dynamic_cast等。

下面主要针对C++的几个显示强制类型转换谈谈我的理解。

二、关于static_cast,我想谈的事

关于C++中的强制转换static_cast,其基本用法是

用法:static_cast < type-id > ( expression )

它基本上相当于C语言中的大括号。 但是要比后者要稍微安全些,因为它会进行一些编译期间的类型检查,尤其是涉及到一些无关类型指针的转换 问题。 如下两幅图所示:

在这里插入图片描述
图:C语言类型的强制转换
在这里插入图片描述
图:static_cast类型的强制转换

虽然可以进行一定程度上的编译期间的类型检查(尤其是针对于无关指针的转换方面),但是无法进行运行期间的类型转换,所以对于多态这种有可能会在运行期间改变类型的行为,static_cast 就只能无奈的“摇摇头,挥挥手”了。

一般来说,static_cast 适用于以下场合

  1. 用于类层次结构中基类(父类)和 派生类(子类)之间指针或引用的转换。
    (1)、进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
    (2)、 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
  2. 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
  3. 把空指针转换成目标类型的空指针。
  4. 把任何类型的表达式转换成void类型。

总体来说,除了指针类型的转换比C语言风格的要稍微加强了些,其他的好像没啥太大变化。

三、关于typeid,我想谈的事

3.1 什么是运行时类型?

首先说明一个问题:什么是运行时类型?

按照编程小白的理解,一个变量或者说对象的类型不是在编译期间就确定的吗? 是 int 就int, 是 char* 就char* 是 class A 就class A ,为什么还会有运行时的类型这回事呢?

理解的基本没啥问题,当然这是在没考虑多态的情况下。

考虑到多态的情形,一个指针指向的对象类型,有可能会变身哦? 这也就是多态的基本的定义了,一个父类的指针指向其派生类的对象,那么这个对象的实际类型(运行时刻的类型)其实就是派生类的类型。

对于多态的指针来说,这里还要做一下区分,即指针的类型和指针所指向的对象类型。有请大家看下面这幅图:
在这里插入图片描述
图: 未有多态的示意图

现在我要问两个问题,
(1)、指针bp和bp1是什么类型?
(2)、对象 *bp和对象 *bp1是什么类型的?

考虑一分钟。

我要公布答案了哦。
答:(1)、bp和bp1都是Base * 类型的。
(2)、* bp 和* bp1 都是 Base 类型的。

诧异不? 不诧异的话,说明小伙子掌握的不错。 因为上面问的几个类型都是在编译时期确定的,没有涉及到多态 (class Base中 没有虚函数,而且class Derived只是普通的继承),*bp1 只是被截断成了Base 类型。 如上图中红圈所示。

好了,小二,上点“多态”,ok。现在有多态了,可以理解为class Base 类中有一个virtual 的虚函数。

同样的问题,问同样的你?
(1)、指针bp和bp1是什么类型?
(2)、对象 *bp和对象 *bp1是什么类型的?

同样的思考一分钟。
… … … …

思考出来了吗? 公布答案了哦。
答:(1)、bp和bp1还都是Base * 类型的。
(2)、* bp 是Base 类型的,但是*bp1 是Derived类型的。

为什么 ? 因为bp1和bp 都是指针,它们的类型是在编译期间就确定了的。无论何时都不会变化。 而*bp 本身就是Base 类型的,也不涉及变化。 关键为什么 *bp1变成了Derived类型了呢?

关键就在运行时的多态上,在实际的运行中,如果程序执行到

Base * bp1 = new Derived();

的时候, *bp1就因为多态的特性,自动绑定到 Derived类型了(如下图红圈所示。)(关于如何确定到具体的子类Derived ,简单来说是根据虚函数等机制,详细的多态实现可以参照【1】、【2】) 。

在这里插入图片描述
图:含有多态的示意图

这就是运行时的类型和编译期间类型不同的情况。 (对于上图,你可以简单把*bp1 在编译期是Base类型的, 在运行期间转换成了Derived 类型)

3.2 为什么需要运行时类型(RTTI)机制?

然后再说明一个问题:为什么需要运行时类型(RTTI)机制?

对于和编程语言关系不是很“熟悉”的人来说,这确实还不太好回答。

我按照参考【1】 中的说法归纳出了以下两点:
(1)、对于异常处理机制来说,其发挥作用是在运行期间当异常抛出的时候才会执行具体的异常处理机制,这个过程中需要进行异常类型匹配,因此它需要在运行时刻检查一个对象的类型信息。

(2)、RTTI对于这里的强制转换可以进行运行时刻的类型检查,为类型的转换提供了一定的安全保障。

3.3 typeid

为了能够在运行时确定对象的类型信息type_info (C++为了描述对象的运行时类型增加了type_info 类,它描述了对象必要的运行时类型信息),C++引入了两个运算符: typeid 和dynamic_cast 。这里先介绍前者。

typeid运算符:就像sizeof一样是C++语言直接支持的,它对一个对象或者类型名作为参数,返回一个匹配 const type_info对象,表明对象的确切类型。可以通过该类型输出对象的运行时类型。 如下面代码所示。

class Base{
public:
    Base(){
        cout<<"in Base\n";
    }

   virtual void test(){
        cout<<"test in Base\n";
    }

    int data;
};

class Derived1:public Base{
public:
    Derived1(){
        cout<<"in derived1\n";
    }

    void Test1(){
        cout<<"Test1 in derived\n";
    }

    int data1;
};

int main()
{
    Base *b = new Base;
    Derived1 *dp = new Derived1;
   // dp->data1 = 19;

    cout<<"type b: "<<typeid(b).name()<<endl;
    cout<<"type *b: "<<typeid(*b).name()<<endl;

    cout<<"type dp: "<<typeid(dp).name()<<endl;
    cout<<"type *dp: "<<typeid(*dp).name()<<endl;

    Base *b2 = (dp);

    cout<<"type b2: "<<typeid(b2).name()<<endl;
    cout<<"type *b2: "<<typeid(*b2).name()<<endl;

    return 0;
}

运行结果如下图。
在这里插入图片描述
图: 代码运行结果

注意,上面显示的类型前面可能带有一些标识,P代表是指针,数字4和8代表xxx(额, ̄□ ̄||,我也不知道),后面应该是类的名称。

除了输出对象的名称之外,type_info 还重载了operator== ,可以用来判断两个类型是否相同。 如下所示:

 if(typeid(*b2) == typeid(Derived1)){
        cout<<"*b2 't type is Derived\n";
    }

四、关于dynamic_cast,我想谈的事

4.1 为什么需要dynamic_cast ?

在我现在看来使用dynamic_cast 最主要的原因是为了更安全的使父类指针(或引用)转化为子类指针(其实本质上我们的目的是为了把父类对象转化为子类对象)。

按照常理说,为了实现多态,我们使用虚函数的机制使父类的指针指向子类对象,那怎么这里为什么又需要把父类的指针转换为子类的指针呢? 原因说起来也很简单,此子对象非彼子对象,也就是说原来父类所指向的子类对象(通过多态实现)和要转换的子类对象不一样。

上盘栗子。

在这里插入图片描述
有的时候我们有这样一个需求,用一个公共的水果类指针(父类指针)来作为参数接受左边梨子和苹果的子类指针(需要有多态),然后把这个父类指针再转化为具体的子类来进行每个子类特殊的一些操作(没有放在水果的公共基类中的,比如说苹果需要消毒、梨子需要浇水(胡诌的一些操作,请见谅… ̄□ ̄||))。

因为我们传进来的也不知道是具体的苹果还是梨子,所以没法进行具体的操作,只有把水果的父类指针在转化为其对应的类型才可以, 而这个过程就需要父类向子类转换了,也就是到了dynamic_cast大显神通的时候了。

说实话,这个例子没有代码描述的不是很好理解,可以参照【1】中RTTI及其构成一章节的例子,或者【3】中的例子。

4.2 dynamic_cast 用法和概况

dynamic_cast 是C++提供的用来在继承体系中上下转换的运算符。 对于static_cast 来说,虽然其也可以进行继承体系的上下转换,但是由上所述,其不提供动态的类型检查,所以相对来说,它不是很安全。 而dynamic_cast 正式为了解决了后者的问题,它提供了运行时的类型检查,虽然从某种程度上来说要有一定的代价(时间上和空间上),但是相对来说是安全了不少。

dynamic_cast 的用法如下:

dynamic_cast < type-id > ( expression )

比如说:

    Derived1 *dp2 = dynamic_cast<Derived1*>(b2);
    if(dp2 != nullptr){
        cout<<dp2->Test1();
    }

说明:该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。

最后,关于dynamic_cast 还有即点老生常谈的东西需要记录下来:

(1)、dynamic_cast<> 可以用来转换指针和引用,但是不能转换对象。当目标类型是某种类型的指针(包括void*)时,如果转换成功则返回目标类型的指针,否则返回NULL;当目标类型为某种类型的引用是,如果成功则返回目标类型的引用,否则抛出std::bad_cast异常,因为不存在NULL引用。 (这点就是不同于static_cast的地方)

(2)、dynamic_cast<> 是通过对象的vptr检查位于其类型vtable第一个slot的type_info对象而得知的,因此它只能用于多态类型多项(用于虚函数或者虚继承),否则将无法通过编译。

五、关于总结,我想谈的事

enen… 好像没啥好说的了。 只说一个小疑问吧:

关于多态,网上也有很多说法,说不建议在工程中使用这个,这样的话,这几种类型的转换不知道具体在工程中用到的多不多了?

参考

【1】、高质量程序设计指南-C++/C语言
【2】、深度探索C++对象模型
【3】、【C++专题】static_cast, dynamic_cast, const_cast探讨
【4】、为什么要用static_cast转换而不用c语言中的转换?

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值