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

原创 2011年05月20日 21:46:00

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

 

首先来看一段程序:

 

 

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

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

 

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

方法如下:

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

 

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

 

运行结果如下,重点就是在main函数中的array数组,呵呵,就是这么方便。

 

Display Rectangle

 

Display Circle

 

Display Star

 

好,接下来,就说说几个关于虚函数的小总结吧:)这些都是从《深入浅出MFC》中的,呵呵。

1、 如果你期望衍生类别重新定义一个成员函数,那么你应该在基础类别中把此函 数设为virtual。

2、以单一指令唤起不同函数,这种性质称为Polymorphism,意思是"the ability to  assume many forms",也就是多态。

3、既然抽象类别中的虚拟函数不打算被调用,我们就不应该定义它,应该把它设为纯虚拟函数(在函数声明之后加上"=0" 即可)

4、抽象类别不能产生出对象实体,但是我们可以拥有指向抽象类别之指针,以便于操作抽象类别的各个衍生类别。
     虚拟函数衍生下去仍为虚拟函数,而且可以省略virtual 关键词。

 

 

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

 

 

运行结果如下:

 

Shape

从这样的一个小程序可以看出,如果将CCircle的对象地址赋值给它的父类CShape的指针,那么这个指针只能调用父类CShape中的一些成员函数,而不能调用子类CCircle中的成员函数。所以可以得出下面的几个结论:

1、 如果你以一个「基础类别之指针」指向「衍生类别之对象」,那么经由该指针你只能够调用基础类别所定义的函数。

2、 如果你以一个「衍生类别之指针」指向一个「基础类别之对象」,你必须先做明显的转型动作(explicit cast)。这种作法很危险,不符合真实生活经验,在程序设计上也会带给程序员困惑。

3、 如果基础类别和衍生类别都定义了「相同名称之成员函数」,那么透过对象指针调用成员函数时,到底调用到哪一个函数,必须视该指针的原始型别而定,而不是视指针实际所指之对象的型别而定。

 

综上所述,我们对C++中的多态和虚函数机制,以及父类之类指针变换后的结果有了更深的认识了。


 

相关文章推荐

C++父类子类中虚函数的使用

构造函数不能是虚函数,因为在调用构造函数创建对象时,构造函数必须是确定的,所以构造函数不能是虚函数。析构函数可以是虚函数。 1.父类Father.h:#pragma once class Fathe...
  • ameyume
  • ameyume
  • 2011年03月29日 14:01
  • 9738

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

c++子类与父类的关系

重载:1.参数个数、类型不同。      2.返回类型不同。继承:class fish:public animal            子:   父      注:构造时先构造父函数,再是子函数。 ...

C++中父类成员变量和子类成员变量重复定义及访问方法的深入探究

在用C++做工程时,继承是C++最常用的特性之一,因为它会使得工程复用性很高。但是现在存在一个问题,例如我们在使用MFC库时,往往会去继承库里面的一个类,但是我们并不去看父类里面的实现代码,所以在定义...

C++使用虚函数的时候,子类也要使用virtual关键字吗

父类使用虚函数是为了让子类重写,那子类重写的时候也需要带virtual关键字吗?比如: class Base{ virtual bool init(); }; class Derive...

C++中父类的虚函数必须要实现吗?

一、情景 C++中父类的虚函数必须要实现吗? class Vir{ public: virtual void tryVirtual(); }; class CVir:public Vir{...

C++ 在继承中虚函数、纯虚函数、普通函数,三者的区别

1.虚函数(impure virtual)   C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。   子类可以重写父类的虚函数实现子类的特殊化。   如...
  • ybhjx
  • ybhjx
  • 2016年06月30日 10:45
  • 2178

C++子类 父类的相互转换 和 虚函数

今天在程序中遇到一个问题,关于子类 父类的强制转换的。查了下网络,大概弄懂了些,记录下来作为笔记。        先看一个例子 【引自雁南飞的博客】在C++的世界中有这样两个概念,...

详谈C++虚函数表那回事(一般继承关系)

C++一般继承关系中虚函数表的深度展现

详谈C++虚函数表那回事(一般继承关系)

C++一般继承关系中虚函数表的深度展现
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:详解C++中的多态、虚函数、父类子类
举报原因:
原因补充:

(最多只允许输入30个字)