多态(******)
概念
同一事物在不同场景下表现出的多种形态
多态 = 一个接口,多种实现
分类
静态的多态->函数重载
编译时是参数匹配和函数名修饰规则
cpp
void func(int a);
void func(double a);
void func(int a, int b);
动态的多态->运行时,跟指向对象有关
三要素(缺一不可)
- 继承关系(父子类)
- 虚函数重写(virtual + 三同)
- 父类指针 / 引用 调用虚函数
虚函数
子类中虚函数可以不加virtual
静态函数、全局函数不能是虚函数
构造函数不能是虚函数,析构函数建议写成虚函数
重写(覆盖)规则:三同->例外协变
函数名相同
参数相同
返回值相同
父类指针或者引用去调用虚函数
指向父类调用父类虚函数,指向子类调子类虚函数
条件
- 基类必须有虚函数,派生类对虚函数重写
- 使用基类的指针或引用调用虚函数
根据指针和引用调用指向不同类的对象,选择对应虚函数
cpp
class Father {
public:
virtual void show() { cout << "父类" << endl; }
};
class Son : public Father {
public:
// 子类可写 virtual,也可以不写
void show() override { cout << "子类" << endl; }
};
Father f;
Son s;
Father* p = &f;
p->show(); // 父类
p = &s;
p->show(); // 子类
重载/重写(覆盖)/隐藏(重定义)
重载
重载也叫做静态多态或者静态联编
- 同一作用域(同一个类里 / 同一个命名空间)
- 函数名相同,参数不同(类型 / 个数 / 顺序)
重写
条件
- 在继承体系中
- 基类是虚函数
- 派生类虚函数必须和基类虚函数完全一致(三同)
- 必须通过父类指针 / 引用调用才体现多态
两个例外
协变
基类虚函数返回基类的指针引用
派生类返回派生类指针引用
这种也算合法重写。
析构
底层统一解释为destructor
隐藏
父类函数不是虚函数
子类的成员和父类相同,会把父类成员隐藏
override和final
override:我要重写,编译器你帮我检查!
final:到此为止,不许继承 / 不许重写!
override
表示这个函数是重写的,编译器要检查该函数父类是否是虚函数,以及是否可以访问
final
- 修饰类
该类不能被继承 - 修饰函数
该函数不能被重写
析构函数建议是虚函数?为什么?
cpp
A* ptr = new B;
delete ptr;
结果(不加 virtual)
只调用~A (),不调用~B ()
纯虚函数
- 纯虚函数写法
cpp
virtual void func() = 0;
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象 不能 A a; 或 new A;
间接强制子类重写,不重写,子类依旧是抽象类,依旧不能实例化
虚表构建规则
基类虚表
按照声明次序 加入到虚表中
表里存的是:各个虚函数的入口地址
派生类虚表
- 先拷贝基类的整个虚表内容
- 派生类重写了哪个虚函数,就进行虚表当中地址的替换
- 如果新增了虚函数,按声明顺序追加到虚表末尾
多态调用过程
对象里有虚表指针(vfptr)
父类对象 → 指向父类虚表
子类对象 → 指向子类虚表(重写过的函数已替换)
用父类指针 / 引用调用虚函数时
不看指针类型,看指向的对象
虚函数调用步骤
- 从对象的前4/8个字节拿出虚表地址
- 从虚表中找到具体的虚函数地址
- 函数参数压栈
- 跳转到该地址执行函数
多态的原理
基类指针可以指向子类对象,通过基类指针访问前四个字节拿到子类虚表,子类虚表存放的是子类的虚函数,此时调用就会调用当前虚表下的虚函数
转型问题
- 向上转型
子类 → 父类(子类转父类 ) 的指针 / 引用转换
安全,编译器直接允许,不需要强转
cpp
B b;//子类
A* pa = &b; // 隐式转换,安全
A& ra = b;
- 向下转型
父类 → 子类(父类转子类 ) 的指针 / 引用转换
本身不安全
因为父类指针可能真指向父类对象,强行转成子类会越界 / 乱访问
cpp
A a;
B* pb = (B*)&a; // 语法过,但运行极危险
想安全向下转:用 dynamic_cast + 虚函数
考点
1. 什么是多态
多态就是同一事物在不同场景下表现出的多种形态
2. 什么是重载、重写、重定义(隐藏)
- 重载是在同一作用域下,函数名相同,函数参数的类型,数量,顺序不同,即构成函数重载
- 重写是指在继承体系中,基类的某一成员函数是虚函数,在子类中对该虚函数进行重新实现就是重写
- 重定义是指在继承体系中,不满足重写的条件,并且子类含有一个和基类相同的成员,就构成了重定义
3. 多态的实现原理
当一个类中含有虚函数时,会为该类创建一个虚函数表,保存的是虚函数的地址
当派生类继承基类时,也会有对应的虚函数表
当定义一个派生类对象时,编译器检测到有虚函数,就会给该派生类对象创建一个虚函数表指针,指向这个虚函数表,这是在构造函数完成的
后续如果有基类的指针指向派生类,那么调用函数时,虚函数表指针调用的就是该派生类的虚函数,即使是基类的指针
4. inline函数可以是虚函数吗
可以,但是会丧失inline属性
编译器会把虚函数的地址放到虚函数表中,如果inline函数被当做虚函数,会自动忽略inline属性
5. 静态成员可以是虚函数吗
不可以
静态成员函数没有this指针,访问静态成员函数的方法是采用类名::函数名
而这样的做法不可以访问虚函数表,因此不可以把静态成员函数放到虚函数表当中
6. 构造函数可以是虚函数吗
构造函数不可以是虚函数
虚函数调用是通过虚函数表指针来进行调用的,而虚函数表指针的初始化工作是在构造函数中完成的,因此无法通过虚函数表指针调用构造函数
7. 析构函数可以是虚函数吗
析构函数一般都建议写成虚函数
当调用派生类对象的析构函数时,如果基类的析构函数没有定义为虚函数,那么默认该派生类的析构函数是派生类的析构函数,不会调用基类的析构函数
导致只会释放派生类的资源和对象,而基类的资源对象会造成内存泄漏
8. 多态的缺点- 性能开销
虚函数调用需要到虚函数表中寻找,开销大
每一个对象还要包含一个虚函数表指针
动态绑定导致编译时不能进行优化 - 没有静态检查
由于是动态绑定,所以可能有些错误在运行时才会出现
通过基类的指针和引用无法访问子类的某些成员变量和成员函数,需要进行转换 - 代码设计更加复杂
设计复杂
调试复杂 - 代码膨胀
虚函数表的存在,会增加代码和数据区的大小
9. 虚函数表是在什么阶段生成的,在哪存放
虚函数表是在编译期间就生成的
一般存放在常量区
10. 同一个类的不同对象,用的是同一张虚表吗
是的
在编译期间会为含有虚函数的类生成一个虚函数表,虚函数表中存放的是虚函数地址
创建不同的对象,会在构造函数初始化虚函数表指针,但是指针指向的是同一个表
11. 一个类的对象可以包含多张虚表吗
一般不会,除非是多继承会间接包含
如果是单继承,会先继承基类的虚函数表,再进行重写,再把新增的虚函数加入到虚函数表中
如果是多继承,会为每一个基类生成一个独立的虚函数表,并在派生类中为每一个基类生成一个虚函数表指针,看起来是一个指针,实际上内部是有多个指针,每个基类对应一个
12. 抽象类的作用是什么 - 声明接口
通过抽象类派生出的派生类,都要提供抽象类的这些接口,是一种标准化的模式 - 强制实现
抽象类强制要求,派生类必须要实现抽象类当中的实现细节,否则也是抽象类 - 实现多态
抽象类是实现多态的基础,借助抽象类可以实现多态,进而形成一个接口,多种实现的效果
- 性能开销