C++进阶 多态
一.多态的概念
多态是面向对象编程的一大核心特性,意思是 同一个操作作用于不同的对象,可以有不同的行为。用一句话总结:
同样的接口,不同的实现。
举个直观例子:
动物有一个 叫() 方法。
不同动物会叫不同的声音:
狗.叫() → “汪汪”
猫.叫() → “喵喵”
鸟.叫() → “啾啾”
这里,调用 叫() 是一样的,但结果因对象类型不同而不同,这就是多态。
二.多态的定义及实现
1.多态的构成条件
通过 继承 + 虚函数(virtual) 实现
基类函数声明为 virtual
派生类重写(override)该函数
通过 基类指针或引用 调用,执行的是派生类版本
注意:
指针或引用调用才能体现多态;如果是对象本身,会发生 对象切片,多态失效。
基类析构函数也要写 virtual,否则通过基类指针删除派生对象时可能不会调用派生类析构。
2.虚函数
虚函数是实现运行时多态的关键。
在基类中用 virtual 修饰的成员函数称为虚函数。
3.虚函数的重写和覆盖
虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
这里:
重写了:
4.析构函数的重写
发现:
名字根本不一样
但是一样能构成重写
这是因为编译器内部会把析构函数统一处理为
为什么析构函数要设计成虚函数
如果析构函数不是虚函数
输出: ~Person()
只有基类析构被调用
假设 Student 有动态资源
如果:
只会调用:
那么:
永远不会执行
导致内存泄露
5.override 和 final关键字
1.override
作用
告诉编译器,这个函数必须是在重写(Override)基类的虚函数,如果不是重写,编译器直接报错。
开发者本来想重写:
结果写成
实际上不是重写,而是隐藏,但是编译器没有报错
所以就需要Override来检查重写
编译器立即报错
立即发现问题
2. final
作用
final 表示:
到此为止,不允许继续继承或重写。
final 修饰虚函数
编译失败
final 修饰类
表示Base 是最终类,任何类都不能继承它。
override 和 final 一起使用
含义:
编译报错
6.重载/重写/隐藏的对⽐
三.纯虚函数和抽象类
1.纯虚函数
定义
如果一个函数在基类中没有实现,或者我们希望派生类必须重写它,就可以声明为 纯虚函数。
语法:
纯虚函数特点
必须是虚函数
不能在基类直接实例化对象
可以在派生类重写,也可以在基类提供实现
2.抽象类
定义
含有至少一个纯虚函数的类叫做 抽象类。
特点:
1.不能实例化
2. 可以作为指针或引用类型使用
3. 派生类必须实现所有纯虚函数,否则派生类仍是抽象类
四.虚函数表指针
1. 什么是虚函数表指针(vfptr)
当一个类中存在虚函数时
编译器会偷偷给对象增加一个成员:
这个隐藏成员就是虚函数表指针
作用:指向当前类对应的虚函数表(vftable)。
2.什么是虚函数表
编译器会生成:
vfptr 指向它:
3. vfptr什么时候产生
只要类中有虚函数:
对象中就有vfptr
4. vfptr存在哪里
vfptr存放在对象里面。
注意:
vfptr属于对象
vftable属于类
每个对象都有自己的vfptr,但都指向同一张虚表
5.多态调用过程
五.多态原理
1.多态是如何实现的
通过虚函数表(vftable)和虚函数表指针(vfptr)实现。
2.多态的实现原理
第一步:生成虚函数表
基类
编译器生成:
第二步:对象中增加vfptr
Person对象:
vfptr:
指向虚函数表
第三步:派生类重写虚函数
编译器生成Student虚表
重写后:
对象布局
创建:
3.多态调用过程
普通函数
如果不是virtual:
编译阶段直接确定
这叫静态绑定
虚函数
有virtual:
编译器不会直接决定。
运行时执行:
① 找p指向的对象
② 找对象中的vfptr
③ vfptr找到虚函数表
④ 从虚表中取函数地址
⑤ 调用对应函数
这叫动态绑定
总结:
C++多态是通过虚函数表(vftable)和虚函数表指针(vfptr)实现的。
当类中存在虚函数时,编译器会为该类生成一张虚函数表,对象中会保存一个虚函数表指针。
当通过基类指针或引用调用虚函数时,程序在运行时根据对象中的vfptr找到对应的虚函数表,再从虚函数表中找到实际要调用的函数地址,从而实现动态绑定和运行时多态。
六.虚函数表
1.同类型对象共用同一张虚表
创建对象:
每个对象都有自己的vfptr但是都指向同一张虚表
因为虚表属于类,不属于对象。
2.基类和派生类有各自独立的虚表
编译器生成:
不是共用一张表
3.为什么派生类对象只有一个vfptr
会不会:
其实并不会
单继承情况下:只有一个vfptr
4.重写后为什么叫覆盖
Person虚表:
Student虚表:
可惜理解成:
原来:
Person::BuyTicket
被替换成:
Student::BuyTicket
这就是覆盖
5.派生类虚表到底包含什么
Person虚表:
Student虚表:
所以派生类虚表包含:
① 继承的虚函数
② 重写后的虚函数
③ 新增的虚函数
6.虚函数表本质是什么
本质上:
就是函数指针数组
数组里面存的是函数地址,不是函数本身
7.虚函数存在哪
很多人以为虚函数在虚表里面
这是错误的
实际上:
8.虚表存在哪
严格来说C++标准没有规定,不同编译器实现不同
VS下虚表存在代码段(常量区)
总结:
1. 类中有虚函数时会生成虚函数表(vftable)。
2. 对象中会保存一个虚函数表指针(vfptr)。
3. 同类型对象共享同一张虚表。
4. 派生类拥有独立的虚表。
5. 重写虚函数时,派生类虚表中的对应函数地址会覆盖基类地址。
6. 虚表本质是函数指针数组。
7. 虚函数代码存放在代码段,虚表一般存放在只读数据区,但标准未规定具体位置。
8. 运行时通过vfptr查找虚表中的函数地址,从而实现多态。
