第13章 类型转换运算符
类型转换是一种机制,让程序员能够暂时或永久性改变编译器对对象的解释。注意,这并不意味着程序员改变了对象本身,而只是改变了对对象的解释。可改变对象解释方式的运算符称为类型转换运算符。
在本章中,您将学习:
• 为何需要类型转换运算符;
• 为什么有些C++程序员不喜欢传统的C风格类型转换;
• 4个C++类型转换运算符;
• 向上转换和向下转换;
• 为什么C++类型转换运算符并非总是最佳选择。
13.1 为何需要类型转换
如果 C++应用程序都编写得很完善,其处于类型是安全的且是强类型的世界,则没有必要进行类型转换,也不需要类型转换运算符。然而,在现实世界中,不同模块往往由使用不同环境的个人和厂商编写,他们需要相互协作。因此,程序员经常需要让编译器按其所需的方式解释数据,让应用程序能够成功编译并正确执行。
来看一个真实的例子:虽然C++编译器支持bool,但是很多年前使用C语言编写的库仍在使用。这些针对C语言编译器编写的库必须依赖于整型来保存布尔值,因此对这些编译器来说,bool类型类似于下面这样:
而返回布尔值的函数可能这样声明:
如果要在新应用程序中使用一个这样的库,而该应用程序将使用最新的 C++编译器进行编译,则程序员必须让其使用的C++编译器能够理解数据类型bool,同时让库能够理解数据类型bool。为此,可使用类型转换:
在C++的发展过程中,不断有新的C++类型转换运算符出现,这导致C++编程社区分裂成两个阵营:一个阵营继续在其C++应用程序中使用C风格类型转换,另一个阵营转而使用C++编译器引入的类型转换关键字。前一个阵营认为,C++类型转换难以使用,且有时候功能变化不大,只有理论意义。后一个阵营则显然由C++语法纯粹论者组成,他们通过指出C风格类型转换的缺陷以支持其论点。在现实世界中,这两个观点各行其道,读者最好通过阅读本章了解每种风格的优缺点,然后形成自己的见解。
13.2 为何有些C++程序员不喜欢C风格类型转换
C++程序员在赞颂这门编程语言时提到的优点之一是类型安全。实际上,大多数 C++编译器都不会让下面这样的语句通过编译:
这是非常正确的!
当前,C++编译器仍需向后兼容,以确保遗留代码能够通过编译,因此支持下面这样的语法:
然而,C 风格类型转换实际上强迫编译器根据程序员的选择来解释目标对象。就上述代码而言,程序员并不认为编译器报告错误是合理的,因此强迫编译器遵从自己的意愿。然而,对不希望类型转换破坏其倡导的类型安全的C++程序员来说,这是无法接受的。
13.3 C++类型转换运算符
虽然类型转换有缺点,但也不能抛弃类型转换的概念。在很多情况下,类型转换是合理的需求,可解决重要的兼容性问题。C++提供了一种新的类型转换运算符,专门用于基于继承的情形,这种情形在C语言编程中并不存在。
4个C++类型转换运算符如下:
• static_cast
• dynamic_cast
• reinterpret_cast
• const_cast
这4个类型转换运算符的使用语法相同:
static_cast用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换——这种转换原本将自动或隐式地进行。用于指针时,static_cast实现了基本的编译阶段检查,确保指针被转换为相关类型。这改进了C风格类型转换,在C语言中,可将指向一个对象的指针转换为完全不相关的类型,而编译器不会报错。使用static_cast可将指针向上转换为基类类型,也可向下转换为派生类型,如下面的示例代码所示:
将 Derived转换为 Base被称为向上转换,无需使用任何显式类型转换运算符就能进行这种转换:
将 Base转换为 Derived被称为向下转换,如果不使用显式类型转换运算符,就无法进行这种转换:
然而,static_cast只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用static_cast编写如下代码,而编译器不会报错:
其中pDerived实际上指向一个不完整的Derived对象,因为它指向的对象实际上是Base()类型。由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此 pDerived->SomeDerivedClassFunction()能够通过编译,但在运行阶段可能导致意外结果。
除用于向上转换和向下转换外,static_cast还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读人员的注意:
在上述代码中,使用Num = dPi将获得同样的效果,但使用 static_cast可让代码阅读者注意到这里使用了类型转换,并指出(对知道static_cast的人而言)编译器根据编译阶段可用的信息进行了必要的调整,以便执行所需的类型转换。
13.3.2 使用dynamic_cast和运行阶段类型识别
顾名思义,与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。可检查 dynamic_cast操作的结果,以判断类型转换是否成功。使用 dynamic_cast运算符的典型语法如下:
例如:
如上述代码所示,给定一个指向基类对象的指针,程序员可使用 dynamic_cast 进行类型转换,并在使用指针前检查指针指向的目标对象的类型。在上述示例代码中,目标对象的类型显然是Derived,因此这些代码只有演示价值。然而,情况并非总是如此,例如,将Derived传递给接受Base参数的函数时。该函数可使用使用 dynamic_cast 判断基类指针指向的对象的类型,再执行该类型特有的操作。总之,可使用dynamic_cast在运行阶段判断类型,并在安全时使用转换后的指针。程序清单13.1使用了一个您熟悉的继承层次结构—— Tuna和Carp类从基类Fish派生而来,其中的函数DetectFishtype()动态的检查Fish指针指向的对象是否是Tuna或Carp。
这种在运行阶段识别对象类型的机制称为运行阶段类型识别(runtime type identification, RTTI)。
程序清单13.1 使用动态转换判断Fish指针指向的是否是Tuna对象或Carp对象
输出:
分析:
这是一个您在第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()。
务必检查dynamic_cast的返回值,看它是否有效。如果返回值为NULL,说明转换失败。
reinterpret_cast是C++中与C风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用如下所示的语法强制重新解释类型:
这种类型转换实际上是强制编译器接受static_cast通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为API能够接受的简单类型(例如,有些API只能使用字节流,即 unsigned char*):
上述代码使用的类型转换并没有改变源对象的二进制表示,但让编译器允许程序员访问SomeClass对象包含的各个字节。由于其他 C++类型转换运算符都不允许执行这样的转换,因此使用reinterpret_cast时,程序员将收到类型转换不安全(不可移植)的警告。
应尽量避免在应用程序中使用 reinterpret_cast,因为它让编译器将类型 X 视为不相关的类型Y,这看起来不像是优秀的设计或实现。
const_cast 让程序员能够关闭对象的访问修饰符 const。您可能会问:为何要进行这种转换?在理想情况下,程序员将经常在正确的地方使用关键字const。不幸的是,现实世界并非如此,像下面这样的代码随处可见:
在下面的函数中,以 const 引用的方式传递 mData 对象显然是正确的。毕竟,显示函数应该是只读的,不应调用非const成员函数,即不应调用能够修改对象状态的函数。然而,DisplayMembers()本应为 const 的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对DisplayMembers()进行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast将是您的救星。
在这种情况下,调用DisplayMembers()的语法如下:
除非万不得已,否则不要使用const_cast来调用非const函数。一般而言,使用const_cast来修改const对象可能导致不可预料的行为。
另外,const_cast也可用于指针:
13.4 C++类型转换运算符存在的问题
并非所有人都喜欢使用C++类型转换,即使那些C++拥趸也如此。其理由很多,从语法繁琐而不够直观到显得多余,不一而足。
来比较一下下面的代码:
在这3种方法中,程序员得到的结果都相同。在实际情况下,第2种方法可能最常见,其次是第3 种,但几乎没有人使用第 1 种方法。无论采用哪种方法,编译器都足够聪明,能够正确地进行类型转换。这让人觉得类型转换运算符将降低代码的可读性。
同样,static_cast的其他用途也可使用C风格类型转换进行处理,且更简单:
因此,使用 static_cast的优点常常被其拙劣的语法所掩盖。Bjarne Stroustrup准确地描述了这种境况:“由于static_cast如此拙劣且难以输入,因此您在使用它之前很可能会三思。这很不错,因为类型转换在现代C++中是最容易避免的。”
再来看其他运算符。在不能使用static_cast时,可使用reinterpret_cast强制进行转换;同样,可以使用const_cast修改访问修饰符const。因此,在现代C++中,除dynamic_cast外的类型转换都是可以避免的。仅当需要满足遗留应用程序的需求时,才需要使用其他类型转换运算符。在这种情况下,程序员通常倾向于使用C风格类型转换而不是C++类型转换运算符。重要的是,应尽量避免使用类型转换;而一旦使用类型转换,务必要知道幕后发生的情况。
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的答案。在继续学习下一章前,请务必弄懂这些答案。
1.您有一个基类对象指针pBase,要确定它指向的是否是Derived1或Derived2对象,应使用哪种类型转换?
2.假设您有一个指向对象的const引用,并试图通过它调用一个您编写的公有成员函数,但编译器不允许您这样做,因为该函数不是const成员。您将修改这个函数还是使用const_cast?
3.判断对错:仅在不能使用static_cast时才应使用reinterpret_cast,这种类型转换是必须和安全的。
4.判断对错:优秀的编译器将自动执行很多基于static_cast的类型转换,尤其是简单数据类型之间的转换。
1.查错:下述代码有何问题?
2.假设有一个Fish指针(pFish),它指向一个Tuna对象:
要让一个 Tuna指针指向该指针指向的 Tuna对象,应使用哪种类型转换?请使用代码证明您的看法。