概述
多态性是面向对象编程中的重要特征之一。其是用父类型别的指针(或引用)指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种由于调用子类的不同可以让父类的指针有“多种形态”,这是一种泛型技术。
多态实现方式:
- 函数重载
- 运算符重载
- 虚函数
多态实现
-
多态分类
1. 编译时多态(静态联编)
: 通过函数重载 和 运算符重载实现。
2. 运行时多态(动态联编)
: 通过类的继承关系 和 虚函数实现。
联编:也称绑定,这里是指计算机程序自身彼此关联的过程,即将函数名与对象关联的过程。
-
编译时多态的实现
概念 | 两个函数在同一作用域,函数名相同,参数列表不同。 |
表达方式 | 在一个类中重载 |
在不同类中重载 | |
基类的成员函数在派生类中重载 | |
被重载的同名函数在编译期间区分,编译器会通过:1. 根据实参类型;2. 使用类作用域限定符“::”区分;3. 根据调用函数的类对象区分 |
运算符重载:
运算符重载实质上是函数重载。
语法: 函数类型 operator 运算符名称(参数列表){ // 重载处理 }
说明:
- 运算符重载中:运算符顺序和优先级不能更改,但参数和返回类型可以重载;
- 运算符参数都是内部类型时不能重载;
- 基本数据之间关系是确定的,若允许在其上重新定义,那么基本数据类型的内在关系将混乱;
- C++规定:. 、:: 、.* 、?: 4个运算符不能重载;
-
运行时多态的实现
实现条件:
- 基类中必须存在虚函数,派生类必须对基类中的虚函数进行重写;
重写:两个函数作用域不同(分别在基类和派生类),函数名相同,参数列表相同,用virtual修饰即基类一定有虚函数。
规则:
1. 返回值类型相同;
2. 派生类重写基类的虚函数的 关键字"virtual" 可加可以不加;
3. 派生类虚函数必须与被重写的基类函数原型一致(返回值,函数名字,参数类型必须完全相同);
例外:协变:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。// 仅适用于返回值,而不是参数。 析构函数 :如果基类中的析构函数是虚函数,派生类虚构函数提供构成重写,且构成重写的虚函数的函数名字不同。
4. 派生类虚函数的访问权限可以与基类虚函数的访问权限不同,基类中虚函数访问权限必须是公有的。
- 必须通过基类指针或者引用来调用虚函数。
虚函数的注意事项:
- 构造函数不能虚函数。因为构造派生类对象方式不同于继承时,先调用派生类的构造函数,然后派生类的构造函数将使用基类的一个构造函数。而且虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。
- 析构函数可以为虚函数,且通常声明为虚函数。将析构函数声明为虚函数保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
- 友元函数不能为虚函数。因为友元函数不是类的成员函数,因为虚函数仅适合用于有继承关系的对象;
- 静态成员函数不能为虚函数,因为静态成员函数没有this指针,不受限与任一对象的;
- 内联函数不能为虚函数,因为内联函数不能在运行中动态确定;
- 如果派生类没有重写虚函数,则将使用该函数的基类版本。如果派生类在一条派生链中,则使用最新虚函数版本;
- 派生类被重写的方法,不仅隐藏参数列表相同的同名基类方法,而且隐藏参数列表不相同的同名基类方法;
纯虚函数与抽象类:
纯虚函数:
语法:virtual 函数返回值 函数名(参数列表)= 0;
说明:
- 声明为纯虚函数,基类中就不能在给出函数实现部分;
- C++中有一种情况:空的虚函数,空的虚函数指函数体为空,而纯虚函数压根没有函数体,有着本质区别;
- 纯虚函数必须在派生类中重写,否则这个函数就没有存在意义。
抽象类:
概念:含有纯虚函数的类称为抽象类。
作用: 抽象类是不能实例化对象的,它是为了组织一个继承的层次结构,专门为派生类提供基本类型和接口函数的基类。
所以说抽象类也被称为抽象基类,且必须被继承,否则没有存在意义。
-
多态调用原理
在这插入一个例子:
#include <iostream>
using namespace std;
class Base{
virtual void Func1()
{}
private:
int _val;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
这调用监视窗口查看测试代码的结果:
按我们所理解的,类中只有一个整型变量,应该占4字节,也就是说多了4个字节。
我们再查看我们所构建的对象就能清楚明白:
这是多存了一个指针_vfptr,这个指针所指向的是一个叫虚函数表(vtable)的东西。
虚表的结构可以查看相关博客:https://blog.csdn.net/timecur/article/details/95981159
派生类虚表构造过程:
1. 先将基类虚表中虚函数拷贝一份到派生类虚表中;
2. 如果派生类重写了基类中的虚函数,用派生类自己的虚函数替换(覆盖)相同偏移量位置同名的虚函数。
3. 将派生类新增加的虚函数按照其在类中声明次序添加到派生类虚表的最后。
重载 | 两个函数在同一作用域,函数名相同,参数列表不同 |
重写(覆盖) | 两个函数作用域不同(分别在基类和派生类),函数名相同,参数列表相同,用virtual修饰 |
重定义(同名隐藏) | 1.如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。 2.如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。 |
总结多态的调用步骤:
1. 从指针或引用的具体的对象中取虚表的地址;
2. 传递this指针;
3. 从虚表中取虚指针;
4. 调用该虚函数。
C++11 override 和 final
override 和 final 来修饰虚函数。
overrid修饰虚函数,表示要强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。 ---可以防止写错虚函数的函数名。
final修饰虚函数,表示最终虚函数,之后不能再被重写。