多态概念:
多种形态,完成某个具体行为,不同的对象完成时可能会有不同的效果
多态:看实际指向的实体
非多态:看类型
例如:微信抢红包
多态的定义与实现
1.构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生不同的行为。
必须通过基类的指针或者引用调用虚函数。
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
//基类的虚函数重写
//子类中有和父类,函数接口完全相同(返回值、函数名、参数都相同)的虚函数(协变、析构重写除外)
//**协变的返回值可以不同,但返回值必须是父子关系,父类返回值是父类,子类返回值是子类**
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
//引用调用
void fun(Person& p) {
p.BuyTicket();
}
//指针调用
void fun2(Person* p) {
p->BuyTicket();
}
void test() {
Person p;
Student s;
fun(p);
fun(s);
fun2(&p);
fun2(&s);
}
注意:父类的函数必须是虚函数,否则就会形成函数隐藏,子类的函数加不加virtual都是函数重写,但最好都加上。
析构函数的重写
1.只要父类的析构函数是虚函数,则子类的析构函数和父类的析构函数构成重写
原因:编译器对继承关系下的所有类的析构函数的名字作了统一处理,保证了继承关系下所有类的析构函数同名,一般把析构函数的名称统一成desturctor
关键字
final
final定义的函数不能被重写,体现实现继承,一般在父类中使用
override
override定义的函数被强制重写,一般是强制子类函数重写父类的某一个虚函数,体现接口继承
抽象类定义
包含纯虚函数的类是抽象类,抽象类不能实例化,而抽象类的存在是为了实现多态,接口继承。
子类在继承抽象父类的时候,重写父类的纯虚函数,那么此时父类也会获得子类重写之后的函数效果
纯虚函数
没有函数体的虚函数
多态的基本原理
class Car{
public:
virtual void drive(){}
//包含一个隐含成员变量:虚函数表指针,该变量大小与操作系统位数有关
//32位占4个字节,64位占8个字节
}
#include<iostream>
using namespace std;
class Base{
public:
virtual void Func1(){
cout << "Base::Func1()" << endl;
}
virtual void Func2(){
cout << "Base::Func2()" << endl;
}
void Func3(){
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base{
public:
virtual void Func1(){
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void test(){
Base b;
Derive d;
}
多态的实现依赖于虚函数表,如下图
图中变量b和d中各包含一个_vfptr虚表指针,该指针指向一个虚表,虚表中存放各自的虚函数指针;
如果子类中有对父类重写的虚函数,则子类的虚表中对应位置使用子类虚函数指针进行覆盖;
若没有对父类的任何虚函数进行重写,则与父类的虚表相同;
虚函数指针在虚表中的存放位置和其声明或定义顺序是一致的,子类新定义的虚函数,按顺序放在虚表后面,虚表一般以nullptr结尾
总结
多态原理
如果访问的是虚函数,则通过指针/引用直到实际指向的实体,获取实体中的虚表指针,通过虚表指针访问虚表,在虚表中找到需要执行的虚函数的指针,再通过虚函数指针找到执行具体的函数行为
继承下的虚表
单继承虚表:
1.子类继承父类虚表
2.用重写的虚函数指针覆盖父类对应的虚函数指针
3.子类新定义的虚函数,其指针按照声明或者定义的顺序存入虚表的尾部
多继承虚表
1.虚表个数:等同于直接父类的个数
2.子类继承所有父类的虚表
3.用重写的虚函数指针覆盖父类中对应的虚函数指针
4.子类新定义的虚函数,其指针按照声明或者定义的顺序存入第一个直接父类的虚表的尾部