类型转换是一种机制,让程序员能够暂时或永久性改变编译器对对象的解释。但这并不意味着程序员改变了对象本身,而只是改变了对对象的解释。可改变对象解释方式的运算符称为类型转换运算符。
如果 C++应用程序都编写得很完善,其处于类型是安全的且是强类型的世界,则没有必要进行类型转换,也不需要类型转换运算符。然而,在现实世界中,不同模块往往由不同的人编写,而使用不同开发环境的厂商需要协作。因此,程序员经常需要让编译器按其所需的方式解释数据,让应用程序能够成功编译并正确执行。
C++提供了一种新的类型转换运算符,专门用于基于继承的情形,4 个 C++类型转换运算符如下:
- static_cast
- dynamic_cast
- reinterpret_cast
- const_cast
这 4 个类型转换运算符的使用语法相同:
destination_type result = cast_operator<destination_type> (object_to_cast);
1.使用 static_cast
static_cast 用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换——这种转换原本将自动或隐式地进行。用于指针时,static_cast 实现了基本的编译阶段检查,确保指针被转换为相关类型。这改进了 C 风格类型转换,在 C 语言中,可将指向一个对象的指针转换为完全不相关的类型,而编译器不会报错。使用 static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型,如下面的示例代码所示:
Base* objBase = new Derived ();
Derived* objDer = static_cast<Derived*>(objBase); // ok
Unrelated* notRelated = static_cast<Unrelated*>(objBase); // Error
将 Derived*转换为 Base*被称为向上转换,无需使用任何显式类型转换运算符就能进行这种转换:
Derived objDerived;
Base* objBase = &objDerived; // ok!
将 Base*转换为 Derived*被称为向下转换,如果不使用显式类型转换运算符,就无法进行这种转换:
Derived objDerived;
Base* objBase = &objDerived; // Upcast -> ok!
Derived* objDer = objBase; // Error: Downcast needs
explicit cast
然而,static_cast 只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用static_cast 编写如下代码,而编译器不会报错:
Base* objBase = new Base();
Derived* objDer = static_cast<Derived*>(objBase); // Still no errors!
其中 objDer 实际上指向一个不完整的 Derived 对象,因为它指向的对象实际上是 Base()类型。由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此 objDer -> DerivedFunction()能够通过编译,但在运行阶段可能导致意外结果。
除用于向上转换和向下转换外,static_cast 还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读者的注意:
double Pi = 3.14159265;
int num = static_cast<int>(Pi); // Making an otherwise implicit cast, explicit
在上述代码中,使用 num = Pi 将获得同样的效果,但使用 static_cast 可让代码阅读者注意到这里 使用了类型转换,并指出(对知道 static_cast 的人而言)编译器根据编译阶段可用的信息进行了必要的调整,以便执行所需的类型转换。对于使用关键字 explicit 声明的转换运算符和构造函数,要使用它们,也必须通过 static_cast。
2.使用 dynamic_cast 和运行阶段类型识别
与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。可检查 dynamic_cast 操作的结果,以判断类型转换是否成功。使用 dynamic_cast 运算符的典型语法如下:
destination_type* Dest = dynamic_cast<class_type*>(Source);
if(Dest) // Check for success of the casting operation
{
Dest->CallFunc();
}
举个例子:
Base* objBase = new Derived();
Derived* objDer = dynamic_cast<Derived*>(objBase);
if(objDer) // Check for success of the cast
{
objDer->CallDerivedFunction();
}
如上述代码所示,给定一个指向基类对象的指针,程序员可使用 dynamic_cast 进行类型转换,并在使用指针前检查指针指向的目标对象的类型。在上述示例代码中,目标对象的类型显然是 Derived,因此这些代码只有演示价值。然而,情况并非总是如此,例如,将 Derived*传递给接受 Base*参数的函数时。该函数可使用 dynamic_cast 判断基类指针指向的对象的类型,再执行该类型特有的操作。总之,可使用 dynamic_cast 在运行阶段判断类型,并在安全时使用转换后的指针。这种在运行阶段识别对象类型的机制称为运行阶段类型识别(runtime type identification, RTTI)。
如下使用了一个继承层次结构—Tuna 和 Carp 类从基类 Fish 派生而来,其中的函数 DetectFishtype( )动态地检查 Fish 指针指向的对象是否是 Tuna 或 Carp。
#include <iostream>
using namespace std;
class Fish
{
public:
virtual void Swim()
{
cout << "Fish swims in water" << endl;
}
// base class should always have virtual destructor
virtual ~Fish() {}
};
class Tuna : public Fish
{
public:
void Swim()
{
cout << "Tuna swims real fast in the sea" << endl;
}
void BecomeDinner()
{
cout << "Tuna became dinner in Sushi" << endl;
}
};
class Carp : public Fish
{
public:
void Swim()
{
cout << "Carp swims real slow in the lake" << endl;
}
void Talk()
{
cout << "Carp talked Carp!" << endl;
}
};
void DetectFishType(Fish* objFish)
{
Tuna* objTuna = dynamic_cast <Tuna*>(objFish);
if (objTuna) // check success of cast
{
cout << "Detected Tuna. Making Tuna dinner: " << endl;
objTuna->BecomeDinner();
}
Carp* objCarp = dynamic_cast <Carp*>(objFish);
if (objCarp)
{
cout << "Detected Carp. Making carp talk: " << endl;
objCarp->Talk();
}
cout << "Verifying type using virtual Fish::Swim: " << endl;
objFish->Swim(); // calling virtual function Swim
}
int main()
{
Carp myLunch;
Tuna myDinner;
DetectFishType(&myDinner);
cout << endl;
DetectFishType(&myLunch);
return 0;
}
这个示例使用了这样的继承层次结构:Tuna 和 Carp 从基类 Fish 派生而来。为了方便解释,这两个派生类不仅实现了虚函数 Swim( ),还分别包含一个特有的函数,即 Tuna::BecomeDinner( )和 Carp::Talk( )。这个示例的独特之处在于,给定一个基类指针(Fish*), 您可动态地检测它指向的是否是 Tuna 或 Carp。这种动态检测(运行阶段类型识别)是在函数 DetectFishType( ) 中进行的。使用 dynamic_cast 传入的基类指针(Fish*)参数指向的是否是 Tuna 对象。如果该 Fish*指向的是 Tuna 对象,该运算符将返回一个有效的地址,否则将返回 NULL。因此,总是需要检查 dynamic_cast 的结果是否有效。如果通过了检查,您便知道指针 objTuna 指向的是一个有效的 Tuna 对象,因此可以使用它来调用函数 Tuna::BecomeDinner( )。如果传入的 Fish*参数指向的是 Carp 对象,则使用它来调用函数 Carp::Talk( )。返回之前, DetectFishType( )调用了 Swim( ),以验证对象类型;Swim( )是一个虚函数,这行代码将根据指针指向的对象类型,调用相应类(Tuna 或 Carp)中实现的方法 Swim( )。
注意:务必检查 dynamic_cast 的返回值,看它是否有效。如果返回值为 NULL,说明转换失败。
3.使用 reinterpret_cast
reinterpret_cast 是 C++中与 C 风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用如下所示的语法强制重新解释类型:
Base* objBase = new Base ();
Unrelated* notRelated = reinterpret_cast<Unrelated*>(objBase);
// The code above compiles, but is not good programming!
这种类型转换实际上是强制编译器接受 static_cast 通常不允许的类型转换,通常用于低级程序(如驱动程序), 在这种程序中,需要将数据转换为 API (应用程序编程接口)能够接受的简单类型(例如, 有些 OS 级 API 要求提供的数据为 BYTE 数组,即 unsigned char*):
SomeClass* object = new SomeClass();
// Need to send the object as a byte-stream...
unsigned char* bytesFoAPI = reinterpret_cast<unsigned char*>(object);
上述代码使用的类型转换并没有改变源对象的二进制表示,但让编译器允许程序员访问 SomeClass 对象包含的各个字节。由于其他 C++类型转换运算符都不允许执行这种有悖类型安全的转换,因此除非万不得已,否则不要使用 reinterpret_cast 来执行不安全(不可移植)的转换。
4.使用 const_cast
const_cast 让程序员能够关闭对象的访问修饰符 const。在理想情况下,程序员将经常在正确的地方使用关键字 const。但现实世界并非如此,像下面这样的代码随处可见:
class SomeClass
{
public:
// ...
void DisplayMembers(); //problem - display function isn't const
};
在下面的函数中,以 const 引用的方式传递 object 显然是正确的。毕竟,显示函数应该是只读的, 不应调用非 const 成员函数,即不应调用能够修改对象状态的函数。然而,DisplayMembers()本应为 const 的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对 DisplayMembers()进 行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast 将会是您修改的方式。
void DisplayAllData (const SomeClass& object)
{
object.DisplayMembers (); // Compile failure
// reason: call to a non-const member using a const reference
}
在这种情况下,调用 DisplayMembers()的语法如下:
void DisplayAllData (const SomeClass& object)
{
SomeClass& refData = const_cast<SomeClass&>(object);
refData.DisplayMembers(); // Allowed!
}
除非万不得已,否则不要使用 const_cast 来调用非 const 函数。一般而言,使用 const_cast 来修改 const 对象可能导致不可预料的行为。
另外,const_cast 也可用于指针:
void DisplayAllData (const SomeClass* data)
{
// data->DisplayMembers(); Error: attempt to invoke a non-const function!
SomeClass* pCastedData = const_cast<SomeClass*>(data);
pCastedData->DisplayMembers(); // Allowed!
}
5.C++类型转换运算符存在的问题
来比较一下下面的代码:
double Pi = 3.14159265;
// C++ style cast: static_cast
int num = static_cast <int>(Pi); // result: Num is 3
// C-style cast
int num2 = (int)Pi; // result: num2 is 3
// leave casting to the compiler
int num3 = Pi; // result: num3 is 3. No errors!
在这 3 种方法中,程序员得到的结果都相同。在实际情况下,第 2 种方法可能最常见,其次是第 3 种,但几乎没有人使用第 1 种方法。无论采用哪种方法,编译器都足够聪明,能够正确地进行类型转换。这让人觉得类型转换运算符将降低代码的可读性。
同样,static_cast 的其他用途也可使用 C 风格类型转换进行处理,且更简单:
// using static_cast
Derived* objDer = static_cast <Derived*>(objBase);
// But, this works just as well...
Derived* objDerSimple = (Derived*)objBase;
因此,使用 static_cast 的优点常常被其拙劣的语法所掩盖。Bjarne Stroustrup 准确地描述了这种境况:“由于 static_cast 如此拙劣且难以输入,因此您在使用它之前很可能会三思。这很不错,因为类型转换在现代 C++中是最容易避免的。”
再来看其他运算符。在不能使用 static_cast 时,可使用 reinterpret_cast 强制进行转换;同样,可以使用 const_cast 修改访问修饰符 const。因此,在现代 C++中,除 dynamic_cast 外的类型转换都是可以避免的。仅当需要满足遗留应用程序的需求时,才需要使用其他类型转换运算符。在这种情况下,程序员通常倾向于使用 C 风格类型转换而不是 C++类型转换运算符。重要的是,应尽量避免使用类型转换;而一旦使用类型转换,务必要知道幕后发生的情况。
注意
- 将 Derived*转换为 Base*被称为向上转换;这种转换是安全的。
- 将 Base*转换为 Derived*被称为向下转换;除非使用 dynamic_cast 并核实转换成功,否则这种转换不安全。
- 创建继承层次结构时,应尽量将函数声明为虚函数。这样通过基类指针调用这些函数时,如果该指针指向的是派生类对象,将调用相应类的函数版本。