1.多态
多态就是多种形态的意思。
分为编译时多态,也叫静态多态,通过传递不同参数的方式使同一个函数好像实现了不同的状态。eg:函数模板,函数重载
还有运行时多态,也叫动态多态。通过使用不同的对象调用“同一个函数”实现不同的功能来达到运行时多态。eg:虚函数重写
2.定义与实现
2.1实现前提
(1)必须由基类的指针或引用使用实现了多态的函数
因为存在切片现象,传递基类或派生类都可以兼容,若是派生类使用则无法兼容基类
(2)实现了多态的函数必须是虚函数且完成虚函数的重写
2.2虚函数的重写
虚函数就是使用了virtual修饰的类中成员函数(非类内函数无法用virtual修饰)。
而虚函数的重写还需要满足“三同”的条件:返回值,函数名,函数参数相同。
ps:派生类的virtual其实是可以省略不写的,虽然这样子不规范,但是面试题会利用这个点考察
接下来我们实现一下动态多态
这里我们依次判断是否满足多态
首先都是虚函数,其次满足返回值,函数名,函数参数相同,且完成了重写。
调用的时候使用了基类的指针。
综上两个动态多态的条件都满足了,我们看看运行结果
我们发现确实实现了使用不同的对象调用同一个函数的不同的实现的功能,多态完成
2.3析构函数重写
为什么建议将基类析构函数写为虚函数?
当出现使用基类的指针或引用指向派生类对象的时候
若不实现析构函数的虚函数重写,会导致没有多态
对于被基类指针指向的派生类对象调用析构函数只能调用基类析构,而无法调用派生类析构,从而导致内存泄漏。
若我们调用了对应类的析构会在控制面板输出对应的析构函数
理想结果:第一个函数调用基类析构,第二个函数先调用派生类析构然后调用基类析构
实际结果,第二个函数只调用了基类的析构函数
原因:析构函数不满足多态,根据指针类型查找析构函数,由于是基类指针,所以调用的是基类析构函数
改进:使用虚函数重写
我们在基类析构中用virtual修饰,使他变为虚函数,又根据派生类的virtual可以省略不写,所以满足多态,按照指向的对象的类型调用析构函数。从而调用了派生类析构,再根据基类析构会自动调用,所以结果中先出现了派生类析构,然后是基类析构
疑问:明明析构函数的名字不一样,为什么还是满足多态?
因为编译器会对析构函数的名字进行特殊处理,将他们的名字统一处理成destructor,所以实际上名字是一样的
2.4override与final
由于重写可能会出现错误,如果不检查后面报错的时候错误就不会报的很精准,所以我们使用一个关键字override来检查派生类重写是否完成。
final用于修饰虚函数,使他不能被重写
2.5重载,重写,隐藏的比对
重载:
(1)作用域:同一个作用域
(2)条件:函数名相同,参数不同
重写:
(1)作用域:一个基类,一个派生类
(2)条件:虚函数,函数名,函数返回值,函数参数均相同
隐藏:
(1)作用域:基类与派生类
(2)条件:函数名相同/变量名相同
(3)若函数名相同,但是不构成重写则为隐藏关系
3.抽象类,纯虚函数
纯虚函数就是虚函数后面给个0,纯虚函数不需要定义,且包含纯虚函数的类叫抽象类,抽象类是无法被实例化的。某种程度说,派生类需要重写纯虚函数,否则无法实例化对象
4.多态原理
4.1虚函数表与其指针
虚函数表:是一个函数指针数组,每个包含虚函数的类都有一个唯一的虚函数表,该表中存储所有该类的虚函数地址
ps:如果类中继承了父类的虚函数,子类的虚函数表会覆盖父类虚函数表对应的函数地址
我们看到即使我们如果没有对基类的虚函数进行重写,派生类的虚函数表指针指向的位置是改变的,但是存的虚函数地址是不变的。
虚函数表指针指向的位置改变:因为每个类都有一个独立的虚函数表,派生类创建了一个和积累地址不同的虚函数表
存的虚函数地址不变:因为没有对虚函数完成重写
接下来我们看看完成了虚函数重写的情况
这里我们就发现虚函数表中存储的第一个函数地址不一样了,因为我们完成了对基类虚函数的重写
注意:当对象销毁后,虚函数表不会销毁,因为虚函数表是类级别的,而不是对象级别的
虚函数表指针:通常存储在对象的内存布局的第一个位置,本质是一个函数指针数组指针。
也就是说无论有多少个虚函数,虚函数表指针都只有一个
在32位环境下指针大小为4字节,64位环境下位8字节
那么我们接下来讲讲为什么多态需要用基类的指针或者引用去调用虚函数
因为在编译时,调用函数时用call指令跳转到对应函数地址,而对于满足多态的虚函数,我们需要在虚函数表指针中寻找虚函数的地址
而使用基类指针或者引用去使用虚函数,根据切片效应,可以兼容基类和派生类的对象调用虚函数,并且谁调用就会用谁的虚函数表查找虚函数,从而达到谁调用就跳到谁的虚函数的目标