前言
多态分为两类 ——
静态的多态:函数重载。传入不同参数,看起来调用一个函数,但是有不同的行为,最典型的比如流插入流提取的“自动识别类型”
int i = 10; double d = 1.1; cout << i; //cout.operator<<(int) cout << d; //cout.operator<<(double)
动态的多态:一个父类的引用或指针调用同一个函数,传递不同的对象,会调用不同的函数
怎么样区分呢?
静态:在编译时决议,(编译时决定调用谁)
动态:在运行时决议,(运行时决定调用谁)
不过本文主要围绕的是动态的多态进行展开
一. 多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
比如买票,我们想让不同身份的人,买票的价格不同,就可以借助多态实现
class Person { public: virtual void BuyTicket() { cout << "买票——全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; class Soldier : public Person { public: virtual void BuyTicket() { cout << "优先买票" << endl; } }; void Func(Person& p)//父类的指针/引用 { p.BuyTicket();//虚函数重写 } int main() { Person ps; Student st; Soldier sd; Func(ps);//传父类对象 —— 调父类的 Func(st);//传子类对象 —— 调子类的 Func(sd);//传子类对象 —— 调子类的 return 0; }
其中子类的函数满足 三同(返回值类型、函数名、参数列表完全相同)的虚函数这两个条件,叫做重写(覆盖)
ps:此时的函数名相同,但是不构成隐藏,不满足三同的才叫做隐藏
这样就可以什么人对应什么政策
二. 多态的定义及实现
🌈多态的条件
🥑多态有两个条件,缺一不可:
必须通过父类的 指针或者引用调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数:被virtual修饰的类成员函数
重写要求 :虚函数 + 三同(父类和子类的返回值类型、函数名字、参数列表完全相同)(形参名和缺省参数名不一样不影响)
构成多态,传的哪个类型的对象,调用的就是哪个类型的虚函数 - 跟对象有关
不构成多态,调用的就是p类型函数 - 跟类型有关
下面进行验证:如果用对象来调用,够不够构成多态
void Func(Person p) { p.BuyTicket(); }
我们思考为什么一定要是父类的指针或引用呢?为什么是父类?为什么是指针和引用?
因为只有指针和引用访问才能实现晚绑定,如果使用的是对象的话,在编译期间就已经绑定完毕了(也就是已经确定call好了地址了),也就不能实现多态
我们知道一个子类的第一个成员是父类成员,父类下的第一个就是虚表指针,我们是要通过父类指针来找到这个虚表的!如果是通过子类去访问就是静态绑定,不能达到动态调节
没有重写的话,是编译时决定还是运行时决定地址?
调试打开反汇编可以看见是运行时决定的
此处的编译器并没有完全检查你是否重写,只是初略的检查是否是虚函数以及父类指针调用,但是调用的还是同一个虚函数,因为没有完成覆盖
🌈虚函数重写的两个特例
🥑协变
协变,返回值可以不同,但要求必须是父子关系的指针或者引用
实际上用的不多
🥑析构函数的重写
如果析构函数构是虚函数,这里构成重写吗?yes!但是他们的函数名不相同啊,因为析构函数名被特殊处理了,都处理成了destructor(),至于为什么要特殊处理,就是源于多态
//建议在继承中析构函数定义成虚函数 class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: //析构函数名会被处理成destructor,所以完成了重写 virtual ~Student() { cout << "~Student()" << endl; } }; int main() { Person p; Student s; return 0; }
普通的场景下是没有出现问题的,但是有特殊的场景!要记住,面试高频考点
✨ 那什么场景下,析构函数要是虚函数呢?
Person* ptr1 = new Person; delete ptr1; Person* ptr2 = new Student; delete ptr1;
如果不是虚函数,那也就不构成多态,那与类型有关,都会去调用父类的析构函数,但是这样会导致子类对象可能有资源未被清理,我们希望的是父类调用父类,指向子类调用子类的(完了再调用父类),这样是不是就符合我们多态的理念
析构函数的重写很简单,因为函数名“相同”,没有参数,加一个virtual就可以
在其他场景,析构函数是不是虚函数都可以
🌈只有父类带 virtual 的情况
虚函数,允许父子类两个都是虚函数 或 只有父类是虚函数也行。这其实是C++不是很规范的地方,建议两个都写上virtual
这是因为虽然子类没带virtual,但是它 继承了父类的虚函数属性,重写是实现
🌈C++11 final & override
🥑final
final有两个功能
修饰一个类,这个类不能被继承
修饰虚函数,限制它不能被子类中的虚函数重写
C++11中final还可以限制重写
修饰虚函数,限制它不能被子类中的虚函数重写
🥑override
override放在子类重写的虚函数后面,帮助检查是否完成重写,没有重写会报错
类似于核酸检测,,没有做就报错(做核酸魔怔了)
三. 重载 vs 重写 vs 隐藏
四. 抽象类
💛 包含纯虚函数的类叫做抽象类(接口类)。在虚函数的后面写上=0 ,则这个函数为纯虚函数
纯虚函数一般只声明,不实现,抽象类不能实例化出对象;相当于间接强制你重写!
即使我们创造了一个子类对象,其派生类继承后也不能实例化出对象,因为继承了抽象类后,这个派生类就继承了纯虚函数,那它同样也是一个抽象类!
只有重写纯虚函数,派生类才能实例化出对象。所以呀,抽象类本质上强制继承它的子类完成虚函数重写
class Car { public: virtual void Drive() = 0; }; class BMW: public Car { public: virtual void Drive() { cout << "操控——好开" << endl; } }; class Benz :public Car { public: virtual void Drive() { cout << "Benz-舒适" << endl; } }; int main() { //BMW b; Car* ptr = new BMW; ptr->Drive(); Car* ptr = new Benz; ptr->Drive(); return 0; }
ps:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口,重写的是实现。所以如果不实现多态,不要把函数定义成虚函数
五. 多态的原理
🔥虚函数表
⚡引入
// 其中sizeof(Base)是多少? class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; char _ch = 'A'; }; int main() { cout << sizeof(Base) << endl; }
如果我只考虑到了内存对齐的话,答案就是8
但此处的考点不仅仅只有内存对齐,真正考察的是多态,那究竟是什么东西的存在多了4个字节
通过监视窗口,发现这个对象多了一个成员,虚函数表指针_vfptr(virtual function table)(简称虚表指针) ,所谓的虚函数表就是一个指针数组,里面存放的是函数指针(放的是虚函数地址),一般这个数组的最后面放了一个nullptr——
虚函数等等的函数都是放在代码段的!
记住对象里面没有虚表,只有指向虚表的指针;
🔥多态的原理
虚函数表是理解多态原理的关键,下面将在底层剖析
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } protected: int _a = 0; }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } protected: int _b = 0; }; void Func(Person& p) { p.BuyTicket(); } int main() { Person Scort; Func(Scort); Student Durant; Func(Durant); return 0; }
虚函数的“重写”也叫“覆盖”,重写是语法上的概念,覆盖是原理层的概念;子类继承父类的虚函数,可以认为深拷贝了一份虚函数表,没有重写时,子类与父类虚表完全相同;若重写了,便会用新地址覆盖。
🍂转到反汇编可以发现:
对于普通成员函数的调用,是在编译后就已经确定了调用地址(橙色的);
给父类/子类对象,调用虚函数p.BuyTichet()的汇编代码却是相同的,那就说明此时调用函数时,不再是直接确定地址,而是借助了eax这个寄存器,这是多态原理的关键
(汇编不要求全部看懂,懂大概意思就可)
🟢多态的本质原理,基类的指针/引用指向谁,就去谁的虚函数表中找到对应位置的虚函数进行调用,这是在运行中确定的,所以叫动态的多态
而普通函数,在编译链接的时候已经确定了函数运行地址,直接调用即可
🔥小细节
p1和p2是共用一个虚表吗?
class Person { public: virtual void BuyTicket() { cout << "买票——全价" << endl; } }; int main() { Person p1; Person p2; return 0; }
结论是:同一类型的对象共用一个虚表
class Person { public: virtual void BuyTicket() { cout << "买票——全价" << endl; } }; class Student :public Person { public: //virtual void BuyTicket() { cout << "买票——半价" << endl; } }; int main() { Person p1; Person p2; Student s1; Student s2; return 0; }
结论:vs下,不管是否完成重写,子类虚表和父类的虚表不是同一个