1 多态的概念
当不同的对象去完成某个行为,会产生不同的状态!
举一个生活中的例子,我们买火车票,儿童是免票的,学生是半价票,成人是全票的!
2 多态的实现
2.1 虚函数
在继承中,我们知道了被virtual修饰的类,就叫做虚继承!那么同样的,一个类中,被virtual修饰的成员函数就叫做虚函数!
2.2 多态实现的条件
1️⃣子类中的虚函数要求重写父类中的虚函数!父子类之间虚函数必须参数相同,返回值相同,函数名也要相同
2️⃣必须通过父类的指针或者引用去调用虚函数
如下所示,就简单的实现了一个多态:
class animal { public: virtual void func() { cout << "动物会叫" << endl; } }; class cat :public animal{ public: virtual void func() { cout << "喵喵喵" << endl; } }; void test(animal& a) { a.func(); } int main() { animal a1; cat c1; test(a1); test(c1); return 0; }
在这里需要注意一个问题,在多态中,子类是继承了父类虚函数的声明,子类只是重新实现了这个虚函数!
2.3 重写虚函数中的两个例外
1 协变(父类与子类的返回值不同)
父类虚函数返回值是父类对象的指针或者引用,子类虚函数的返回值是子类对象的指针或者引用!如下图所示:
2 析构函数的重写
我们先来看这样一段代码:
对于这样的析构是否是正确的呢?很明显,这肯定是不对的,因为子类的析构函数没有调用,那就说明子类中的资源并没有清理掉,这样就会出现内存泄漏的问题了!那么为什么会出现这种问题呢?那么我们就要明白以下两种调用方式:
1️⃣普通调用:根据对象,指针,引用的类型进行调用对应的函数
2️⃣多态调用:根据指针或者引用指向的对象的类型进行调用
而我们这里的delete是会转化成调用destruct()函数,根据指针的类型,完成的是普通调用!所以就会出现内存泄漏,那么如何解决这种问题呢?因为子类和父类析构函数都会被处理成同样的函数名字!所以我们把析构函数变成虚函数,利用多态调用,就可以解决这样的问题了!
有些人就会疑问,为什么子类析构没有加上virtual,事实上,由于是继承关系,子类会继承父类的声明的!所以不加还是构成多态的!为了语法的规范性,建议还是都加上virtual,但是对于一个函数,如果你父类没有加上virtual,而子类的函数加上了virtual,那么是不构成多态的,只能算是重定义关系!
3 多态实现的原理
由于上面实现了多态,那么我们可以来看一下,父类与子类对象中存了哪些东西:
我们可以发现,加了virtual关键字后,子类对象和父类对象就多了一个_vfptr的指针,这个指针叫啥呢?这个指针就是虚函数表指针,指向了虚表,虚表中存储的就是每个虚函数的函数指针!我们可以发现,子类只要重写了父类,不是继承的,那么子类的虚函数表中存储的就是子类对应虚函数的函数指针,就是我们常说的覆盖,对于语法层面来讲就叫做虚函数的重写,原理层上来看就是覆盖!如果是继承的,那么就会把父类对应的虚函数地址拷贝一份到子类的虚表中!也就是说对象中会多拿出四个字节来存储虚表指针!但要注意,虚表不是存在对象中的,是在常量区的!而虚函数是存储在代码段的,和我们之前学过的类和对象是一样的!所以多态的原理就是根据指针或者是引用所指向的类型的调用,去虚表中找到对应虚函数的地址,从而调用对应的虚函数!从而实现多态!
调用的流程如下所示:
1 先传参数
2 根据所传参数指向的对象的类型来确定虚函数表指针
3 根据虚函数表指针在去寻找对应的虚函数指针,从而调用
3.1 普通调用与多态调用的区别
普通调用我们都知道,编译的时候就会确定所要调用函数的地址,而多态调用是在运行时才会确定所要调用的函数地址!这就是两者之间最大的区别!这也间接说明了多态是在运行时,通过虚函数表去寻找对应的虚函数指针,从而进行函数调用,而普通调用,在编译时就会根据类型就会确定调用函数的地址。
3.2 动态绑定与静态绑定
静态绑定(也称为早期绑定)指的就是在编译期间就确定函数的行为,也被叫做静态多态!最典型的就是函数重载
动态绑定(也称为晚期绑定)指的是在程序运行期间,才会确定函数的行为,被称为动态多态,例如就是重写(覆盖)
4 抽象类
在虚函数后面写上=0,那么这个虚函数就被称为纯虚函数,含有纯虚函数的类也被称为抽象类(接口类),抽象类是不可以实例化对象的
在C++中,普通继承是实现继承,子类可以调用父类中的成员函数,而接口继承就是为了让子类重写父类中的函数!从而可以实现多态!
5 单继承和多继承关系中的虚函数表
5.1 单继承情况
单继承中虚函数表的情况,和我上面提到的多态原理中的分布情况是一样的!就是父类与子类都会有一个虚表!如果没有实现重写,那么子类中虚表的内容和父类中虚表的内容是一样的,如果实现了重写,那么子类会在其虚表中覆盖掉原来父类中的函数地址!
5.2 多继承情况
class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; }; int main() { Derive d; Base1 b1; Base2 b2; return 0; }
下面让我来图解一下对象d的内存是怎么样的!说明多继承子类的未重写的虚函数放在第一个继承父类部分的虚函数表中
5.3 菱形继承中的虚表(了解)
class A { }; class B :public A{ }; class C : public A{ }; class D :public B,public C{ };
如果在B和C类中都重写了A中的虚函数,那么在D中就必须也重写该函数,不然在A的虚表中不知道用B重写过的地址还是C重写过的地址!也会导致二义性!
虚表与虚基表
1️⃣虚表是实现多态的原理,里面存储的是虚函数的函数指针
2️⃣虚基表用于虚拟继承,解决菱形继承中的二义性与数据冗余的问题!里面保存的是地址的偏移量!