More Effective C++ (2):最好使用C++转型操作符

版权声明:欢迎转载,请注明出处 https://blog.csdn.net/weixin_38339025/article/details/89086278

那些不少人常用的低阶转型动作,几乎像goto一样被视作是程序设计上的“贱民”。尽管如此,却仍苟延残喘,因为当某种情况变糟时,转型可能时必要的。

不过,旧式的C型转型方式并非时唯一选择。它几乎允许你将任何类型转换为任意其他类型,这是极为拙劣的。若每次转型都能更精确地指明意图,则更好。例如:将一个pointer-to-const-object转型为一个pointer-to-non-const-object(即只改变对象地常量性),和将一个pointer-to-base-class-object转型为一个pointer-to-derived-class-object(即完全改变一个对象地类型),其间有很大地差异。传统地C转型动作对此并无区分(毕竟C转型是为C设计,而非C++)。

此外,旧式转型地第二个问题是他们难以辨识。旧式转型的语法结构由一对小括号加上一个对象名称(标识符)组成,而小括号和对象名称是在C++的任何地方都有可能被使用。因此,我们很难对这种旧式转型进行辨别。

为此,为解决这些缺点,C++导入了4个新的转型操作符(cast operators):static_cast,const_cast,dynamic_cast,reinterpret_cast。对大部分使用目的而言,对于应用到转型时应写为:

static_cast<type>(expression)

例如将一个int转型为double,以强迫一个整数表达式导出一个浮点数值。旧式C转型为:

int firstNumber,secondNumber;
...
double result=((double)firstNumber)/secondNumber;

而采用C++转型则为:

double result=static_cast<double>(firstNumber)/secondNumber;

这种C++风格的转型不管是对程序员还是机器,都十分容易辨认。

static_cast基本上拥有C旧式转型相同的威力与意义,以及相同的限制。例如,不能够利用static_cast将一个struct转型为int,或讲一个double转型为pointer;static_cast甚至不能移除表达式的常量性(constness),因为有一个新式转型操作符const_cast专司其值。
其他新式 C++ 转型操作符适用于更集中(范围更狭窄)的目的。const_cast 用来改变表达式中的常量性(constness)或变易性(volatileness)。使用 const_cast, 便是对程序员(以及编译器)强调 ,通过此转型操作符,你唯一打算改变的是某物的常量性或变易性。这项意愿将由编译器贯彻执行。如果你将 const_cast 应用于上述以外的用途,那么转型动作会被拒绝。例如:

class Widget { ... };  
class SpecialWidget: public Widget { ... };  
void update(SpecialWidget *psw);  
SpecialWidget sw;                 		// sw 是个 non-const 对象,  
const SpecialWidget& csw = sw;   // csw 却是一个代表 sw 的 reference, 并视之为一个 const 对象。  
 
update(&csw);           // 错误!不能将 const SpecialWidget* 传给一个需要 SpecialWidget* 的函数。  
 
update(const_cast<SpecialWidget*>(&csw));  
                         // 可!&csw 的常量性被去除了。也因此,csw(亦即 sw)在此函数中可被更改。  
 
update((SpecialWidget*)&csw);  
                         // 情况同上,但使用的是较难辨识  
                         // 的 C 旧式转型语法。  
 
Widget *pw = new SpecialWidget;  
update(pw);            // 错误!pw 的类型是 Widget*,但update() 需要的却是 SpecialWidget*。  
 
update(const_cast<SpecialWidget*>(pw));  
                         // 错误!const_cast 只能用来影响常量性或变易性,无法进行继承体系的向下转型(cast down)动作。 

显然,const_cast 最常见的用途就是将某个对象的常量性去除掉。

第二个特殊化的转型操作符是dynamic_cast,用来执行继承体系中"安全的向下转型或跨系转型动作"。也就是说**你可以利用 dynamic_cast,将"指向 base class objects的pointers或references"转型为"指向derived(或sibling base)class objects 的 pointers 或 references",并得知转型是否成功 **。如果转型失败,会以一个 null 指针(当转型对象是指针)或一个 exception(当转型对象是 reference)表现出来:

Widget *pw;  
...  
update(dynamic_cast<SpecialWidget*>(pw));  
                         // 很好,传给 update() 一个指针,指向pw 所指的 SpecialWidget--如果 pw  
                         // 真的指向这样的东西;否则传过去的将是一个 null 指针。  
 
void updateViaRef(SpecialWidget& rsw);  
 
updateViaRef(dynamic_cast<SpecialWidget&>(*pw));  
                         // 很好,传给 updateViaRef() 的是pw 所指的 SpecialWidget--如果  
                         // pw 真的指向这样的东西;否则抛出一个 exception。 

dynamic_cast 只能用来协助你巡航于继承体系之中。它无法应用在缺乏虚函数的类型身上,也不能改变类型的常量性(constness):

int firstNumber, secondNumber;  
...  
double result =  
dynamic_cast<double>(firstNumber)/secondNumber;     // 错误!未涉及继承机制。  
const SpecialWidget sw;  
...  
update(dynamic_cast<SpecialWidget*>(&sw));               // 错误!dynamic_cast 不能改变常量性。 

如果你想为一个不涉及继承机制的类型执行转型动作,可使用 static_cast;要改变常量性(constness),则必须使用 const_cast。
最后一个转型操作符是** reinterpret_cast。此操作符转换结果几乎总是与编译平台息息相关。所以 reinterpret_casts 不具移植性。**
reinterpret_cast 的最常用用途是转换"函数指针"类型。假设有一个数组,存储的都是函数指针,有特定的类型:

typedef void (*FuncPtr)();      // FuncPtr 是个指针,指向某个函数. 后者无须任何自变量,返回值为 void。  

FuncPtr funcPtrArray[10];       // funcPtrArray 是个数组,内有 10 个 FuncPtrs。 

假设由于某种原因,你希望将以下函数的一个指针放进 funcPtrArray 中:

int doSomething(); 

如果没有转型,不可能办到这一点,因为 doSomething 的类型与 funcPtrArray 所能接受的不同。funcPtrArray 内各函数指针所指函数的返回值是 void,但 doSomething 的返回值却是 int:

funcPtrArray[0] = &doSomething;     // 错误!类型不符。使用 reinterpret_cast,可以强迫编译器了解你的意图。

funcPtrArray[0] =reinterpret_cast<FuncPtr>(&doSomething);                      // 这样便可通过编译。  

函数指针的转型动作,并不具移植性(C++ 不保证所有的函数指针都能以此方式重新呈现),某些情况下这样的转型可能会导致不正确的结果,所以你应该尽量避免将函数指针转型,除非你已走投无路,像是被逼到墙角。

如果你的编译器尚未支持这些新式转型动作,你可以使用传统转型方式来取代 static_cast,const_cast 和 reinterpret_cast。甚至可以利用宏(macros)来仿真这些新语法。

#define static_cast(TYPE,EXPR)       ((TYPE)(EXPR))  
#define const_cast(TYPE,EXPR)        ((TYPE)(EXPR))  
#define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR)) 

上述新语法的使用方式如下:
double result = static_cast(double, firstNumber)/secondNumber;  
update(const_cast(SpecialWidget*, &sw));  
funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething); 

这些近似法当然不像其本尊那么安全,但如果你现在就使用它们,一旦你的编译器开始支持新式转型,程序升级的过程便可简化。
至于 dynamic_cast,没有什么简单方法可以模拟其行为,不过许多程序库提供了一些函数,用来执行继承体系下的安全转型动作。如果你手上没有这些函数,而却必须执行这类转型,你也可以回头使用旧式的 C 转型语法,但它们不可能告诉你转型是否成功。当然,你也可以定义一个宏,看起来像 dynamic_cast,就像你为其他转型操作符所做的那样:

#define dynamic_cast(TYPE,EXPR)     ((TYPE)(EXPR)) 

这个近似法并非执行真正的 dynamic_cast,所以它无法告诉你转型是否成功。

这些新式转型操作符看起来十分冗长。如果实在不想用,值得安慰的是 C 旧式转型语法仍然可继续使用。然而这么一来也就丧失了新式转型操作符所提供的严谨意义与易辨识度。如果你在程序中使用新式转型法,比较容易被解析(不论是对人类还是对工具而言),编译器也因此得以诊断转型错误(那是旧式转型法侦测不到的)。这些都是促使我们舍弃 C 旧式转型语法的重要因素。至于可能的第三个因素是:让转型动作既丑陋又不易键入(typing),或许未尝不是件好事。

猜你喜欢

转载自blog.csdn.net/weixin_38339025/article/details/89086278
今日推荐