最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
C++的语法是被设计成类型安全的。如果你的程序通过了编译并没有发出警告信息,就表示它并不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作。
但类型转换(cast)破坏了类型系统的安全性,C++中的类型转换不像C、Java或C#中的类型转换风险低,所以我们需要小心谨慎使用类型转换。
C++中的类型转换有三种不同的方式:
//C风格
(T)expression; //将expression转型为T
//函数风格
T(expression); //将expression转型为T
//C风格和函数风格被称为旧式转型
//C++风格
static_cast<T>(expression);
dynamic_cast<T>(expression); //T必须是指向多态类型的指针或引用
const_cast<T>(expression); //T必须是指针或引用
reinterpret_cast<T>(expression); //T必须是指针或引用
-
const_cast
:通常用来移除对象的常量性,它也是唯一有此能力的C++风格转型操作符。 -
dynamic_cast
:主要用来执行“
安全向下转换
”(safe down casting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费成本极高的转型动作。 -
reinterpret_cast
:执行低级转换,实际结果可能取决于编译器,这也就表示它不可移植。例如将 int* 转换为 int。这一类型在低级代码以外很少见。 -
static_cast
:强制隐式转换,如将非常量对象转换为常量对象,将 int 转换为 double 等等。它也可以用来执行上述多种转换的反向转换,如将 void* 指针转换为 typed 指针,将 基类指针转换为派生类指针,但它无法将常量转换为非常量——这个只有
const_cast
才办得到。
旧式转型仍然合法,但新式较受欢迎。我们唯一使用旧式转型的时机是,当调用一个 explicit 构造函数将一个对象传递给一个函数时。例如:
class Widget{
public:
explicit Widget(int size);
...
};
void doSomething(const Widget& w);
doSomething(Widget(15)); //更像构造临时对象,但也可以理解为函数风格类型转换
doSomething(static_cast<Widget>(15)); //C++风格类型转换
二者没有功能上的差异,都会把15转换为临时的Widget对象来传进函数。如果要突出创建对象,用函数风格,如果要突出类型转换,用C++风格。
许多人认为,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。这是错的,任何一个类型转换往往真的令编译器编译出运行期间执行的代码。例如下面这段程序:
int x,y;
double d = static_cast<double>(x)/y;
将 int x 转型为 double 肯定会产生一些代码,因为在大部分计算器体系结构中,int 的底层表述不同于 double 的底层表述。接下来是下面这个例子:
class Base{...};
class Derived : public Base{...};
Derived d;
Base* pb = &d; //derived*隐式转换为base*
这里我们是用一个基类指针指向派生类对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量在运行期被施行于 Derived* 指针身上,用以取得正确的 Base* 指针值。这说明了单一对象(例如一个派生类对象)可能拥有一个以上的地址(例如“以Base* 指向它”时的地址和以“Derived* 指向它”时的地址),其它语言中不可能发生这种事,但C++可能。一旦使用多重继承,这一定会发生,即使在单一继承中也可能发生。
请注意,就算你知道这两个地址间存在一个固定的偏移量,在一个平台上偏移量是这么多,不代表另一个平台上的编译器生成的偏移量也是这么多。
另一件关于转型的有趣事情是:我们容易写出某些似是而非的代码(在其它语言中也许是对的)。例如许多应用框架都要求派生类的虚函数代码的第一个动作是先调用基类对应的函数,如下面的例子:
class Window{
public:
virtual void onResize(){...}
...
};
class SpecialWindow : public Window{
public:
virtual void onResize(){
static_cast<Window>(*this).onResize();//想要把*this转换为Window再调用它的虚函数
... //执行SpecialWindow的专属操作
}
};
这段程序将
*this
转型为
Window
,因此也能调用
Window ::onResize()
。但它调用的并不是真正的当前对象
*this
上的函数,因为类型转换会生成一个基类部分的本地拷贝,所以我们只是用这份拷贝调用了
onResize()
,然后在
*this
这个类上执行
SpecialWindow
的专属操作。如果
Window ::onResize()
修改了对象内容,当前对象其实没被改动,而
SpecialWindow ::onResize()
也修改了对象内容,当前对象真的会被改动。
解决方法:
去掉转型动作
class SpecialWindow : public Window{
public:
virtual void onResize(){
Window::onResize(); //调用Window::onResize作用于*this身上
...
}
...
};
上面这个例子说明了在你打算用类型转换时,错误就可能来找你了,尤其是对于
dynamic_cast
来说。
dynamic_cast
可能会非常慢,因为有些类型检测的实现是基于字符串对比。假如你在一个4层深的单继承体系内的某个对象做
dynamic_cast
,就会导致多达4次的
strcmp
调用,而深度继承或多重继承的成本更高。所以在对类型转换保持小心谨慎的同时,更应该在注重效率的代码中对
dynamic_cast
保持小心谨慎。
dynamic_cast
的用途在于当你想对派生类执行操作时,但你只有指向基类的指针或者引用,想要避免使用耗费性能的
dynamic_cast
,有两种方法:
第一种方法:使用容器储存直接指向派生类对象的指针(经常是智能指针,
见条款13
),就消除了需要通过基类接口来操作子类的需求。假设在
Window
和
SpecialWindow
继承体系中只有
SpecialWindow
能闪烁,我们可以把以下代码:
class Window{...};
class SpecialWindow : public Window{
public:
void blink();
...
};
typedef std::vector<std::shared_ptr<Window>> VPW; //智能指针存储基类
VPW winPtrs; //通过基类指针操作子类
...
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
//如果当前对象是一个派生类,if条件里就不是null,就能调用blink()
if(SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink(); //但是条件里用了慢的dynamic_cast
}
代替为:
typedef std::vector<std::shared_ptr<SpecialWindow>> VPSW;//智能指针存储派生类
VPSW winPtrs; //现在直接使用派生类指针
...
for(VPSW::iterator = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter)->blink();//更好的代码,不用dynamic_cast
}
当然这种方法不能让你在同一个容器里储存继承体系里所有的类型对象,因为容器里储存的是派生类对象不是基类,所以如果想要保管多种类型,会需要多个这样的容器。
第二种方法:在基类内提供虚函数做你想对各个
Window
派生类做的事,例如用这种方法实现上面的例子:
class Window{
public:
virtual void blink(){} //声明为虚函数,空操作作为默认实现
...
};
class SpecialWindow : public Window{
public:
virtual void blink(){...} //派生类这里做真正的实现
...
};
typedef std::vector<std::shared_ptr<Window>> VPW;//智能指针存储基类
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter) ->blink(); //注意这里也没有dynamic_cast
}
这两种方法可能并不能适用于全部的情景,但它们确实能有效代替
dynamic_cast
,所以当可以用的时候就要尽量用。
绝对必须避免的一件事是所谓的“串联
dynamic_cast
”,也就是下面这种代码:
class Window{...};
class SpecialWindow1 : public Window{...};
class SpecialWindow2 : public Window{...};
... //其它子类
typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter){
//串联起来的dynamic_cast
if(SpecialWindow1* psw1 = dynamic_cast<SpecialWindow1*>(iter->get())){...}
else if(SpeciaWindow2* psw2 = dynamic_cast<SpecialWindow2*>(iter->get())){...}
else if(SpeciaWindow3* psw3 = dynamic_cast<SpecialWindow3*>(iter->get())){...}
...
}
这样产生出来的机器码又大又慢,而且基础不稳。因为每次
Window
继承体系一有改变,所有这类代码必须再次检查是否需要修改。
优良的C++代码很少使用转型,但又不可能完全摆脱它们。所以我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部的影响。
Note:
- 如果可以,尽量避免类型转换,特别是在注重效率的代码中避免dynamic_cast。如果有些设计需要类型转换,尽量用其它方案代替
- 如果类型转换是必要的,就把它隐藏在函数里。用户可以调用接口而自己的代码里就不会出现类型转换
- 要多用C++风格的类型转换,少用C风格类型转换。它们更容易被认识出来,且比较清楚它们的职能