这一篇主要是想讲解一下C++中的多态性,这也是我在学习Win32和MFC编程中碰到的,算是总结一下吧。
首先来看一段程序:
class CObject{
public:
virtual void Serialize(){
cout<<"CObject::Serial() /n/n";
}
};
class CDocument: public CObject{
public :
int m_data1 ;
void func(){
cout<<"CDocument::func()"<<endl;
Serialize();
}
virtual void Serialize(){
cout<<"CDocument::Serial() /n/n";
}
};
class CMyDoc : public CDocument {
public:
int m_data2;
virtual void Serialize(){
cout<<"CMyDoc::Serialize() /n/n";
}
};
int main(void){
CMyDoc myDoc ;
CMyDoc* pMyDoc = new CMyDoc();
cout<<"#1 testing"<<endl;
myDoc.func();
cout<<"#2 testing"<<endl;
((CDocument*)(&myDoc))->func();
cout<<"#3 testing"<<endl;
pMyDoc->func();
cout<<"#4 testing"<<endl;
((CDocument)myDoc).func();
return 0;
}
从程序中可以看出这样的继承关系:
CMyDoc -> CDocument -> CObject,这里要注意由于继承关系的存在,CMyDoc类中其实是存在如下的成员函数和变量的:
1、func() 从CDocument继承得到的
2、m_data1也是从CDocument继承得到的
但是CMyDoc和CDocument都重写了各自父类的虚函数Serialize().
它的运行结果为:
#1 testing
CDocument::func()
CMyDoc::Serialize()
#2 testing
CDocument::func()
CMyDoc::Serialize()
#3 testing
CDocument::func()
CMyDoc::Serialize()
#4 testing
CDocument::func()
CDocument::Serial()
从前三个运行结果可以得出这样的结论:由于myDoc是CMyDoc类,而pMyDoc是指向CMyDoc的类的指针,两者都是和MyDoc类有关联的,所以前三种情况在调用CDocument::func()中的Serialize()时,由于子类CMyDoc中已经重写了父类的Serialize(),所以都会最终落实到对子类CMyDoc::Serialize()的调用,而不是执行父类CDocument::Serialize().
但是在执行第四个测试时,情况不一样了,这里直接把CMyDoc类型对象upcast强制转化为了CDocument类型对象,这种由子类强制转化为父类的过程,就称为对象切割。
一般情况下,从内存占用的角度来看,子类对象要比父类对象大,因为子类会从父类那边继承相关的成员变量以及成员函数,同时又会在自己类内部增加自己的成员变量以及成员函数。所以这里当通过 ((CDocument)myDoc).func();调用Serialize()时,调用的就是CDocument::Serial() 了,我的理解是子类CMyDoc::Serialize() 在进行upcast的时候,把这些自己的信息丢失掉了。
好,接下来就要说一下,虚函数这样的机制是如何实现的。
虚函数其实是一种动态绑定机制,因为在编译时,编译器是不知道是该调用父类中的虚函数还是子类中的虚函数的,而是在程序执行过程中,动态确定的。虚函数的本质是,C++编译器透过某个表格,在执行时期「间接」调用实际上欲绑定的函数(注意「间接」这个字眼)。这样的表格称为虚拟函数表(常被称为vtable)。每一个「内含虚拟函数的类」,编译器都会为它做出一个虚拟函数表,表中的每一笔元素都指向一个虚拟函数的地址。此外,编译器当然也会为类别加上一项成员变量,是一个指向该虚拟函数表的指针(常被称为vptr)。
每一个由此类衍生出来的对象,都有这么一个vptr。当我们透过这个对象调用虚拟函数,事实上是透过vptr 找到虚拟函数表,再找出虚拟函数的真正地址。
好了,到这里我们至少对虚函数的实现机制有了一个补充了解,那么像上面示例程序的原理是怎么一回事呢?
奥妙在于这个虚拟函数表以及这种间接调用方式。虚拟函数表的内容是依据类别中的虚拟函数声明次序,一一填入函数指针。衍生类别会继承基础类别的虚拟函数表(以及所有其它可以继承的成员),当我们在衍生类别中改写虚拟函数时,虚拟函数表就受了影响:表中元素所指的函数地址将不再是基础类别的函数地址,而是衍生类别的函数地址。
这就是为什么,在前三个测试中,CDocument::func()函数中调用Serilize()时,调用的都是被子类CMyDoc重写的Serilize()虚函数。
那么在具体的程序设计中我们应该如何利用虚函数所具有的性质,以达到接口统一的目的的?
方法如下:
在基类中声明一个虚函数(最好,声明成纯虚函数,这样基类就成为了抽象基类),但不用声明它的方法体,让所有继承于基类的子类重写这个虚函数。以后要想统一调用这些子类的这个接口函数时,只要先获得抽象基类的指针,然后获取各个子类对象的地址,赋值给基类的指针,最后通过基类指针调用这个接口函数。
我来举个例子吧,这样比较清晰:
ofstream out("out.txt");
class CShape{
public :
virtual void display() =0;
};
class CCircle : public CShape{
public:
virtual void display(){
out<<"Display Circle /n/n";
}
};
class CRectangle : public CShape{
public:
virtual void display(){
out<<"Display Rectangle /n/n";
}
};
class CStar : public CShape {
public:
virtual void display(){
out<<"Display Star /n/n";
}
};
int main(void) {
CShape* array[]={
new CRectangle(),
new CCircle(),
new CStar()
};
int arraySize = sizeof(array)/sizeof(*array[0]); //3
cout<<arraySize<<endl;
for(int i=0 ; i < arraySize ; i++)
array[i]->display();
return 0;
}
运行结果如下,重点就是在main函数中的array数组,呵呵,就是这么方便。
Display Rectangle
Display Circle
Display Star
好,接下来,就说说几个关于虚函数的小总结吧:)这些都是从《深入浅出MFC》中的,呵呵。
1、 如果你期望衍生类别重新定义一个成员函数,那么你应该在基础类别中把此函 数设为virtual。
2、以单一指令唤起不同函数,这种性质称为Polymorphism,意思是"the ability to assume many forms",也就是多态。
3、既然抽象类别中的虚拟函数不打算被调用,我们就不应该定义它,应该把它设为纯虚拟函数(在函数声明之后加上"=0" 即可)
4、抽象类别不能产生出对象实体,但是我们可以拥有指向抽象类别之指针,以便于操作抽象类别的各个衍生类别。
虚拟函数衍生下去仍为虚拟函数,而且可以省略virtual 关键词。
好,接下来我们再看一个例子,这个例子也是关于父类与子类的:
ofstream out("out.txt");
class CShape{
public :
void display();
void OutputName(){
out<<"Shape /n/n";
}
};
class CCircle : public CShape{
public:
void display(){
out<<"Display Circle /n/n";
}
void OutputName(){
out<<"Circle /n/n";
}
void hello(){
out<<"hello !/n/n";
}
};
int main(void) {
CShape* shape;
CCircle circle;
shape = &circle;
shape->OutputName();
//shape->hello();
return 0;
}
运行结果如下:
Shape
从这样的一个小程序可以看出,如果将CCircle的对象地址赋值给它的父类CShape的指针,那么这个指针只能调用父类CShape中的一些成员函数,而不能调用子类CCircle中的成员函数。所以可以得出下面的几个结论:
1、 如果你以一个「基础类别之指针」指向「衍生类别之对象」,那么经由该指针你只能够调用基础类别所定义的函数。
2、 如果你以一个「衍生类别之指针」指向一个「基础类别之对象」,你必须先做明显的转型动作(explicit cast)。这种作法很危险,不符合真实生活经验,在程序设计上也会带给程序员困惑。
3、 如果基础类别和衍生类别都定义了「相同名称之成员函数」,那么透过对象指针调用成员函数时,到底调用到哪一个函数,必须视该指针的原始型别而定,而不是视指针实际所指之对象的型别而定。
综上所述,我们对C++中的多态和虚函数机制,以及父类之类指针变换后的结果有了更深的认识了。