C++多态 & 虚函数 & 虚析构 & 覆盖 & 虚表

来源:互联网 发布:网络不稳定不能玩dnf 编辑:程序博客网 时间:2024/05/22 08:10

多态概述

讲到多态,那什么是多态呢?

所谓多态,简单来说就是,当发出一条命令的时候,不同的对象接收到同样的命令之后,所做出的动作是不同的,那么我们就把这种情况称之为多态。

虚函数及其实现原理

上一小节,我们笼统了解释了什么是多态,现在,我们来看一看书本上是如何来定义多态的,如下:指相同对象收到不同消息或不同对象收到相同消息时产生不同的动作

为了能够把这个概念说清楚,我们简而言之,其实就是在说两个概念:静态多态和动态多态。

静态多态(早绑定)

所谓静态多态,也称之为早绑定在我们之前的课程中其实就已经涉及到并且已经应用了,只不过当时怕用这个概念把大家搞晕,就没有给大家提这个概念。那么,这节课就给大家讲一讲什么是静态多态,为什么把静态多态也叫做早绑定。接下来看下面这个例子。

在这个例子中,我们定义了一个矩形类(Rect),并且在这个类中,还定义了两个函数,然而这两个成员函数的名字是相同的,都是叫做calcArea(计算矩形面积),但是这两个函数的参数个数不同,那么对于这两个函数,我们之前称之为互为重载的函数。那么,作为互为重载的两个函数来说,当我们去实例化一个矩形的对象rect之后,我们就可以通过这个对象来分别调用这两个函数。

在调用时,对于第一个函数,我们传入一个参数,对于第二个函数,我们传入两个参数,那么,计算机在编译的时候就会自动的调用相应的函数,那么,大家会发现,程序在运行之前的编译阶段就已经确定下来,到底要使用哪个函数了。可见,很早的就已经将函数编译进去了,那么,我们就把这种情况叫做早绑定,也叫做静态多态。

动态多态(晚绑定)

那么什么是动态多态呢?我们还是从一个例子开始讲起。比如,当前我们下达一道指令----计算面积。于是,就给圆形这个类下达了这道指令,让它来计算面积,同时,我们又向矩形这个类也下达了这道指令,也让它来计算面积。那么,对于圆形和矩形来说,它们分别有自己的计算面积的方法,可见,这两种方法肯定是不同的。

 

这种情况就是,对于不同的对象下达相同的指令,但是却做着不同的操作。而动态多态是有着前提的,它必须以封装和继承为基础。那么,动态多态起码是有两个类:一个是子类,一个是父类。当然,也可以有三个类,只有使用三个类的时候,动态多态才表现得更加明显。我们还是从代码的角度给大家讲解动态多态的例子。

我们先来看一看,适用我们之前所学习的一些代码是否能够实现动态多态呢?比如,我们在这里定义了一个形状类(Shape),在这个类当中,定义了一个成员函数叫做计算面积(calcArea)。

然后,我们再定义两个类:一个Circle类(圆),一个Rect类(矩形)。这两个类都以public方式继承Shape类,如下:

接下来我们所要讲的重点就是:在main函数中去使用的时候,我们可以使用父类的指针shape1去指向其中的一个子类对象Circle,并且用另外一个父类的指针shape2去指向一个矩形的对象。从而,这两个子类对象都被它的父类指针所指向,而我们进行操作的时候呢,是通过shape1和shape2分别调用计算面积的函数calcArea(),如下所示:

大家想一想,如果你去尝试一下的话,你肯定知道结果,而结果呢,其实并不是我们所想要的,因为要用到的都是父类的计算面积,也就是说,在屏幕上会打印出来年该行计算面积出来,即“calcArea()”的字样。那么,如果想要实现动态多态,看来以前我们所学习到的这些知识是没法实现的了。所以,接下来要学习一个新的知识:虚函数,即采用关键字virtual来修饰类的成员函数。实际当中,怎么来实现呢。我们还是以刚刚的例子来说明问题。我们在刚才的例子中,提到了计算面积(calcArea)这个函数,我们需要在父类(Shape)中去定义成员函数的时候,就把我们想要实现多态的成员函数前面加virtual关键字,使其成员虚函数,如下所示:

然后呢,我们在去定义它的子类的时候,给它计算面积的成员函数前面也要加上关键字virtual(此时这个关键字virtual不是必须要加上的,如果不加,系统会自动给你加上,如果加上了,我们会在后续的适用当中看得更加明显,所以,最好还是加上的好),使其成为虚函数,如下所示:

最后,就是关键的main函数中的使用了。

那么,在main函数当中,用父类指针再去指向子类对象的时候呢,如果用shape1调用calcArea()的时候呢,那么调用的就是Circle这个类自己的calcArea()函数,当然也会计算出实际的圆的面积出来。同理,用shape2调用calcArea()的时候呢,那么调用的就是Recr这个类自己的calcArea()函数,也会计算出矩形的面积来。


结果分析:

我们要实例化一个Rect的对象,显然就要先执行其父类的构造函数,然后再执行其本身的构造函数,同理,要实例化一个Circle的对象,首先也要执行其父类的构造函数,然后再执行其本身的构造函数,这就是前面四行代码的打印结果。接下来的两行就验证了我们之前所讲的一样,通过shape1和shape2去调用calcArea()函数的时候,调用到的都是Shape这个父类的calcArea()函数,并没有像我们想象中那样去调用Rect或Circle中的calcArea()函数,这个问题该如何来解决呢??。最后,在销毁Shape1和shape2的时候,只执行了Shape这个父类的析构函数,而并没有执行Rect和Circle本身的析构函数,这是为什么呢??(这就是虚析构知识点了!!!)。

针对执行结果中问题,我们前面已经学过,可以通过虚函数来达到多态的效果。也就是在计算面积(calcArea())函数前加上关键字virtual即可。当我们在三个头文件中的calcArea()函数前加上virtual及Shape虚函数前加virtual后的运行结果如下:






虚函数虚表的原理(覆盖的概念)

我们就可以一起来学习多态的实现原理了。我们先来看一个例子。

在这个例子中,我们定义了一个Shape类,在这个Shape类中,还定义了一个虚函数和一个数据成员。然后,又定义了一个Circle子类(public继承自Shape)。大家请注意,我们在这里并没有给Circle定义一个计算面积的虚函数,也就是说,Circle这个子类所使用的也应该是Shape的虚函数来计算面积。当我们这样定义完成之后,我们来想一想,此时的虚函数如何来实现呢?

当我们去实例化一个Shape的对象的时候,在这个Shape对象当中,除了数据成员m_iEdge(表示边数)之外,还有另外一个数据成员-----虚函数表指针,它也是一个指针,占有4个基本内存单元。顾名思义,虚函数表指针就指向一个虚函数表。这个虚函数表会与Shape类的定义同时出现。在计算机中,虚函数表也是占有一定的内存空间的,这里假设虚函数表的起始位置是0xCCFF,那么这个虚函数表指针的值vftable_ptr就是0xCCFF。父类的虚函数表只有一个。通过父类实例化出来的所有的对象的虚函数表指针的值都是0xCCFF,以确保它的每一个对象的虚函数表指针都指向自己的虚函数表。在父类Shape的虚函数表当中,肯定定义了一个这样的函数指针,这个函数指针就是计算面积(calcArea())这个函数的入口地址。这里假设计算面积函数的入口地址是0x3355,那么虚函数表中的函数指针(calcArea_ptr)的值就是0x3355。调用的时候就可以先找到虚函数表指针,再通过虚函数指针找到虚函数表,再通过位置的偏移找到相应的虚函数的入口地址,从而最终找到当前定义的这个虚函数----计算面积(calcArea())。整个过程如下所示:

当我们去实例化Circle的时候,又会是怎样的呢?如果我们实例化一个Circle对象,因为Circle当中并没有定义虚函数,但是它却从父类Shape当中继承了虚函数,所以我们在实例化Circle这个对象的时候,也会产生一个虚函数表。请大家注意,这个虚函数表是Circle自己的虚函数表,它的起始地址是0x6688。但是在Circle的虚函数表当中,它的计算面积的函数指针(calcArea_ptr)却是一样的,都是0x3355,这就能够保证:在Circle当中去访问父类的计算面积的函数,也能够通过虚函数表指针找到自己的虚函数表,在自己的虚函数表中找到的计算面积的函数指针也是指向父类的计算面积的函数入口的。整个过程如下所示:

那么,如果我们在Circle中定义了计算面积的函数(如下所示),又会是怎样的呢?

我们来看一看,对于Shape这个类来说,它的情况是不变的,有自己的虚函数表,并且在实例化一个Shape的对象之后,通过虚函数表指针指向自己的虚函数表,然后虚函数表当中有一个指向计算面积的函数,这样就Ok了。对于Circle来说,则有些变化。如下所示:

Circle的虚函数表与之前的虚函数表示一样的,但是,因为Circle此时自己已经定义了自己的计算面积的函数,所以它的虚函数表中关于计算面积的这个函数指针已经覆盖掉了父类当中的原有的指针的值。换句话说,0x6688当中的计算面积的函数指针的值变成了0x4B2C,而Shape当中的0xCCFF这个虚函数表中的所记录的计算面积的函数指针的值则是0x3355,这两者是不一样的。于是,我们如果用Shape的指针去指向Circle对象,那么,它就会通过Circle对象当中的虚函数表指针找到Circle的虚函数表,通过Circle的虚函数表(偏移量也是一样的),和父类一样,就能够找到Circle的虚函数的函数入口地址,从而执行子类当中的虚函数,这个就是多态的原理。

函数的覆盖

在我们还没有学习多态的时候,如果定义了父类和子类。当父类和子类出现了同名函数,那么这时就称之为函数的隐藏。

函数的覆盖是今天要讲的知识。怎么就覆盖了呢?大家请注意:

如果我们没有在子类当中定义同名的虚函数,那么在子类虚函数表当中就会写上父类的相应的那个虚函数的函数入口地址。如果,在子类当中也定义了同名的虚函数,那么在子类的虚函数表当中就会把原来的父类的虚函数的函数入口地址覆盖一下,覆盖成子类的虚函数的函数地址,那么这种情况就称之为函数的覆盖。


虚析构函数的实现原理

虚析构函数的特点是:当我们在父类当中,通过virtual修饰析构函数之后,我们通过父类的指针再去指向子类的对象,然后通过delete接父类指针就可以释放掉子类的对象。

理论前提:执行完子类的析构函数就会执行父类的析构函数。

有了这个前提,我们想一想,如果有了父类的指针,通过delete的方式去释放子类的对象,那么只要能够实现通过父类的指针执行到子类的析构函数就可以实现了。我们来看一看例子:

在这个例子当中,我们给Shape类多加了一个函数:虚析构函数。在Circle类当中,我们也定义了它自己的虚析构函数。如果你不写,计算机会默认给你定义一个虚析构函数的,前提是,必须在父类中必须有关键字virtual修饰的析构函数。如果我们在main函数当中,通过父类的指针来指向子类的对象(如下)

然后,通过delete接父类的指针来释放子类的对象,那么这个时候虚函数表如何来工作呢?我们来看一看。

如果我们在父类当中定义了虚析构函数,那么,在父类当中的虚函数表中就会有一个父类的析构函数的函数指针,而在子类的虚函数表中也会产生一个子类的析构函数的函数指针,指向的是子类的析构函数。这个时候,如果我们使用父类的指针指向子类的对象,或者说,使用Shape的指针来指向Circle的对象,那么,通过deleite来接shape这样一个指针的时候,我们就可以用shape来找到子类的虚函数表指针,然后通过虚函数表指针找到虚函数表,再通过虚函数表找到子类的析构函数,从而使得子类的析构函数得以执行。子类的析构函数执行完毕之后,系统就会自动执行父类的析构函数,这个就是虚析构函数的实现原理。接下来向大家证明一下:虚函数表指针的存在



原创粉丝点击