第13章 类型转换运算符

    类型转换是一种机制,让程序员能够暂时或永久性改变编译器对对象的解释。注意,这并不意味着程序员改变了对象本身,而只是改变了对对象的解释。可改变对象解释方式的运算符称为类型转换运算符。

    在本章中,您将学习:

    • 为何需要类型转换运算符;

    • 为什么有些C++程序员不喜欢传统的C风格类型转换;

    • 4个C++类型转换运算符;

    • 向上转换和向下转换;

    • 为什么C++类型转换运算符并非总是最佳选择。

    13.1 为何需要类型转换

    如果 C++应用程序都编写得很完善,其处于类型是安全的且是强类型的世界,则没有必要进行类型转换,也不需要类型转换运算符。然而,在现实世界中,不同模块往往由使用不同环境的个人和厂商编写,他们需要相互协作。因此,程序员经常需要让编译器按其所需的方式解释数据,让应用程序能够成功编译并正确执行。

    来看一个真实的例子:虽然C++编译器支持bool,但是很多年前使用C语言编写的库仍在使用。这些针对C语言编译器编写的库必须依赖于整型来保存布尔值,因此对这些编译器来说,bool类型类似于下面这样:

    第13章 类型转换运算符 - 图1

    而返回布尔值的函数可能这样声明:

    第13章 类型转换运算符 - 图2

    如果要在新应用程序中使用一个这样的库,而该应用程序将使用最新的 C++编译器进行编译,则程序员必须让其使用的C++编译器能够理解数据类型bool,同时让库能够理解数据类型bool。为此,可使用类型转换:

    第13章 类型转换运算符 - 图3

    在C++的发展过程中,不断有新的C++类型转换运算符出现,这导致C++编程社区分裂成两个阵营:一个阵营继续在其C++应用程序中使用C风格类型转换,另一个阵营转而使用C++编译器引入的类型转换关键字。前一个阵营认为,C++类型转换难以使用,且有时候功能变化不大,只有理论意义。后一个阵营则显然由C++语法纯粹论者组成,他们通过指出C风格类型转换的缺陷以支持其论点。在现实世界中,这两个观点各行其道,读者最好通过阅读本章了解每种风格的优缺点,然后形成自己的见解。

    13.2 为何有些C++程序员不喜欢C风格类型转换

    C++程序员在赞颂这门编程语言时提到的优点之一是类型安全。实际上,大多数 C++编译器都不会让下面这样的语句通过编译:

    第13章 类型转换运算符 - 图4

    这是非常正确的!

    当前,C++编译器仍需向后兼容,以确保遗留代码能够通过编译,因此支持下面这样的语法:

    第13章 类型转换运算符 - 图5

    然而,C 风格类型转换实际上强迫编译器根据程序员的选择来解释目标对象。就上述代码而言,程序员并不认为编译器报告错误是合理的,因此强迫编译器遵从自己的意愿。然而,对不希望类型转换破坏其倡导的类型安全的C++程序员来说,这是无法接受的。

    13.3 C++类型转换运算符

    虽然类型转换有缺点,但也不能抛弃类型转换的概念。在很多情况下,类型转换是合理的需求,可解决重要的兼容性问题。C++提供了一种新的类型转换运算符,专门用于基于继承的情形,这种情形在C语言编程中并不存在。

    4个C++类型转换运算符如下:

    • static_cast

    • dynamic_cast

    • reinterpret_cast

    • const_cast

    这4个类型转换运算符的使用语法相同:

    第13章 类型转换运算符 - 图6

    13.3.1 使用static_cast

    static_cast用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换——这种转换原本将自动或隐式地进行。用于指针时,static_cast实现了基本的编译阶段检查,确保指针被转换为相关类型。这改进了C风格类型转换,在C语言中,可将指向一个对象的指针转换为完全不相关的类型,而编译器不会报错。使用static_cast可将指针向上转换为基类类型,也可向下转换为派生类型,如下面的示例代码所示:

    第13章 类型转换运算符 - 图7

    第13章 类型转换运算符 - 图8将 Derived转换为 Base被称为向上转换,无需使用任何显式类型转换运算符就能进行这种转换:

    第13章 类型转换运算符 - 图9

    将 Base转换为 Derived被称为向下转换,如果不使用显式类型转换运算符,就无法进行这种转换:

    第13章 类型转换运算符 - 图10

    然而,static_cast只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用static_cast编写如下代码,而编译器不会报错:

    第13章 类型转换运算符 - 图11

    其中pDerived实际上指向一个不完整的Derived对象,因为它指向的对象实际上是Base()类型。由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此 pDerived->SomeDerivedClassFunction()能够通过编译,但在运行阶段可能导致意外结果。

    除用于向上转换和向下转换外,static_cast还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读人员的注意:

    第13章 类型转换运算符 - 图12

    在上述代码中,使用Num = dPi将获得同样的效果,但使用 static_cast可让代码阅读者注意到这里使用了类型转换,并指出(对知道static_cast的人而言)编译器根据编译阶段可用的信息进行了必要的调整,以便执行所需的类型转换。

    13.3.2 使用dynamic_cast和运行阶段类型识别

    顾名思义,与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。可检查 dynamic_cast操作的结果,以判断类型转换是否成功。使用 dynamic_cast运算符的典型语法如下:

    第13章 类型转换运算符 - 图13

    例如:

    第13章 类型转换运算符 - 图14

    如上述代码所示,给定一个指向基类对象的指针,程序员可使用 dynamic_cast 进行类型转换,并在使用指针前检查指针指向的目标对象的类型。在上述示例代码中,目标对象的类型显然是Derived,因此这些代码只有演示价值。然而,情况并非总是如此,例如,将Derived传递给接受Base参数的函数时。该函数可使用使用 dynamic_cast 判断基类指针指向的对象的类型,再执行该类型特有的操作。总之,可使用dynamic_cast在运行阶段判断类型,并在安全时使用转换后的指针。程序清单13.1使用了一个您熟悉的继承层次结构—— Tuna和Carp类从基类Fish派生而来,其中的函数DetectFishtype()动态的检查Fish指针指向的对象是否是Tuna或Carp。

    第13章 类型转换运算符 - 图15这种在运行阶段识别对象类型的机制称为运行阶段类型识别(runtime type identification, RTTI)。

    程序清单13.1 使用动态转换判断Fish指针指向的是否是Tuna对象或Carp对象

    第13章 类型转换运算符 - 图16

    输出:

    第13章 类型转换运算符 - 图17

    分析:

    这是一个您在第10章熟悉的继承层次结构:Tuna和Carp从基类Fish派生而来。为方便解释,这两个派生类不仅实现了虚函数Swim(),还分别包含一个特有的函数,即Tuna::BecomeDinner()和Carp::Talk()。这个示例的独特之处在于,给定一个基类指针(Fish),您可动态地检测它指向的是否是Tuna或Carp。这种动态检测(运行阶段类型识别)是在第 43~61行定义的函数 DetectFishType()中进行的。在第 45行,使用dynamic_cast传入的基类指针(Fish)参数指向的是否是Tuna对象。如果该Fish指向的是Tuna对象,该运算符将返回一个有效的地址,否则将返回NULL。因此,总是需要检查dynamic_cast的结果是否有效。如果通过了第46行的检查,您便知道指针pIsTuna指向的是一个有效的Tuna对象,因此可以使用它来调用函数Tuna::BecomeDinner(),如第 49行所示。如果传入的 Fish参数指向的是 Carp对象,则使用它来调用函数Carp::Talk(),如第56行所示。返回之前,DetectFishType()调用了Swim(),以验证对象类型;Swim()是一个虚函数,这行代码将根据指针指向的对象类型,调用相应类(Tuna或Carp)中实现的方法Swim()。

    第13章 类型转换运算符 - 图18务必检查dynamic_cast的返回值,看它是否有效。如果返回值为NULL,说明转换失败。

    13.3.3 使用reinterpret_cast

    reinterpret_cast是C++中与C风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用如下所示的语法强制重新解释类型:

    第13章 类型转换运算符 - 图19

    这种类型转换实际上是强制编译器接受static_cast通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为API能够接受的简单类型(例如,有些API只能使用字节流,即 unsigned char*):

    第13章 类型转换运算符 - 图20

    上述代码使用的类型转换并没有改变源对象的二进制表示,但让编译器允许程序员访问SomeClass对象包含的各个字节。由于其他 C++类型转换运算符都不允许执行这样的转换,因此使用reinterpret_cast时,程序员将收到类型转换不安全(不可移植)的警告。

    第13章 类型转换运算符 - 图21应尽量避免在应用程序中使用 reinterpret_cast,因为它让编译器将类型 X 视为不相关的类型Y,这看起来不像是优秀的设计或实现。

    13.3.4 使用const_cast

    const_cast 让程序员能够关闭对象的访问修饰符 const。您可能会问:为何要进行这种转换?在理想情况下,程序员将经常在正确的地方使用关键字const。不幸的是,现实世界并非如此,像下面这样的代码随处可见:

    第13章 类型转换运算符 - 图22

    在下面的函数中,以 const 引用的方式传递 mData 对象显然是正确的。毕竟,显示函数应该是只读的,不应调用非const成员函数,即不应调用能够修改对象状态的函数。然而,DisplayMembers()本应为 const 的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对DisplayMembers()进行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast将是您的救星。

    第13章 类型转换运算符 - 图23

    在这种情况下,调用DisplayMembers()的语法如下:

    第13章 类型转换运算符 - 图24

    除非万不得已,否则不要使用const_cast来调用非const函数。一般而言,使用const_cast来修改const对象可能导致不可预料的行为。

    另外,const_cast也可用于指针:

    第13章 类型转换运算符 - 图25

    13.4 C++类型转换运算符存在的问题

    并非所有人都喜欢使用C++类型转换,即使那些C++拥趸也如此。其理由很多,从语法繁琐而不够直观到显得多余,不一而足。

    来比较一下下面的代码:

    第13章 类型转换运算符 - 图26

    在这3种方法中,程序员得到的结果都相同。在实际情况下,第2种方法可能最常见,其次是第3 种,但几乎没有人使用第 1 种方法。无论采用哪种方法,编译器都足够聪明,能够正确地进行类型转换。这让人觉得类型转换运算符将降低代码的可读性。

    同样,static_cast的其他用途也可使用C风格类型转换进行处理,且更简单:

    第13章 类型转换运算符 - 图27

    因此,使用 static_cast的优点常常被其拙劣的语法所掩盖。Bjarne Stroustrup准确地描述了这种境况:“由于static_cast如此拙劣且难以输入,因此您在使用它之前很可能会三思。这很不错,因为类型转换在现代C++中是最容易避免的。”

    再来看其他运算符。在不能使用static_cast时,可使用reinterpret_cast强制进行转换;同样,可以使用const_cast修改访问修饰符const。因此,在现代C++中,除dynamic_cast外的类型转换都是可以避免的。仅当需要满足遗留应用程序的需求时,才需要使用其他类型转换运算符。在这种情况下,程序员通常倾向于使用C风格类型转换而不是C++类型转换运算符。重要的是,应尽量避免使用类型转换;而一旦使用类型转换,务必要知道幕后发生的情况。

    第13章 类型转换运算符 - 图28

    13.5 总结

    本章介绍了各种 C++类型转换运算符以及支持和反对类型转换运算符的根据。一般而言,应避免使用类型转换。

    13.6 问与答

    问:是否可使用const_cast对指向常量对象的指针或引用进行类型转换,以便修改常量对象的内容?

    答:不要这样做。这样做的结果是不确定的,也绝不是您希望的。

    问:我需要一个Bird,但只有一个Dog。编译器不允许将指向Dog对象的指针用作Bird。然而,当我使用 reinterpret_cast 将 Dog转换为 Bird*时,编译器并不报错。看起来可使用这个指针来调用Bird的成员函数Fly(),可以这样做吗?

    答:绝对不要这样做。reinterpret_cast只改变对指针的解释,并不改变指向的对象(它还是Dog)。对Dog对象调用Fly()函数将得不到所需的结果,还可能导致应用程序出现故障。

    问:我有一个Base指针pBase,它指向一个Derived对象。我确信pBase指向的是一个Derived对象,是否还需要使用dynamic_cast?

    答:由于您确定指向的是Derived对象,因此可使用static_cast提高运行性能。

    问:C++提供了类型转换运算符,但却建议尽量不使用它们。这是为什么?

    答:您家里备有阿司匹林,却不会把它当饭吃。仅当真正需要时才使用类型转换。

    13.7 作业

    作业包括测验和练习,前者帮助读者加深对所学知识的理解,后者提供了使用新学知识的机会。请尽量先完成测验和练习题,然后再对照附录D的答案。在继续学习下一章前,请务必弄懂这些答案。

    13.7.1 测验

    1.您有一个基类对象指针pBase,要确定它指向的是否是Derived1或Derived2对象,应使用哪种类型转换?

    2.假设您有一个指向对象的const引用,并试图通过它调用一个您编写的公有成员函数,但编译器不允许您这样做,因为该函数不是const成员。您将修改这个函数还是使用const_cast?

    3.判断对错:仅在不能使用static_cast时才应使用reinterpret_cast,这种类型转换是必须和安全的。

    4.判断对错:优秀的编译器将自动执行很多基于static_cast的类型转换,尤其是简单数据类型之间的转换。

    13.7.2 练习

    1.查错:下述代码有何问题?

    第13章 类型转换运算符 - 图29

    2.假设有一个Fish指针(pFish),它指向一个Tuna对象:

    第13章 类型转换运算符 - 图30

    要让一个 Tuna指针指向该指针指向的 Tuna对象,应使用哪种类型转换?请使用代码证明您的看法。