目录
[四.final 和 override](#四.final 和 override)
一.多态的基本概念
1.多态的定义
多态允许一个接口(通常是基类)被不同的类(通常是派生类)以不同的方式实现,多态使得程序可以在运行时 根据对象的实际类型来决定调用哪个方法,而不仅仅是根据编译时的类型。
2.多态的实现方式
C++中的多态主要通过以下两种方式实现:
函数重载 :它允许在同一个作用域内存在多个同名函数,这些函数的参数列表不同。根据所传参数类型不同调用不同的函数,它是静态多态(编译时多态)。
继承和虚函数 :通过继承,一个类(派生类)可以继承另一个类(基类)的属性和方法。通过将基类的成员函数声明为虚函数 ,可以使得派生类可以重写 这些函数,从而在运行时根据对象的实际类型调用相应的函数,它属于动态多态(运行时多态)。
本文主要讲述的是运行时多态的实现机制~~
3.动态多态前置知识
a.虚函数和虚函数表
虚函数 :在基类中,使用 virtual关键字声明的函数称为虚函数,虚函数允许在派生类中被重写,从而实现多态性。
注意:只有成员函数能加virtual,全局函数或其它局部函数不能加virtual修饰!!
虚函数表 :编译器为每个包含虚函数的类 生成一个虚表,该表存储了指向类中所有虚函数的指针。当通过基类指针或引用调用虚函数时,编译器会查找对象的虚表来确定应该调用哪个函数。
b.抽象类和纯虚函数
抽象类 :一个包含至少一个纯虚函数的类 被称为抽象类。抽象类不能被实例化,它们通常作为基类,为派生类提供接口。
纯虚函数 :在基类中,使用 virtual 关键字和**=** 0 声明的函数称为纯虚函数。纯虚函数没有实现,派生类必须重写这些函数才能被实例化。
二.构成多态的两个条件
1.虚函数重写
被调用的函数必须是虚函数, 即在基类的函数前加 virtual 修饰 ,且派生类 必须对基类的虚函数进行重写。
a.虚函数重写的条件
①函数类型:重写的函数在基类中必须是虚函数,而派生类可以不用加virtual。
②函数签名相同 :重写的函数必须与基类中的虚函数具有完全相同的函数签名,包括函数名、返回类型、参数列表以及参数的类型、顺序和数量。
③**构造函数或析构函数不能被重写,**虽然析构函数可以是虚函数,但这种情况是特殊的,用于多态删除。
注意:如果基类的虚函数是常量成员函数(const),则派生类中的重写函数也必须是常量成员函数;如果基类的虚函数是引用传递参数,则派生类中的重写函数也必须使用引用传递参数。
注意:重写的函数不能拥有比基类虚函数更严格的访问权限。例如,如果基类的虚函数是protected 或 public,派生类中的重写函数也必须是 protected 或 public。不能是 private。
b.协变
协变允许派生类中的重写虚函数具有与基类虚函数不同的返回类型,但这个不同的返回类型必须是基类返回类型的派生类。
即,如果基类中的虚函数返回一个指向基类对象的指针或引用,派生类中的重写函数可以返回一个指向派生类对象的指针或引用。
注:虚函数重写的是"函数实现",虚函数的声明仍以基类为准!!
2.基类的指针或引用
我们必须通过基类的指针或引用去调用重写的虚函数,才能实现多态。
如:
三.析构函数的特殊处理
1.虚析构函数
基类的析构函数必须是虚函数,且派生类默认会对基类析构函数重写,即若A类被B类所继承,那么A类的析构函数就必须是虚函数,B类会对A类的析构完成重写操作。
这样做的原因是,当通过基类指针删除派生类对象 时,如果析构函数不是虚函数,那么只会调用基类的析构函数 ,而不会调用派生类的析构函数。这会导致派生类特有的资源没有被正确释放,从而造成内存泄漏,如:
通过将析构函数声明为虚函数,可以确保在删除对象时,无论对象的实际类型是什么,都会调用正确的析构函数。
那么,问题来了,咱们上面也讲了,要对虚函数重写的条件其一便要求函数名相同,但派生类的析构名怎么可能与基类的析构名相同呢??
表面上确实不同,因为析构名=~类名,由于基类与派生类的类名肯定不可能相同,所以在编译器底层,析构函数会被统一处理成 destructor,让它们的函数名保持相同!!
2.析构函数的调用顺序
在多态情况下,析构函数的调用顺序是先调用派生类的析构函数,然后调用基类的析构函数。这与构造函数的调用顺序相反(先基类后派生类)。
这种顺序确保了对象在销毁时,先释放派生类特有的资源,然后再释放基类共有的资源。
四.final 和 override
1.final关键字
功能一:阻止类被继承
当 final 关键字用于修饰一个类时,它表示该类不能被其他类继承。这有助于创建不可扩展的类,从而确保类的行为不会被派生类修改。
功能二 :阻止虚函数被重写
当 final 关键字用于修饰一个虚函数时,它表示该函数在派生类中不能被重写。
2.override关键字
功能:帮助派生类检查被修饰的函数是否正确完成虚函数的重写。
五.深入理解虚函数表(重点)
当一个类包含虚函数时,编译器会为该类生成一个虚函数表,该表包含了类中所有虚函数的地址。每个包含虚函数的类的对象都会包含一个指向其所属类的虚函数表的指针(通常称为vptr,即虚函数表指针)
虚函数表本质是一个数组,其中每个元素都指向一个虚函数的地址。
1.单继承下的虚函数表
a.虚函数表的初始化
虚函数表的初始化是在初始化列表中进行的,具体过程:类实例化对象 ------> 进入构造函数初始化列表 ------> 初始化虚表函数指针 ------> 虚表内记录类的虚函数地址,完成虚函数表的初始化;
对派生类 而言,初始化虚表函数指针时,先将基类的虚表内容拷贝到自己的虚表中 ,再判断自己是否对基类虚函数进行重写,如果有,则用自己重写后的虚函数地址覆盖基类对应虚函数地址 ,最后将自己的虚函数写入虚表。
同类型的对象共用一个虚表,当派生类对象"赋值切片"给基类对象时,并不会将派生类的虚表拷贝给基类,否则会导致多态乱套!
b.多态的底层实现原理
当基类的指针或引用指向基类对象时,通过基类的虚表函数指针 ,找到基类的虚函数表 ,遍历基类虚表找到调用的虚函数 ;当基类的指针或引用指向派生类对象时,通过派生类的虚表函数指针 ,找到派生类的虚函数表 ,遍历派生类虚函数表找到调用的虚函数,实现多态!
c.虚函数表的存储位置
博主看过许多网上的资料(包括AI),他们大多都说虚函数表实际存储在静态区中,但事实真的如此吗?咱们可以通过实验来进行验证~~
咱们可以用数据验证,通过各区段的数据地址打印,我们可以比较虚表地址距离哪个区段更近,从而判断虚表真实存在哪里。
上图不难发现,虚表是存储在常量区,而不是静态区的!!
d.通过虚表打印虚函数地址
那么,如何确定打印出来的地址,就是真实的Base类中的虚函数地址呢??--- 很简单,直接调用虚函数做验证即可,操作结果如下: