1. 什么是多态?
对于同一种指令,针对不同的对象产生不同的行为
分为静态多态和动态多态:静态多态发生在编译时 --- 函数重载、运算符重载、模板;动态多态发生在运行时 --- 虚函数
2. 虚函数
2.1 虚函数的定义
成员函数名前 + virtual关键字
在派生类中重写虚函数:函数名、参数个数、类型、顺序、返回类型必须和基类中一致
2.2 虚函数的实现:
基类定义虚函数时,会在基类存储布局顶部生成一个虚函数指针指向虚函数表。派生类继承虚函数时同样会继承虚函数,并生成派生类自己的虚函数指针和虚标。派生类重写虚函数,覆盖虚表中的函数入口地址。
NOTICE: 对虚函数的解析发生在运行时。
动态多态被激活的条件
- 基类定义虚函数
- 派生类重写虚函数
- 创建派生类对象
- 基类指针指向派生类对象 / 基类引用绑定派生类对象
- 基类指针(引用)调用虚函数
Q:动态多态和虚函数等价吗?
A:否,体现动态多态必须有虚函数,但有虚函数不一定能体现动态多态。
2.3 不能被设置为虚函数的函数
① 普通函数(非成员函数)
② 静态成员函数
- 发生时机:编译阶段;虚函数在运行阶段
- 被所有对象共享,无法通过this指针找到虚函数入口地址
③ 内联函数
- 发生时机不一致
- 在inline前添加virtual会使其失去内联意义
④ 友元函数
- 如果函数本身是普通函数,则不能设置
- 如果函数本身是另一个类的成员函数,则可以设置
⑤ 构造函数
- 不能被继承,但虚函数可以被继承
- 发生时机不对
- 虚函数的调用需要查询虚表,而查询虚表需要虚函数指针,虚函数指针位于内存布局的顶部,如果构造函数没有执行,则无法得到虚函数指针
2.4 虚函数的访问
① 引用访问 --- 动态联编,和指针相同
② 对象访问 --- 静态联编,无法体现动态多态
③ 成员函数访问 --- 动态联编,使用this指针
④ 构造函数和析构函数中访问 --- 静态联编,编译器将虚函数的调用解释为“本类名::虚函数名”,如果在本类中没有实现,则调用基类中的虚函数,但绝不会调用派生类中的虚函数。
静态多态:先期联编 / 静态联编 / 编译时多态
动态多态:晚期联编 / 动态联编 / 运行时多态
3. 特殊的虚函数
3.1 纯虚函数
virtual type funcName(parameters) = 0
在基类中只声明,不定义。
Q:什么是抽象基类?
A:抽象基类有两种形式。一是含有纯虚函数的类;二是将构造函数访问权限设为protected。(抽象基类无法创建对象)
3.2 虚析构函数
① 原因:如果存在基类的指针指向派生类对象,当想通过该指针delete派生类对象时,只会执行基类的析构函数而不会执行派生类析构函数,造成内存泄漏。
② 如果基类的析构函数为虚函数,则派生类中的析构函数自动成为虚函数。
③ 任何一个类都只有一个析构函数且无法重载,因为编译器将所有的析构函数解释为destructor。
4. 重载、隐藏和覆盖
4.1 定义
重载:发生在同一个作用域。函数名相同,返回类型和参数列表不完全相同。
隐藏:发生在基类和派生类。函数名相同,返回类型和参数列表不一定相同。
覆盖:发生在基类和派生类。函数名、返回类型和参数列表完全相同。
4.2 三者的区别
作用域 | 虚函数 | 函数名 | 参数列表 | 返回类型 | |
重载 | 相同 | 不一定 | 相同 | 不同 | 不一定 |
隐藏 | 不同 | 不一定 | 相同 | 不一定 | 不一定 |
覆盖 | 不同 | 存在 | 相同 | 相同 | 相同 |