一、什么是多态
多态指的是同一接口,不同形态。多态的本质是接口复用。区别与继承,继承的本质是代码复用。
二、多态的分类
一般多态分为:静多态、宏多态以及动多态。
静多态:编译阶段确定函数调用,汇编下直接call 的是函数的入口地址;并且我们可以通过函数重载或者模板来实现静多态;像这种在编译阶段就能确定函数调用也称为静态/早绑定;
动多态:在运行阶段确定函数调用,汇编下call的是eax寄存器;并且我们可以通过虚函数实现动多态,virtaul关键字实现虚函数。像这种在运行时才能确定函数调用的称为动态/晚绑定。
宏多态:在与处理阶段确定函数调用。
接下来重点介绍我们的动多态。
三、虚函数
基类中是虚函数,派生类中同名且形参类型相同的函数也是虚函数。
例如:
Show是普通的成员方法时,用A的指针指向B的对象,输出的是A::show();
此时,B的内存布局是这样的:
当将A中的show写成虚函数的时候,此时打印的结果为B::show();
此时整个的内存布局变化为:
Show在A中是虚函数,那么B中的show也是虚函数,它满足同名同参。虚函数会在生成对象的时候生成一个vfptr虚函数指针指向虚函数表,那么在B中一是继承了A的虚函数指针,二是自身也有一个虚函数指针,因此这里就存在一个虚表合并的过程。
虚表的合并:沿着继承链的反方向合并,虚表的确定是在编译时期间存放在.rodata段中。一个类中对应只有一个虚表。注意vfptr的优先级高于任何成员;
派生类中的同名同参的虚函数覆盖了基类同名同参的虚函数,两者的关系式覆盖也就是重写的关系。
四、动多态的发生条件
指针或者引用调用虚函数并且对象完整;
理解对象完整:对象的构成需要两步,一是开辟内存,二是赋资源;两者必须都完成之后才是一个完整的对象;
五、哪些函数可以写成虚函数
有两个条件:一能取地址;二依赖对象调用;
例如以下的函数就不能写成虚函数:普通函数、构造函数、static成员函数和内联函数;
可以写成虚函数的函数有:普通成员函数和析构函数;
六、虚表的写入时机
在构造函数的第一行代码执行之前
派生类对象生成时,可能有多次的虚表写入。
七、关于虚析构
满足同名覆盖
当A、B各自的析构函数没有关系时,A的指针指向B对象,析构的时候只调用A的析构,
当A的析构为虚析构时,再进行同样的动作时,会先调用A的析构然后再调用B的析构。
八、纯虚函数
抽象类:不能实例化对象的类称为抽象类;
在该例中Print就是我们的纯虚函数。
下面是D通过继承C,在D中子类对父类的纯虚函数进行重写,通过D进行对象的生成,C只提供对象行为的接口。同时,这里用到dynamic_cast,用来进行RTTI信息的转换。
九、四种类型转换
Const_cast:去除常性
例子:
const int a = 10;
const int *p = &a;
int *q = const_cast<int *>(p);
*q = 20;
Dynamic_cast:RTTI信息的转换
例子:参照上面纯虚函数
C* pc = new D("ddd");
pc->Print();
D* pd = dynamic_cast<D*>(pc);
运行时提取类型与<类型>比较,进行转换,相等时:转换成功;不相等时:转换失败,指针为空;
Static_cast:安全性高
Reinterpret_cast:类似于C的转换,一般用于指针的转换
小结:c++三大特性分别为封装继承和多态,多态的本质是我们的接口复用。在多态的研究中,我一般是以动多态为主.。也就是在运行阶段才能确定函数调用。实现动多态一般采用虚函数,那实现虚函数 一般用的是virtaul关键字。同时,也包括我们的纯虚函数,虚析构等等。