(一)什么是多态
1.现实中的多态:
所谓的多态在我们的生活当中其实很常见。举一个简单的例子:当我们需要买票的时候有很多种不同的票可以供我们购买,如果你是学生就可以享受半价票的优惠,如果你是VIP用户就可以享受八五折的优惠,如果你是烈士家属就可以享受优先购票的权利.......那么像这种针对于同一个窗口,但是由不同的人,来进行相同的购票操作,最终得到不同的结果的现象就叫做多态。
2.C++当中的多态
在C++当中多态一般发生在继承的过程当中。如果我们想要实现我们上述说明的功能就需要使用C++当中有关于多态的语法。设置一个虚函数,之后对函数进行重写,使得我们不同的对象在调用相同的虚函数的时候可以输出不同的结果。C++当中的多态效果如下:
我们会发现通过上述的代码我们已经实现了我们在显示当中所描述的场景,但是大家肯定对上述的代码有很多不理解的地方,不要着急,慢慢学习即可。
(二)多态的定义及实现
多态实际上就是不同继承关系的类对象,在调用同一个函数,做出了不同行为的现象。比如我们上面编写的代码当中学生类以及VIP类等都继承了购票窗口类。之后又在本身的类当中重新编写了一个同名的函数:购票函数。之后通过调用就实现了多态。
想要构成多态我们需要满足两个条件:
1、存在虚函数,并且在派生类当中对虚函数进行重写操作
2、通过基类的指针或者引用进行调用
(1)虚函数
那么什么是虚函数呢?所谓的虚函数其实很简单,就是在我们需要的函数前面加上virtual关键字即可。加上virtual关键字的函数就会被认定为虚函数。需要注意的是:只有在继承当中才存在虚函数的概念。
(2)虚函数的重写
表明虚函数之后就要对虚函数进行重写操作。首先虚函数的重写需要满足三同。即函数名相同,返回值相同,参数类型相同。也就是说设置一个和原函数的列表一模一样的函数进行重新的编写,只有函数体不相同的操作就叫做函数的重写。
回到我们刚才编写的代码进行观察:
我们会发现,我们编写好的派生函数完全满足我们函数重写的概念。
但是难道只有三同的时候才可以构成虚函数的重写吗?其实对于多态来说有一个例外,那就是协变,在协变当中可以允许我们重写的函数的返回值不同,但是必须要求我们的返回值是基类和派生类的指针,基类对应基类,派生类对应派生类。例如我们可以将上述代码修改成为以下形式:
我们会发现,修改完成之后我们的代码依旧可以正常的运行,在协变当中可以作为返回值的基类和父类指针并不一定要求是我们本身的类的指针也可以是其他的基类和派生类的关系。例如:
我们会发现我们也可以返回其他的基类跟派生类的关系,但是需要注意的是:基类必须对应基类,派生类必须对应派生类。这种用法在实际的使用当中并不多见,所以我们了解即可。
1.特殊存在:析构函数的多态
大家觉得我们的析构函数可以进行重写吗?我们来思考一下:我们的析构函数格式为:
~类名()由于我们基类跟派生类的名字不同,所以我们的析构函数的命名也不相同,那么岂不是构不成函数重写的形式了?那么我们再来分析一下:我们的析构函数需要进行重写吗?有一个最简单的例子进行证明:
我们会发现通过基类的指针调用的时候可以正常的调用我们已经完成重写的虚函数,但是我们的析构函数没有经过重写,所以只会调用我们基类当中的析构函数。但是我们又会发现,就像是我们已经修改过的学生的类当中展示的那样:我们的派生类当中也可能会有新的从堆上开辟的空间。那么我们就需要调用我们student类当中的析构函数,进行该部分空间的释放,仅仅调用基类当中的析构函数显然是无法满足我们的要求的。所以我们得到一个结果:析构函数也需要进行重写。但是对于析构函数怎么进行重写呢?
不要担心,在编译器当中我们的析构函数已经被封装成为一个函数名均为destroy的函数。这样我们就可以进行析构函数的重写了。所以我们可以将代码修改成为如下的形式:
我们会发现程序运行正常了,当我们删除student的时候会自动调用student当中的析构函数。但是我们又会发现一个很奇怪的现象:那么就是为什么最后的析构函数调用的很奇怪呢?
2.析构函数调用的顺序
这个就涉及到了我们析构函数调用的顺序,重新梳理一下我们继承的构造流程。在使用派生类对基类进行继承之后。创建一个派生类的对象会优先调用我们基类当中的构造函数进行初始化,之后再调用派生类当中的构造函数进行初始化。这样可以避免在派生类的构造函数当中可能会有需要使用基类成员的情况,没有进行初始化我们就无法正常的使用基类成员。
那么反观我们的析构函数。首先我们是先调用基类的构造函数,再调用派生类的构造函数。由于函数调用是一个栈结构,所以我们会优先调用派生类的析构函数进行释放空间,之后再调用基类的析构函数进行释放空间,符合我们析构函数调用的顺序。其次,我们再派生类的析构函数当中依旧可能使用到基类当中的成员,所以我们先进行析构派生类也符合我们正常的使用需求。
那么肯定会有人有疑问了:派生类释放一次空间,基类再释放一次空间难道就不怕空间的重复释放吗?其实不需要担心这个问题,因为我们在派生类当中进行析构的时候进行的操作仅仅释放了我们在派生类当中开辟的空间,而我们调用基类的析构函数释放的就是我们在基类当中开辟的空间,两个空间完全不相同,所以不需要担心空间的重复释放的问题出现。
(3)为什么要使用基类指针或引用进行调用重写的虚函数
我们可以将问题拆分成为两个部分:为什么要使用指针或引用进行调用重写的虚函数,以及为什么一定要用基类的指针或引用才可以调用我们重写的虚函数呢?这样又有什么好处呢?我们来一一进行分析。
1.为什么要使用基类进行调用虚函数
记得我们之前学继承的时候说到的派生类跟基类之间的相互转换吗?在两个数据类型进行转换的时候我们可以将派生类当中的数据进行切割得到一个基类的对象,完成赋值操作,但是我们却无法使用基类对派生类进行赋值,因为对于我们派生类多出来的部分我们不能够得到明确的数据。想到这点其实就很容易理解了。
使用基类进行虚函数的调用无非就是为了让我们各个类型的对象都可以成功的调用我们的虚函数,无论是派生类还是基类都可以以直接或间接的方式进行访问最终达到我们想要的效果。
2.为什么要使用指针或引用进行调用虚函数
那么为什么要使用指针或引用进行调用虚函数呢?这就涉及到我们比较底层的一些只是了。实际上在将我们基类当中的函数设置成为虚函数的时候,会自动生成一张虚函数表。在虚函数表当中保存了我们所有设置成为虚函数的地址。经过继承之后我们的虚函数表也会被派生类所继承。如果派生类对我们的虚函数进行了重写的操作,那么就会有一个新的函数地址。我们使用这个新的函数地址将原有函数表当中的地址进行覆盖就成了一张新的虚函数表。
如果使用对象作为参数的话,在切割的时候并不会将虚函数表进行复制给我们的基类,因为基类当中的虚函数并不改变。但是如果我们使用引用或指针作为参数传参的话,那么我们实际上使用的还是我们派生类当中的虚函数表。(就好比我们使用int类型数据的指针或引用进行传参的时候,所使用的数据完全是我们本身一样)那么我们使用的就是经过修改之后的虚函数表了。自然查找的也就是我们经过重写之后的虚函数。这也就是我们使用引用或指针作为调用虚函数的参数的原因。