掘金 阅读 ( ) • 2024-04-16 12:26

转载于:https://www.zhihu.com/people/sun-zi-xuan-27-41

C++ 四种cast

类型转换基本上是所有的C++项目中都要用到的,在C++中主要分为四种case,分别是:static_castdynamic_castconst_castreinterpret_cast,下面讲逐一对这四种cast进行讨论

C 风格强制类型转换

C语言风格的强制类型转换比较强大且万能,需要使用类型转换的地方都可以使用C语言风格的强制类型转换进行转换。正是因为这种万能性,语言风格的类型转换在使用上没有提供任何约束。没有任何约束代表着无法提供任何检查,包括静态和动态检查,所以使用此类型转换的时候,需要编程者来负责转换的安全性

C语言风格的类型转换有两种形式,这两种形式都可,就是写法不一样

  1. T(expression)
  2. (T)expression

为了弥补C语言风格强制类型转换无任何约束,无法提供语法层面的类型转换相关的安全检查,C++提供了四种cast来应对不同过的情况

C++风格强制类型转换

C++引入了强制类型转换相关的转换函数,这些转换函数可以提供一些静态类型检查或者运行期动态类型检查,来帮助开发者检查代码中的错误类型检查,检查程序中的BUG。并且通过这些类型转换函数,可以使得代码的语义性更强,表述更清晰,可以提高代码的阅读性和可维护性

dynamic_cast动态类型转换

动态类型转换,主要用于面向对象中多态应用场景,用于基类指针和派生类指针或者基类引用和派生类引用的安全转换,提供动态的安全检查

作用介绍

上图是cpp reference中的关于这个dynamic_cast的介绍,对应的中文介绍如下:

该函数的作用是用来安全的对类的指针或者引用在继承体系中进行向上(up),向下(down),和侧面(sideways)进行转换

解释一下上面的转换的名次,解释如下(基于上图中的示例进行讨论):

  1. 向上转换(upcast):派生类向基类转换,比如从B转换到A
  2. 向下转换(downcast):基类向派生类转换,比如从A转换到B
  3. 侧面转换(sideways):同一继承继承级别的类之间的转换,比如B转换成C

如果**dynamic_cast**类型转换成功,则该函数会返回目标类型对应的值。如果类型转换失败,对于指针类型,则会返回空指针,对于引用类型则会抛出异常**std::bad_cast**(因为有空指针,但是没有空引用的概念)

下面就上面两种类型转换,说明一下dynamic_cast在其中的情况

  1. 对于向上类型转换的时候,总是成功并且安全的
  2. 对于向下类型转换不都是成功的。对于指针,如果转换不成功,返回空指针。对于引用,如果转换不成功,则会跑出异常(std::bad_cast)
  3. 对于侧面转换,则需要转换前和转换后的两种类型有同一个基类

RTTI

dynamic_cast中有一个关键词dynamic,该词对应的中文为动态。动态转换中的动态指的是运行期进行动态的转换。该函数的运行依赖于一种RTTI(run-time type identification,动态类型识别的技术)。通常编译器都会提供一个选项,可以用来开启或者关闭该功能。使用RTTI动态类型识别技术,会带来而外的性能消耗。dynamic_cast来进行类型转换的需要根据虚函数表来判断继承关系(动态多态的实现,就是依托于虚函数表来实现的),这里对虚函数表进行查表的操作,会带来额外的性能损耗(如果用static_cast来进行转换,则不会进行运行期的动态检查,不会有这方面的新能损耗,这个本篇后续章节会讨论)

上面提到了,需要有虚函数表才能进行使用该类型转换函数,下面我们实际测试一下,如果没有虚函数会带来什么问题?

下面的终端输出是对将a对象转换成类B的引用类型,a的类型(A)是类型B的基类,且A是没有虚函数定义的(这里就是示范代码,通常基类是要求析构函数是虚函数的,否则会造成内存泄漏的问题)

通过下面终端输出得知,在这种情况下,在编译期间会报错,如下图所示,编译器提示类型A不是多态的(polymoriphic),即类型A内没有虚函数

在前面我们说过通常编译器RTTI功能可以通过编译时的命令参数来进行开启或者关闭,当RTTI功能关闭的时候,代码运行的时候,如果执行到dynamic_cast相关代码的时候,由于该转换函数依赖于RTTI功能,而编译器在编译程序的时候该功能被关闭,此时由于没有RTTI相关的信息在编译的时候编译到程序中,这个转换功能将无法完成转换,程序会崩溃,这个一定要注意

static_cast静态类型转换

静态类型转换,在编译期间提供类型转换检查,主要用于非多态的场景(当然也可以用于多态的场景)。相比较于C语言风格引入了一些静态的约束,比如检查const属性和voliate属性

 const int g = 20;
 int *h = static_cast<int*>(&g); 

上边示例的转换会导致编译错误,因为非const 指针h想要指向一个常量。

对于多态的场景,向上转换(up cast,从派生类转换到基类,指针或者引用)、向下转换(down cast,基类转换成派生类,指针或者引用)的情况如下

  1. 对于向上转换(up)是安全的
  2. 对于向下(down)转换总是成功的,并且不提供检查。效果和C风格的强制类型转换是一样的,需要编程者保证转换的安全性,既被转换的派生类和目标转换类型之间必须是继承派生关系

多态场景下static_cast和dynamic_cast的横向对比

从上面的讨论中,我们得知对于多态场景,我们有两种方法static_castdynamic_cast来实现类型转换。对于派生类的指针或者引用向其基类的指针或者引用进行转换(up cast)的时候,总是成功的并且是安全的。对于基类的指针或者引用向派生类的指针或者引用转换(down cast)的时候dynamic_cast提供动态类型检查,当转换失败的时候可以通过返回空指针或者抛出异常的方式来告知调用者(caller)转换失败了,而static_cast则不会进行动态检查,其像C语言中的强制类型转换一样,默认为可以转换,并且返回强制转换之后的指针或者引用,如果参与转换的两个类型不是继承关系,则会产生未定义行为,所以此时保证类型可以安全转换是编程者的职责,需要编程者来进行保证

看起来,使用dynamic_cast来进行向下转换(down_cast)是一个更好的选择,其能提供动态检查,通过动态检查,我们可以在运行期进行转换是否成功的判断

但是问题没有这么简单,主要的问题就发生在RTTI(运行期类型识别)上,我们先看一下谷歌C++编程规范关于RTTI相关的规范要求,文章截图如下图所示

上图中第一句话中就说了,避免使用RTTI。第二句话给出了RTII的介绍定义,RTII允许开发者在运行时通过typeid或者dynamic_cast来查询一个对象的类型

避免使用RTTI技术这句话被放在了第一句话,说明谷歌编程规范中强烈要求不要使用RTTI,而基于RTTIdynamic_cast也自然强烈要求不要使用,原因如下:

  1. 使用RTTI技术需要带来额外的运行期性能损耗
  2. 使用RTTI类型通常意味着代码总体设计有问题,因为C++本身是强类型的语言,所以在代码设计的时候,应该明确的考虑好各部分的类型,避免为运行期引入潜在的代码缺陷(通常这些缺陷不好侦测和调试)。并且这种依赖于运行期类型检查的代码必然引入类似于if-else或者switch-cast的这种冗长的类型分支以及对于类型转换dynamic_cast返回值的有效性检查,以及返回无效指针的时候的异常处理(而这通常可以通过优良的设计而规避掉),这不利于后期代码维护
  3. 使用RTTI进行运行时类型检查,通常可以通过良好的设计比如用vistor设计模式来规避掉。对于通过派生类类型来执行不同代码的这种情况,通常应该通过虚函数多态的形式,将这些由于是不同派生类的导致代码差异的不同行为,封装在派生类的内部,通过多态的方式进行功能实现,而不是在类的外边写冗长的分支语句来实现,不好维护,同时破坏了OO(面相对象)中对象的封装性

但是dynamic_cast也不是一无是处,谷歌C++编程规范中指出,该类型转换函数可以在单元测试(unit test)中放心的进行使用,比如测试工厂类返回的派生类是否正确,可以使用该技术,但是在实际的上线生产代码中不要进行使用

所以,在实际的编程实践中,应该在除了单元测试代码中,避免使用dynamic_cast转而使用static_cast,并且在代码设计的时候保证static_cast的向下转换是安全且正确的

谷歌C++编程规范中对于这块还有其他细节描述,比较详细,感兴趣的推荐看一下谷歌的这块的文档

const_cast

用来在不同cv属性的类型的数据之间转换,这里面的cv指的是constnessvolatility

具体参考cpp reference

reinterpret_cast

通过对底层字节数据进行重新解释来进行类型转换。不同于static_castreinterpret_cast不会变成任何机器指令(整型数据与指针之间的转换除外或者在一些复杂的指令架构中)。它是一个单纯的编译器命令,用来告诉编译器这个表达式应该当成什么类型来看待

值得注意的是,其不能用来处理cv属性,如果转换前后的类型中的cv属性不一致,则无法进行转换,编译会报错,如下图所示

总结

在实际应用实践中,判断如何使用上边四种转换的方式通过如下的思路选择

  1. 如果是多态应用场景中(非单元测试代码),在基类和派生类进行类型转换(指针或者引用),则优先使用static_cast,不要使用dynamic_cast
  2. 如果是在单元测试中,需要对类型进行检查(比如测试工厂代码返回的类是否正确),则可以使用dynamic_cast
  3. 如果是不涉及到cv属性的变化的时候,并且非多态应用场景下,优先使用static_cast
  4. 如果需要改变cv属性,使用const_cast
  5. 最后选择使用reinterpret_cast进行转换,或者几种转换组合使用