本篇文章引用材料:
Scott Meyers<<Effective C++>>第三版---侯捷译
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。然而,转型破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。在C++中转型是一个你会想带着极大尊重去亲近的一个特性。
C风格的转型动作(旧式转型):
(T)expression //将expression 转型为T
函数风格转型动作:
T(expression) //将expression 转型为T
C++提供的四种新式转型(常被称为new-style或C++-style casts):
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
各有不同的目的:
const_cast通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符。
dynamic_cast注要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。dynamic_cast运算符可以在执行期决定真正的类型。
reinterpret_cast取决于编译器,也就是表示它不可移植。
static_cast 用来强迫隐式转换(implicit conversions),如将int转换为double,将non-const对象转换为const对象等等。
旧式转型仍然合法,但新式转型较受欢迎。原因是:1. 它们很容易在代码中被辨识出来(不论是人工识别或使用工具如grep),因而得以简化“找出类型系统在哪个地点被破坏”的过程。2. 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。举个例子,如果你打算将常量性去掉,除非使用新式转型的const_cast否则无法通过编译。
许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。这是错误的观念。任何一种类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间指行的码。
class Base{.....};
class Derived:public Base{......};
Derived d;
Base *pb = &d; //隐喻地将Derived* 转换为Base*
这里我们不过是建立一个base class指针指向一个derived class对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量(offset)在运行期被施行于Derived*指针身上,用于取得正确的Base*指针值。
上个例子表明,单一对象(例如一个类型为Derived的对象可能拥有一个以上的地址,实际上一旦使用多重继承,这事几乎一直发生着。即使在单一继承中也可能发生。)
这里我写个程序解释下,相关知识可参考,Stanley B.Lippman<<Inside The C++ Object Model>>----侯捷译,相关章节第三章 Data语意学。
class Base1{
public:
int b1;
};
class Base2{
public:
int b2;
};
class Derived:public Base1,public Base2{
public:
};
int main(int argc,char* argv[]){
Derived d;
Base1 *pb1 = &d;
Base2 *pb2 = &d;
cout<<"&d = "<<&d<<endl;
cout<<"pb1 = "<<pb1<<endl;
cout<<"pb2 = "<<pb2<<endl;
system("Pause.");
return 0;
}
运行程序观察,可发现d 和pb1的地址是一样的。
pb1 = &d;
//虚拟的C++码
pb1 = (Base1*)((char*)&d) ;
pb2 = &d;
//虚拟的C++码
pb2 = (Base2*)((char*)&d + sizeof(Base2));
另一件关于转型动作的有趣事情是:我们很容易写出某些似是而非的代码(在其他语言也许真是对的)。例如许多应用框架(Application frameworks)都要求derived class内的virtual函数代码的第一个动作就先调用base class的对应函数。假设我们有个Window base class和一个 SpecialWindow derived class,两者都定义了virtual函数onResize。进一步假设SpecialWindow的OnResize函数被要求首先调用Window的OnResize。下面是实现方式之一,它看起来对,但实际上是错误的:
class Window{
public:
virtual void OnResize(){cout<<"Window::OnResize()\n";}
};
class SpecialWindow:public Window{
public:
void OnResize(){
static_cast<Window>(*this).OnResize();//
cout<<"SpecialWindow::OnResize()\n";
}
};
int main(int argc,char* argv[]){
Window *pw;
SpecialWindow sp;
pw = &sp;
pw->OnResize();
system("Pause.");
return 0;
}
一如你所预期的,这段程序将*this转型为Window,对函数OnResize的调用也因此调用了Window::OnResize。但恐怕你没有想到的,它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个"*this 对象之base class成分"的暂时副本身上的OnResize!(译注:函数就是函数,成员函数只有一份,"调用其那个对象身上的函数"有什么关系呢?关键在于成员函数都有个隐藏的this指针,会因此影响成员函数操作的数据。)在说一次,上述代码并非在当前对象身上调用Window::OnResize之后又在该对象身上执行SpecialWindow专属动作。不,它是在“当前对象之base class成分”的副本上调用Window::OnResize,然后在当前对象身上执行SpecialWindow专属动作。如果Window::OnResize修改了对象内容(不能说没有可能性,因为OnResize是个non-const成员函数),当前对象其实没被改动,改动的是副本。然而SpecialWindow::OnResize内如果也修改对象,当前对象真的会被改动。这使当前对象进入一种“伤残”状态:其base class成分更改没有落实,而derived class成分更改倒是落实了。
可写程序测试:
class Window{
private:
int w;
public:
Window(int ww):w(ww){ }
virtual void OnResize(){
w = 1111111;
//cout<<"Window::OnResize()\n";
}
int getw(){return w;}
};
class SpecialWindow:public Window{
private:
int s;
public:
SpecialWindow(int ww,int ss):Window(ww),s(ss){ }
void OnResize(){
static_cast<Window>(*this).OnResize();
s = 222222;
// cout<<"SpecialWindow::OnResize()\n";
}
int gets(){ return s;}
};
int main(int argc,char* argv[]){
SpecialWindow sp(1,2);
cout<<"Before cast: \n";
cout<<"w = "<<sp.getw()<<endl;;
cout<<"s = "<<sp.gets()<<endl;
cout<<"After cast:\n";
sp.OnResize();
cout<<"w = "<<sp.getw()<<endl;;
cout<<"s = "<<sp.gets()<<endl;
system("Pause.");
return 0;
}
static_cast<Window>(*this)相当于旧式转换的Window(*this),这下明白了吧, Window(*this)将调用Window的copy构造函数,创造出一个临时对象来!你可以试着往Window添加copy构造函数和析构函数就会明白了。
解决之道:
class SpecialWindow:public Window{
public:
void OnResize(){
Window::OnResize();// 调用 Window::OnResize()作用于*this身上
cout<<"SpecialWindow::OnResize()\n";
}
};
在探究dynamic_cast设计意涵之前,值得注意的是,dynamic_cast的许多实现版本指向速度相当慢。例如至少有一个很普遍的实现版本基于“class名称之字符串比较”,如果你四层深的单继承体系内的某个对象身上执行dynamic_cast,刚才说的那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcmp调用,用于比较class名称。深度继承或多重继承的成本更高!某些实现版这样做有原因(它们必须支持动态链接)。然而我还是要强调,除了对一般转型保持机敏与猜疑,更应该在注重效率的代码中对dynamic_cast保持机敏与猜疑。