C++ 在设计中一直强调类型安全,而且也采取了一定的措施来保障这条准则的执行。但是,从C继承而来的强制转型却破坏了C++类型系统,C中的强制转型可谓是“无所不能”,其超强的能力给C++带来了很大的安全隐患。强制转型会引起各种各样的麻烦,有时这些麻烦很容易被察觉,有时它们却又隐藏极深,难以察觉。
在C/C++语言中,强制转型是“一个你必须全神贯注才能正确使用”的特性。所以一定要慎用强制转型。
首先来回顾一下C 风格(C-style)的强制转型语法,如下所示:
- // 将表达式的类型转换为 T
- (T) expression
- T(expression)
这两种形式之间没有本质上的区别。在C++中一般称为旧风格的强制转型。
在赋值时,强制类型的转换形式会让人觉得不精密、不严格,缺乏安全感,主要是因为不管表达式的值是什么类型,系统都自动将其转为赋值运算符左侧变量的类型。而转变后数据可能会有所不同,若不加注意,就可能产生错误。
将较大的整数转换为较短的数据类型时,会产生无意义的结果,而程序员可能被蒙在鼓里。正如下面的代码片段所示:
- unsigned i = 65535;
- int j = (int) i;
输出结果竟然成了-1。较长的无符号类型在转换为较短的有符号类型时,其数值很可能会超出较短类型的数值表示范围。编译器不会监测这样的错误,它所能做的仅仅是抛出一条非安全类型转换的警告信息。如果这样的问题发生在运行时,那么一切会悄无声息,系统既不会中断,也不会出现任何的出错信息。
类似的问题还会发生在有符号负数转化为无符号数、双精度类型转化为单精度类型、浮点数转化为整型等时候。以上这些情况都属于数值的强制转型,在转换过程中,首先生成临时变量,然后会进行数值截断。
在标准C中,强制转型还有可能导致内存扩张与截断。这是因为在标准C中,任何非void类型的指针都可以和void类型的指针相互指派,也就可以通过void类型指针这个中介,实现不同类型的指针间接相互转换了。代码如下所示:
- double PI = 3.1415926;
- double *pd = Π
- void *temp = pd;
- int *pi = temp; //转换成功
指针pd指向的空间本是一个双精度数据,8字节。但是经过转换后,pi却指向了一个4字节的int类型。这种发生内存截断的设计缺陷会在转换后进行内存访问时存在安全隐患。不过,这种情况只会发生在标准C中。在C++中,设计者为了杜绝这种错误的出现,规定了不同类型的指针之间不能相互转换,所以在使用纯C++编程时大可放心。而如果C++中嵌入了部分C代码,就要注意因强制转型而带来的内存扩张或截断了。
与旧风格的强制转型相对应的就是新风格的强制转型了,在C++提供了如下四种形式:
- const_cast(expression)
- dynamic_cast(expression)
- reinterpret_cast(expression)
- static_cast(expression)
新风格的强制转型针对特定的目的进行了特别的设计,如下所示。
const_cast<T*> (a)
它用于从一个类中去除以下这些属性:const、volatile和 __unaligned。
- class A { // … };
- void Function()
- {
- const A *pConstObj = new A;
- A *pObj = pConstObj; //ERROR: 不能将const对象指针赋值给非const对象
- pObj = const_cast<A*>( pConstObj); // OK
- //...
- }
这种强制转型的目的简单明确,使用情形比较单一,易于掌握。
dynamic_cast<T*>(a)
它将a值转换成类型为T的对象指针,主要用来实现类层次结构的提升,在很多书中它被称做“安全的向下转型(Safe Downcasting)”,用于继承体系中的向下转型,将基类指针转换为派生类指针,这种转换较为严格和安全。如下面的代码片段所示:
- class B { //... };
- class D : public B { //... };
- void Function(D *pObjD)
- {
- D *pObj = dynamic_cast<D*>( pObjD);
- //...
- }
如果pObjD指向一个D类型的对象,pObj则指向该对象,所以对该指针执行D类型的任何操作都是安全的。但是,如果pObjD指向的是一个B类型的对象,pObj将是一个空指针,这在一定程度上保证了程序员所需要的“安全”,只是,它也付出了一定的运行时代价,而且代价非常大,实现相当慢。有一种通用实现是通过对类名称进行字符串比较来实现的,只是其在继承体系中所处的位置越深,对strcmp的调用就越多,代价也就越大。如果应用对性能要求较高,那么请放弃dynamic_cast。
reinterpret_cast<T*>(a)
它能够用于诸如One_class* 到 Unrelated_class*这样的不相关类型之间的转换,因此它是不安全的。其与C风格的强制转型很是相似。
- class A { // ... };
- class B { //... };
- void f()
- {
- A* pa = new A;
- B* pb = reinterpret_cast<B*>(pa);
- // ...
- }
在不了解A、B内存布局的情况下,强行将其进行转换,很有可能出现内存膨胀或截断。
static_cast<T*>(a)
它将a的值转换为模板中指定的类型T。但是,在运行时转换过程中,它不会进行类型检查,不能确保转换的安全性。如下面的代码片段所示:
- class B { ... };
- class D : public B { ... };
- void Function(B* pb, D* pd)
- {
- D* pd2 = static_cast<D*>(pb); // 不安全
- B* pb2 = static_cast<B*>(pd); // 安全的
- }
之所以说第一种是不安全的,是因为如果pb指向的仅仅是一个基类B的对象,那么就会凭空生成继承信息。至于这些信息是什么、正确与否,无从得知。所以对它进行D类型的操作将是不安全的。
C++是一种强类型的编程语言,其规则设计为“保证不会发生类型错误”。在理论层面上,如果希望程序顺利地通过编译,就不应该试图对任何对象做任何不安全的操作。不幸的是,继承自C语言的强制转型破坏了类型系统,所以建议尽量少地使用强制转型,无论是旧的C风格的还是新的C++风格的。如果发现自己使用了强制转型,那么一定要小心,这可能就是程序出现问题的一个信号。
请记住:
由于强制转型无所不能,会给C++程序带来很大的安全隐患,因此建议在C++代码中,努力将强制转型减到最少。