我们都知道,在C++中有很多类型转换。今天在这里,我们不讨论普通变量的类型转换(比如int转换成double等等)。本文主要讨论面向对象相关的类型转换:向上转换和向下转换。
首先,我们定义一个基类Base和继承类Derived,相关代码如下:
//Base.h class Base { public: int i; Base(); void func1(); virtual void func2(); virtual ~Base(); }
//Derived.h class Derived: public Base { public: int i; Derived(); void func1(); virtual void func2(); virtual ~Derived(); }
//Derived.cpp Derived::Derived() { cout<<"Constructor Derived"<<endl; } void Derived::func1() { cout<<"Derived func1"<<endl; } void Derived::func2() { i = 3; cout<<"Derived func2"<<endl; }
//Base.cpp Base::Base() { i = 1; cout<<"Base Constructor"<<endl; } void Base::func1() { cout<<"Base func1"<<endl; } void Base::func2() { cout<<"Base func2"<<endl; }
向上转换:指的是子类向基类的转换。即:Derived向Base的转换。
向下转换:指的是基类向子类的转换。即:Base向Derived的转换。
此时,我们需要注意的是:除了一种特殊情况外(我们会在下面讲),所有的向上、向下转换指的都是指针或是引用的转换,而不是普通对象的转换!
首先,在此处我们简要的介绍一下变量的静态类型和动态类型(运行时类型)。由于OOP的引入,使得指针或引用变量存在了这两种类型,在它们声明的时候所标明的类型就是该变量的静态类型,而在程序运行到某处时该变量的实际类型就是动态类型,而普通的变量则只存在静态类型。
正是因为对于指针或引用变量存在着两种变量类型,这种根据运行时的实际类型来调用对应的虚函数的方式,可以简单地理解为动态束定。这也就是OOP中多态机制能够实现的基础。而如果只有普通变量、对象,则就不存在多态(C++中)。
所以我们有如下代码片段,这种将子类转为父类的方式,将父类指针指向子类指针的方式,称为向上转换。向上转换是隐式转换。
Derived *_pD = new Derived(); Base *_pB = _pD;
而下面的代码片段,将父类转换为子类的方式,将子类指针指向父类的方式,称为向下转换。向下转换是必须强制转换的。而且,向下转换的方式是存在一定的危险的,使用时要极其小心的!
Base _oB; Derived *_pD = (Derived*)&_oB;
下面,我们来通过一段代码来理解一下类型转换时的动态类型和静态类型,以及向下转换的危险性。
1 //Main.cpp 2 void main() 3 { 4 5 Derived _oD; 6 Base _oB; 7 8 //segment 1 9 Base *_pB1 = &_oD; 10 _pB1->func1(); 11 _pB1->func2(); 12 cout << _pB1->i << endl; 13 cout << endl; 14 15 //segment 2 16 Derived *_pD1 = (Derived*)&_oB; 17 _pD1->func1(); 18 _pD1->func2(); 19 cout << _pD1->i << endl; 20 21 }
在segment1中,我们进行向上转换,输出的结果如下:
由于func1是普通的函数,所以此处在编译时就已经确定,根据变量的静态类型调用,即调用Base的func1();而func2是虚函数,运行时绑定,根据运行时的实际动态类型来调用,运行时_pB1已经绑定到一个Derived类型对象上,所以此时调用Derived中的func2();最后的变量i也是根据静态类型决定。
在segement2中,我们进行向下转换,输出结果如下:
由于func1是普通的函数,所以此处在编译时就已经确定,根据变量的静态类型调用,即调用Derived的func1();而func2是虚函数,运行时绑定,根据运行时的实际动态类型来调用,运行时_pD1已经绑定到一个Base类型对象上,所以此时调用Base中的func2()。
此处的变量i的值需要讨论,由于变量i的值也是根据静态类型决定,所以应该是Derived中的变量i的值,由于最开始在Derived的构造函数中没有对i进行初始化,所以它的值是未定义的乱值。此时,细心的同学肯定主要到了,我们在Derived的func2中对变量i的值进行了修改,但是似乎没有起到效果。这是由于,func2是虚函数,此时_pD1调用的是Base中的func2函数,因此它修改的相应的是Base域中的i的值,而最后输出时的i,仍是由变量的静态类型决定,输出的仍是Derived域中的i,仍是未初始化的。这也是之前我们提到的,为什么强制进行向下转换存在一定的危险性。
dynamic_cast的引入
但是,我们都知道在实际工程中是要涉及到向下转换的,那么存在这样多的危险性怎么处理。C++中引入了dynamic_cast<type>()。它通过判断在执行到该语句的时候两个变量的运行时类型是否相同来判断是否能够进行向下转换。假设,我们有如下的三个类的关系。
class Geometry; class Line: public Geometry; class Curve: public Geometry;
那么,我们就可以在方法中这么写:
void doSomething(Geometry *_piGeom) { if(dynamic_cast<Line*>(_piGeom)) ... //do something for Line type else if(dynamic_cast<Curve*>(_piGeom)) ... //do something for Curve type }
这样就可以避免未定义行为的产生,也可以实现相应的需求。
向上转换的切割(Slice)
最后,回到我们最开始的时候提出的,我们说除了一种特殊情况外(我们会在下面讲),所有的向上、向下转换指的都是指针或是引用的转换,而不是普通对象的转换。向上转换也可以应用于对象之间,如下:
Derived _oD; Base _oB = _oD;
此时,相当于使用子类对象中的父类部分来对父类对象进行初始化赋值,实际是就是一种切割。