详解C++中的多态、虚函数、父类子类

 这一篇主要是想讲解一下C++中的多态性,这也是我在学习Win32和MFC编程中碰到的,算是总结一下吧。

 

首先来看一段程序:

 

#include using namespace std; class CObject{ public: virtual void Serialize(){ cout<<"CObject::Serial() /n/n"; } }; class CDocument: public CObject{ public : int m_data1 ; void func(){ cout<<"CDocument::func()"<func(); cout<<"#3 testing"<func(); cout<<"#4 testing"<

 

从程序中可以看出这样的继承关系:

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()虚函数。

 

那么在具体的程序设计中我们应该如何利用虚函数所具有的性质,以达到接口统一的目的的?

方法如下:

在基类中声明一个虚函数(最好,声明成纯虚函数,这样基类就成为了抽象基类),但不用声明它的方法体,让所有继承于基类的子类重写这个虚函数。以后要想统一调用这些子类的这个接口函数时,只要先获得抽象基类的指针,然后获取各个子类对象的地址,赋值给基类的指针,最后通过基类指针调用这个接口函数。

 

我来举个例子吧,这样比较清晰:

#include #include using namespace std; 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<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 关键词。

 

 

好,接下来我们再看一个例子,这个例子也是关于父类与子类的:

 

#include #include using namespace std; 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++中的多态和虚函数机制,以及父类之类指针变换后的结果有了更深的认识了。


 

你可能感兴趣的:(C/C++)