条款9 新式转型操作符
在旧式转型(cast)下面隐藏着一些见不得人的、鬼鬼祟祟的东西。它们的语法形式使其在一段代码中通常很难引起人们的注意,但它们可能会搞一些可怕的破坏活动,就好比你冷不丁被一个恶棍猛击一拳似的。让我们阐明旧式转型的含义。显然,在最初的C语法中,在表达式中将类型加括号就是旧式转型:
- char *hopeItWorks = (char *)0x00ff0000; // 旧式转型
C++(www.cppentry.com)引入了另一种转型,即采用函数形式的转型语法来表达同样的意思:
- typedef char *PChar;
- hopeItWorks =
- PChar( 0x00ff0000 ); // 函数形式/旧式转型
函数形式的转型也许看上去比它那可怕的老祖先斯文一些,但实际上同样龌龊,应该像躲避瘟疫一样躲避它们。
诚实可靠的程序员使用新式转型操作符,因为它们能够更精确地表达意思。有四个新式转型操作符,每一个都有着特定的用途。
const_cast操作符允许添加或移除表达式中类型的const或volatile修饰符:
- const Person *getEmployee() { ... }
- //...
- Person *anEmployee = const_cast<Person *>(getEmployee());
在以上代码中,使用const_cast来剥除getEmployee返回类型中的const修饰符。可以使用旧式转型获得同样的效果:
- anEmployee = (Person *)getEmployee();
但使用const_cast的做法更好,这有几方面的原因。首先,它看上去丑陋、醒目,在代码中非常引人注目。这是件好事,因为任何形式的转型都存在危险。所以,它们写起来应该很痛苦,只有当不得不使用时才键入它们。它们还应该易于发现,因为无论何时当代码中出现bug时,人们应该首先检查转型这个“惯犯”。其次,const_cast要比旧式转型的威力小,因为它只影响类型修饰符。这个限制同样是件好事,因为它允许我们更精确地表达意图。使用旧式转型等于告诉编译器,“你给我闭嘴,因为我希望将getEmployee的返回类型转换为Person *”。而使用const_cast则等于告诉编译器:“我只是希望将getEmployee的返回类型的const去掉。”就目前来看,这两种语句没有大的差别(尽管实际上它们都很无礼),但是对getEmployee函数进行了一些维护性的修改后,情况就不同了:
- const Employee *getEmployee(); // 大修改!
旧式转型所强加的规则现在仍然有效,编译器不会对从const Employee *到Person *这个不正确的转换作出反应,但如果使用const_cast,编译器将会发出抱怨,因为这么剧烈的转换已经超出了它的能力范围。简而言之,const_cast优于旧式转型,因为它更丑陋、更难用,并且威力较小。
static_cast操作符用于相对而言可跨平台移植的转型。最常见的情况是,它用于将一个继承层次结构中的基类的指针或引用,向下转型为一个派生类的指针或引用(参见“能力查询”[条款27]):
- Shape *sp = new Circle;
- Circle *cp = static_cast<Circle *>(sp); // 向下转型
在这个例子中,使用static_cast将会产生正确的代码,因为sp确实指向一个Circle对象。然而,如果sp指向其他类型的Shape①,那么当使用cp时,很可能会得到某种运行期错误。因此,重申一遍,虽然这种新式转型操作符比旧式转型安全一些,但还不够安全。
注意, static_cast无法像const_cast那样改变类型修饰符。这意味着有时需要使用由两个新式转型操作符所构成的转型序列,来获得单个旧式转型所能达到的效果:
- const Shape *getNextShape() { ... }
- //...
- Circle *cp =
- static_cast<Circle *>(const_cast<Shape *>(getNextShape()));
标准并没有对reinterpret_cast的行为提供太多的保证,不过它通常的行为可以从它的名字中看出来。它从位(bit)的角度来看待一个对象,从而允许将一个东西看作另一个完全不同的东西:
- hopeItWorks = // 把int假装成指针
- reinterpret_cast<char *>(0x00ff0000);
- int *hopeless = // 把char *假装成int *
- reinterpret_cast<int *>(hopeItWorks);
这类东西在低层编码里偶尔非用不可,但它可能不具移植性。你要慎重行事。注意区分当分别使用reinterpret_cast和static_cast将指向基类的指针向下转型为指向派生类的指针时的行为。reinterpret_cast通常只是将基类指针假装成一个派生类指针而不改变其值,而static_cast(以及旧式转型——从这个角度来说)则将执行正确的地址操作(参见“指针比较的含义”[条款28])。
在类层次结构的范畴谈转型,就会涉及到dynamic_cast。dynamic_cast通常用于执行从指向基类的指针安全地向下转型为指向派生类的指针(请参考“能力查询”[条款27])。不同于static_cast的是,dynamic_cast仅用于对多态类型进行向下转型(也就是说,被转型的表达式的类型,必须是一个指向带有虚函数的类类型的指针),并且执行运行期检查工作,来判定转型的正确性。当然,这种安全性的获得是要付出代价的。使用static_cast通常无需付出(或付出极少)运行期代价,而使用dynamic_cast则意味着要付出显著的运行期开销。
- const Circle *cp =
- dynamic_cast<const Circle *>( getNextShape() );
- if( cp ) { ... }
如果getNextShape返回一个指向Circle(或者从Circle公有派生的东西,换句话说,一些和Circle之间存在着is-a关系的东西。参见“多态”[条款2])的指针,那么转型就是成功的,并且cp将会指向一个Circle。否则cp将为空。注意,我们可以将声明和测试结合于同一个表达式中:
- if( const Circle *cp
- = dynamic_cast<const Circle *>(getNextShape()) ) { ... }
这样做是有好处的,因为它将变量cp的作用域限制在if语句之内,因此,当不再使用它时,cp将会离开作用域(并被销毁)。
有关dynamic_cast一个不太常见的用法是对引用类型执行向下转型:
- const Circle &rc = dynamic_cast<const Circle &>(*getNextShape());
这个操作类似于对指针类型的dynamic_cast操作,不过如果转型失败,操作符将抛出一个std::bad_cast异常而不是仅仅返回一个空指针(记住,不存在空引用!参见“引用是别名而非指针”[条款5])。习惯上,对一个指针进行dynamic_cast等于在说:“这个Shape指针真的指向一个Circle吗?如果不是,我可以处理这种情况”。而对一个引用执行dynamic_ cast则等于声明一个不变式(invariant):“这个Shape应该是一个Circle,否则,肯定是哪儿出了严重的错误!”
与其他新式转型操作符相比,dynamic_cast只是偶尔需要使用,但因为它背上了“安全”的名声,所以常常被滥用。参见“Factory Method模式”[条款30]以便了解一个滥用的例子。