在上一篇文档中,谈到子类型通过按值传递,按地址传递到以基类的行参中去,这称为”upcast”,因为子类型的对象也是基类的对象,所以理所当然就能被传递,只不过这样就丧失了子类的特征。这样的转换不需要任何的机制。
但是如果基类可不可以转换称为子类呢,也就是说,基类对象可不可以调用子类的方法呢?当然可以,需要通过虚函数实现,这就是鼎鼎大名的”多态”(polymorphism)。先看下面的用法:
- #include
-
using namespace std;
-
enum note {middleC,Csharp,Cflag};
-
class Instrument {
-
public:
-
virtual void play(note)
-
{cout<<"Instrument::play"<
-
virtual ~Instrument(){}
-
};
-
class Wind:public Instrument{
-
public:
-
void play(note){
-
-
cout<<"Wind::play"<
-
}
-
~Wind(){}
-
};
-
void tune(Instrument& i){
-
i.play(middleC);
-
}
-
-
-
int main()
-
{
-
Wind flute;
-
tune(flute);
-
Instrument *p = new Wind;
-
p->play(middleC);
-
delete p;
-
return 0;
-
}
执行上面程序,结果如下:
- Wind::play
-
Wind::play
-
Wind::destructor
-
Instrument::destructor
-
Wind::destructor
-
Instrument::destructor
上面大致就是虚函数的用法,讲所需要进行向下转换的函数标记virtual关键字后,就能进行基类向子类的转换,不过实参必须是子类型或者基类的指针但实际指向为子类。
下面开始具体讨论
在编译其间就能决定所需要调用的具体函数,这称之为”早期绑定(静态绑定)”,而在运行时决定所需要调用的具体函数,称为”晚期绑定(动态绑定)”,而虚函数机制就是所谓的动态绑定,关键字virtual就是告诉编译器动态绑定,它通过建立虚表(VTABLE)来完成,其中的每一项就是virtual标识的每一个函数,顺序就是基类函数声明的顺序,另外还有一个vptr,虚表指针来指向具体的虚表VTALBE,每一个类缺一不可,这样就完成了具体的调用行为。
- #include <iostream>
-
using namespace std;
-
-
class NoVirtual
-
{
-
int a;
-
public:
-
void x() const{}
-
int i() const {return 0;}
-
};
-
-
class OneVirtual
-
{
-
int a;
-
public:
-
virtual void x() const {}
-
int i()const {return 0;}
-
};
-
class OneVirtualInherit:public OneVirtual
-
{
-
public:
-
void x() const{}
-
int i()const{return OneVirtual::i();}
-
};
-
class TwoVirtual
-
{
-
int a;
-
public:
-
virtual void x() const{}
-
virtual int i() const{return 0;}
-
};
-
int main()
-
{
-
cout<<"int: "<<sizeof(int)<<endl;
-
cout<<"NoVirtual: "<<sizeof(NoVirtual)<<endl;
-
cout<<"void* :"<<sizeof(void*)<<endl;
-
cout<<"OneVirtual: "<<sizeof(OneVirtual)<<endl;
-
cout<<"OneVirtualInherit:"<<sizeof(OneVirtualInherit)<<endl;
-
cout<<"TwoVirutal: "<<sizeof(TwoVirtual)<<endl;
-
return 0;
-
}
执行结果如下:
- int: 4
-
NoVirtual: 4
-
void* :4
-
OneVirtual: 8
-
OneVirtualInherit:8
-
TwoVirutal: 8
可以看出,安装了虚拟函数后,明显增加了4个字节,这就是vptr(虚表指针)。
继续第一个例子,在通过基类指针指向子类后,就会讲基类的虚表指针VPTR指向子类的虚表起始地址vtable,实现的调用图(第二个方法为虚构)如下:
进行对象初始时,虚函数指针的初始化,虚表的建立过程就在构造函数中实现,所以C++不准构造函数被定义成虚函数形式(编译报错),而在构造函数中调用,不过这些都是自动完成,但需要考虑效率问题:
编译器隐藏插入到构造函数中,初始化虚表指针VPTR,检查this的值(防止operator
new 返回0),
调用基类构造函数,这些加上去,可能会抵消内联构造函数的避免函数调用开销,当出现虚函数时,最好不要内联形式的构造函数。
如果调用内联函数,可以下面形式:
- public:
-
Wind(){lay(middleC);}
-
Wind(int i){
-
Instrument *p = new Wind();
-
cout<<"call in constructor"<<endl;
-
p->play(middleC);
-
cout<<"end call"<<endl;
-
delete p;
-
}
上面的无参数构造函数明显调用的是本地play()函数,而Wind(int
i)在一开始就调用无参数构造函数实际上的vtabl已经初始化完成了,之后的调用肯定能实现动态绑定。当对象生成后,构造函数被首先调用,基类的构造函数被调用,接着就是构造函数本身,等等。虚表被初始化,而虚表指针VPTR则指向初始化自己的本地虚表,而基类的虚表指针VPTR则指向派生类的虚表,这样讲来,基类虚表指针是在子类初始化一步步完成的,这样最后一个子类的构造函数调用决定基类虚表指针的指向。
当虚拟函数与重载函数相结合时,基类出现重载而重载函数为虚拟函数,如果没有被子类实现的函数将会被自动隐藏,而且与普通函数不同的是,这些被重载的函数可以修改行参,如果行参改变,也就意味着子类函数不能调用,但不能修改返回类型。
如果在继承基类后,在子类里添加一些虚拟函数,编译器就会为子类创建一个新的VTABLE,使用任何没有被改写的虚函数作为其中的项,并且添加新的项。但是如果用基类的指针来调用子类新增加的虚函数肯定出错,这样可以通过dynamic_cast进行转换。
- #include <iostream>
-
#include <string>
-
-
using namespace std;
-
-
class Pet{
-
string pname;
-
public:
-
Pet(const string& petName):pname(petName){}
-
virtual string name()const{return pname;}
-
virtual string speak()const{return "";}
-
};
-
-
class Dog:public Pet{
-
string name;
-
public:
-
Dog(const string& petName):Pet(petName){}
-
//New virtual function in the Dog class
-
virtual string sit()const {return Pet::name()+" sits";}
-
string speak() const{return Pet::name()+" says 'Bark!'";}
-
};
-
int main()
-
{
-
Pet *p[]={new Pet("generic"),new Dog("bob")};
-
cout<<"p[0]->speak()="<<p[0]->speak()<<endl;
-
cout<<"p[1]->speak()="<<p[1]->speak()<<endl;
-
cout<<"p[1]->sit()"<<(dynamic_cast<Dog*>(p[1]))->sit()<<endl;
-
return 0;
-
}
运行结果如下:
- p[0]->speak()=
-
p[1]->speak()=bob says '
-
p[1]->sit()bob sits
阅读(914) | 评论(0) | 转发(0) |