多态是C++中的三大特性之一,有着很重要的意义,那什么是多态呢?
官方定义是指为不同数据类型的实体提供统一的接口叫做多态,其实就是一个函数接口,当函数中传入不同类型的对象时能够实现对象中相应的功能,这就是多态。
举个例子:动物都可以进行交流,但是具体的某一种动物交流的方式又不同,比如小狗是” 汪汪汪 “,而猫是” 喵喵喵 “等等,假如我们现在有一个需求:实现一个能够学龄前玩具的功能接口,如下
具体功能,小孩子点哪一种动物,就会发出哪种动物的叫声,但是要求只是用一个公共的接口函数。我们挑选其中的鱼的鸭子作为示例:
思路:以上都属于动物,他们都有一个共同的特征 ” speak “,但是每一种动物的 " speak " 不相同,所以将speak 作为基类的公共方法,各动物继承于该基类,分别都有不同的speak 方法。
#include <iostream>
using namespace std;
class Animal //定义一个动物类为基类
{
public:
virtual void speak() //公共方法speak
{
cout<<"animal can speak"<<endl;
}
};
class duck:public Animal //定义一个鸭子类
{
public:
void speak() //重写公共方法
{
cout<<"ga ga ga"<<endl;
}
};
class fish:public Animal //定义一个鱼类
{
public:
void speak() //重写公共方法
{
cout<<"gulu gulu gulu"<<endl;
}
};
void Animal_speak(Animal* animal) //公共接口
{
animal->speak();
}
int main()
{
duck D; //实例化一个鸭子
fish F; //实例化一个鱼
Animal_speak(&D); //分别为公共接口传入不同的对象
Animal_speak(&F);
return 0;
}
通过这一个接口就实现了当传入的是不同的对象时该接口实现了不同的功能,这就是多态。
那么实现多态的原理是什么呢?我们在刚才的实例中对基类的公共函数" speak " 的定义之前加上了关键字 virtual ,代表将该函数申明为虚函数, 这时我们就需要理解实现的原理了。
示例中的duck 类和 fish 类都是公共继承于 Animal 类,那么其中的公共函数 speak 也会被继承,所以我们在使用公共接口实现多态时应该考虑,既然要将不同类中的方法进行调用,我们就应该先找到能够操作这些对象的句柄,由于每个类都继承了基类,所以我们使用基类的指针或者基类的引用就可以操作其派生类的方法。我们通过一张图解释其中的关系(不加virtual 关键字时):
子类在继承父类之后包含基类部分和自己本身的部分,由于我们使用了基类的指针对子类进行访问所以只能够访问基类的部分,而子类中自己的speak方法就不会被执行,所以只能调用到基类的speak方法,如下,当基类中的 speak 方法中不加 关键字 vrtual 时会产生什么效果:
这时就会发现虽然这个接口中传入了不同的对象,但是执行的是基类中的speak 方法,这就验证了之前的图,基类的指针只访问了基类部分。那么为什么只是加上了 virtual 关键字就可以实现多态了呢,其实 virtual 关键字修饰的函数成为虚函数,这样类中的首地址会使用8字节(64位机)的空间存放虚函数表的位置,而虚函数表中存储的是每一个虚函数的 首地址,也就是只要我们能够将存放在虚函数表中的虚函数首地址改为我们想要执行的函数的首地址,那么基类的指针就可以访问到该函数,如下图,我们应该将基类中的 speak 替换为子类中的 speak ,那么在基类指针访问时,虚函数指针指向的就是子类中 speak 方法的首地址,就调用了该方法。
我们怎么对虚函数表中的函数首地址进行替换呢,很简单,我们只需要对继承的虚函数进行重写就可以完成,由于我们在每个子类 duck 和 fish 中都对基类中的speak 方法进行了重写,即覆盖了基类中的 speak 方法,所以虚函数表中存放的就是 子类中speak 方法的首地址。虚函数的条件有以下几个:
1,非类的成员函数不能设置为虚函数。
2,类的静态成员(不属于对象)不能定义为虚函数
3,构造函数不能定义为虚函数,但是析构函数却可以设置为虚函数(因为虚构函数可以被显示调用)
4,成员函数声明时需使用virtual关键字修饰,定义时不需要。
5,基类的成员函数设置为虚函数,那么派生类中同名函数(函数名,形参类型,个数,返回值类型都一样)自动成为虚函数
注 意:将基类中继承的虚函数重定义以实现其他的功能,重写后不会改变基类中的虚函数的权限。重写的只是派生类中的基类部分,依然可以访问基类原本的虚函数。
即对于上个例子中我们只是对 duck 类和 fish 类中的基类部分中的 speak 方法进行了重写,但是并不会改变 Animal 类中原本的函数,依然可以对其进行调用。