多态的类型与两个条件
一.多态的类型
多态分为两种类型分别是静态多态和动态多态
-
静态多态,又称前期绑定,其实就是我们认识的函数重载,例如我们最熟悉的cin>>和cout<<本质上就是函数重载如下图:
当我们输入和输不同类型的变量时其实就是一种函数重载。静态的重载是指在编译的时候完成的。因为C++中存在函数名命名规则,通过它就可以实现重载。 -
动态多态,又称后期绑定,是在运行期间调用函数,动态多态和静态多态看似只差一个字,但是本质却千差万别。动态多态是基于继承实现的,而且动态多态有两个实现条件分别为:
必须通过基类的指针或者引用去调用虚函数
被调用的函数必须是虚函数并且派生类中必须对基类的虚函数完成重写
具体如下:
这里我们定义了一个Person基类和它的两个派生类,之后我们可以看到在Person类中,我们定义了一个虚函数,这里就用到了virtual,一定要注意,这里的virtual关键字和之前的虚继承中的virtual关键字虽然是一样的,但是他们两个之间是一点关系都没有的,这里一定要分辨清楚。之后看Student类和Soldier类都对其父类中的虚函数进行了重写,这里的重写所需的条件是子类和父类中的虚函数包括函数名,参数,返回值都要相同,这样才能完成重写,但是这只是大部分,之后会提到一些例外,不需要满足这三个条件也可以实现重写。那么我们如何使用这个多态呢?上面我们提到过,必须通过基类的引用或指针来调用虚函数,如下图:
我们在这里先定义了三个不同的类的对象,之后同时调用f函数,这里要注意这个f函数中的参数类型,是父类的引用类型,所以当调用函数进行参数传参的时候就相当于把你的对象赋值给父类的引用,这样也就完成了第一个条件,当然这里用指针也是可以的如下:
这样我们就可以通过父类的指针或者引用来调用虚函数,这里父类的引用或指针指向哪个对象,调用的就是哪个对象的虚函数,比如上面我们依次调用了Person,Student,Soldier对象,那么就会依次得到结果:
多态重写几个例外,纯虚函数以及抽象类
一. 多态重写中的几个例外
上面我们提到过要实现多态就要满足多态的条件,其中一个多态的条件就是需要对虚函数进行重写,而这子类的虚函数和父类中的虚函数需要函数名,参数,返回值都相同,但是有这么几个例外不需要满足这个条件也可以实现多态,首先介绍第一个,就是协变。
-
协变: 正常虚函数重写要求函数名,参数,返回值都相同,但是协变可以返回值不相同,也就是可以让基类虚函数返回基类指针或引用,派生类虚函数返回派生类指针或者引用,这就成协变,具体实现我们举个例子:
这里我们可以看到在A类中的虚函数buy中,返回的就是A类指针,同理在B类中的虚函数buy中返回的就是B类指针。而当我们创建两个对象并分别调用f()函数的时候就可以得到结果:
父类引用指向不同的对象也就调用该对象中的虚函数,也就是实现了多态。 -
析构函数
之前我们在继承哪里就提到过,当父类和子类中都定义了析构函数,那么子类中的析构函数会隐藏父类中的析构函数,但是他们两个函数的名称却不一样啊,这是为什么呢??
因为在编译器中,会自动将所有对象的析构函数都转换成destructor()函数,所以在编译器看这两个析构函数就是同名函数,也就构成了隐藏,所以这里其实也是一样的,虽然我们看着析构函数的名字是不同的,但是在编译器看来他们就是相同名字的,所以可以构成重写如:
这里我们完成对析构函数的重写,之后我们调用delete函数,注意delete函数调用的是该对象的析构函数和operator delete函数
当我们通过父类指针指向子类对象,这样当我们调用delete的时候就会先调用B类对象的析构函数,而B类对象是子类对象,所以当它的析构函数结束时,编译器会自动调用父类函数的析构函数完成释放空间,具体如下图:
另外还有一个特殊的例子要注意一下,就是当父类中使用了virtual关键字了如果子类对父类中的虚函数进行了重写,但是子类的虚函数前面没有写virtual,那么编译器会自动匹配一个virtual也就是子类对象不写virtual也会被当作虚函数,但是最好还是写上virtual,增加代码可读性。
这里再给大家介绍两个虚函数中的关键字分别是:final和override
final是用在父类的,如果你不想子类重写当前父类的虚函数,那么你就再函数名的后面加上一个final,这样该虚函数就不会被重写了,如:
这时子类重写就会报错。
第二个关键字就是override关键字,该关键字是用在子类中的,是用来检查该子类是否对父类完成了重写,如果没有完成重写就会报错,如下:
二. 纯虚函数
上面我们讲完了虚函数,这里我们说一下纯虚函数,那么什么是纯虚函数呢,纯虚函数其实很简单,就是在声明虚函数的时候在其后面加上=0就可以了如:
这里我们将虚函数后面加上=0就可以得到一个纯虚函数,那么一个类中如果存在纯虚函数,那么这样的类就叫做抽象类,抽象类的特点就是它不能通过自己完成实例化,也就是不能构造对象。那么一些读者就会想,一个类都不能构造对象了,那这个类有什么存在的意义吗。存在即合理,抽象类当然有有它自己存在的意义。虽然它无法实例化对象来调用自己的成员函数,但是它可以让一个类继承,通过另一个类来完成对该成员函数的重写并调用。如下:
这里就可以看到,我们定义了一个Benz类来继承这个Car类,并对Car中的纯虚函数进行重写,从而实现调用。要注意的一点,当一个类只继承了抽象类,但是并没有对抽象类中的纯虚函数进行重写的话,那么这个类也会变成一个抽象类,从而失去作用,所以我们要知道,只要你继承了一个抽象类就一定要完成抽象类中的纯虚函数的重写,否则该类将一无用处。
那么抽象类的作用是什么呢??
抽象类的作用就是可以更好的表示出现实世界中没有实例对象的抽象类型,比如: 植物,动物,人等等泛类。
多态的原理,虚表地址的打印
一. 多态的原理
这里我们先看一道面试题
这道题首先定义了一个父类和一个子类,子类对父类中的虚函数完成了重写,让我们计算Base对象的字节数。
这里我们乍一看像是考内存补全的问题,那么我们就先按照之前学的只是算一下,这里int占4个字节,char占一个字节,加一起五个字节,之后补全为4的倍数,那么就是八个字节,所以我们认为这里应该是八个字节,让我们看一下运行结果:
显然我们算错了,答案比我们多出了四个字节,那么这四个字节是从那么来的呢?别忘了我们在父类函数中还定义了一个虚函数,,所以我们就要了解一个新的名词叫虚函数表,也就是用来存储虚函数地址的表,也叫虚表指针。我们先从监控窗口来看一下对象a的结构:
这里我们可以看到对象a中不仅存储着成员变量_b和_ch,还存储着一个叫_vfptr的指针,这个指针就是我们所说的虚表指针,它是一个函数指针数组,它中存储的是虚函数的地址。那么这里我们就可以了解多态的原理了,多态的原理其实就是当一个父类指针或引用指向一个父类对象的时候,就直接去父类对象中的虚函数表中找要调用的虚函数,当一个子类继承了父类的时候,会将父类的虚函数拷贝到子类中,当完成重写后,父类指针或引用指向子类对象是,子类对象会将重写完后的父类的那部分给到父类指针,之后父类指针会在子类修改后的虚函数表中查找到调用的虚函数,从而实现了多态。
那么为什么必须用父类指针或引用而不能使用父类对象呢??
因为父类对象内部的虚函数表就是父类原来的虚函数表,当用子类对其进行拷贝的时候,他只会拷贝子类继承父类的那些成员变量,而不会拷贝虚函数表,所以不能实现多态。
二. 虚函数表的打印
上面我们通过在监视器窗口看到一个对象会将其虚函数的地址存储到虚函数表中,那么我们如何将虚函数表中存储的函数地址都打印出来呢,这个也是不难的,首先我们看单继承这种情况:
这里我们可以看到,定义了一个父类,父类中定义了两个虚函数,再定义一个子类来继承父类之后对父类中的虚函数进行了重写,并且自己也定义了两个属于自己的虚函数,我们先从监视窗口来看一下这两个类中的结构:
通过监视窗口我们可以看到,在父类中虚函数表中存储了两个定义好的虚函数,但是在子类对象中我们只能看到虚函数表中存储的是从父类继承过来的两个虚函数,其中一个子类对象对齐完成了重写,那么下面我们就要打印一下这两个对象中的虚函数表,看看子类中的另外两个虚函数的地址,具体代码如下:
这里我们打印一下就可以发现b,a对象的虚表地址和虚函数地址就可以打印出来了。
当继承方式为多继承的时候,如下:
子类Ase继承两个父类分别为:Base1和Base2,那么Ase类中应该存在继承过来的两个类的虚函数还有自己的虚函数
这时我们就可以发现Ase的对象a中存储着两张虚函数表,但是还是看不到自己创建的虚函数的地址,那么我们依旧按照上面那种方法来打印一下这三个对象中的虚函数表地址和虚函数地址:
这里注意我们是多继承,所以我们要通过Base1的初始位置来找,Base1的初始位置就是a的地址,那么我们就可以将a的地址先转换成cha*类型之后加上Base1的字节数,就可以找到Base2的首地址了,之后再进行下面的操作就可以得到:
通过这张图我们可以知道,Ase对象将自己的那个虚函数地址存储在了第一张发虚函数表中。这也就是虚函数表的打印的全部内容。