为什么多态
- 继承:实现代码的复用
- 多态:实现父子函数在调用(名字)相同的函数时实现不同的作用
分类
- 编译时多态(静态多态):
函数重载和函数模板 - 运⾏时多态(动态多态):我们今天讲的多态
实现条件
- 基/父 类的指针或引用
- 调用虚函数
为什么传引用:
传引用或者指针不会被“切片”
1.虚函数
- 成员函数前面加上virtual就是虚函数
- 写法:
继承中的virtual:
- 写法:
class Student : virtual public Person- 为了D中不会出现A的两份数据
classPerson{public:virtualvoidBuyTicket(){/* TODO: 输出 "买票-全价" */cout<<"买票-全价"<<endl;}};classStudent:publicPerson{public:// TODO: 重写 BuyTicket,输出 "买票-打折"virtualvoidBuyTicket(){cout<<"买票-打折"<<endl;}};voidFunc(Person*ptr){// TODO: 通过 ptr 调用虚函数 BuyTicketptr->BuyTicket();}intmain(){Person ps;Student st;// TODO: 分别把 &ps 和 &st 传给 FuncFunc(&ps);Func(&st);return0;}2.虚函数的重写/覆盖
- 定义:父类、子类有相同的虚函数
- 父类必须写“virtual”,子类可以不写,但是不规范
classAnimal{public:virtualvoidtalk()const=0;// 虚函数};classDog:publicAnimal{public:// TODO: 重写 talk,输出 "汪汪"virtualvoidtalk()const{cout<<"汪汪"<<endl;}};classCat:publicAnimal{public:// TODO: 重写 talk,输出 "(>^ω^<)喵"virtualvoidtalk()const{cout<<"喵喵"<<endl;}};voidletsHear(constAnimal&animal)// 注意:基类引用{// TODO: 调用 talkanimal.talk();}intmain(){Cat cat;Dog dog;// TODO: 分别传入 cat 和 dogletsHear(cat);letsHear(dog);return0;2.1 虚函数的默认参数陷阱(选择题)
要实现什么:理解一个易错点——重写虚函数时,默认参数在编译期就确定了,用的是「当前调用点静态类型」的默认值,而不是实际被调用的派生类函数的默认值。
题目:以下程序输出结果是( )
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确classA{public:virtualvoidfunc(intval=1){cout<<"A->"<<val<<endl;}virtualvoidtest(){func();}};classB:publicA{public:voidfunc(intval=0){cout<<"B->"<<val<<endl;}};intmain(){B*p=newB;p->test();// 问:输出什么?return0;}答案:B(B->1)。逐步拆解:
p->test():test是A的虚函数,B没重写它,所以调A::test();- 在
A::test()内部,func()等价于this->func(),this的静态类型是A*,但真实指向B对象; func是虚函数 → 动态绑定 → 实际调B::func()(打印B->);- 默认参数在编译期确定:
this静态类型是A*,所以默认值取A::func的1,不是B的0; - 结果:
B->1。
p->func();// 输出:B->0而p->func()中p是B*,静态类型是B,默认值取B的0,输出B->0。
4.析构函数是虚函数
- 前提:
- B继承了A
A* p2 = new B;// 基类指针指向派生类对象
- 结果:
- 子类对象(
B)在内存中是包含了父类对象(A)的部分的。必须先把上面的房子(子类)拆掉,再拆地基(父类)。 delete p2;: 调 ~B() 再 ~A()
- 子类对象(
#include<iostream>usingnamespacestd;classA{public:virtual~A(){cout<<"~A()"<<endl;}};classB:publicA{public:~B(){cout<<"~B()->delete:"<<_p<<endl;delete_p;// 释放 B 自己申请的资源}protected:int*_p=newint[10];};intmain(){A*p1=newA;A*p2=newB;// 基类指针指向派生类对象deletep1;// 调 ~A()deletep2;// 调 ~B() 再 ~A(),因为 ~A 是虚函数 → 多态return0;}5.协变
- 了解即可
- 是什么
- 一种「返回值类型不同、但也能构成重写」的特殊情况
- 基类虚函数返回基类对象的指针或引用,派生类重写时返回派生类对象的指针或引用,也构成重写。
6.override 和 final 关键字
override:检查「到底有没有真的重写成功」;final:禁止派生类再重写某个虚函数。- 位置:函数声明的最后面(参数列表的括号后面,函数体或分号的前面)
#include<iostream>usingnamespacestd;// ============ override:检查是否真的构成重写 ============// 下面故意把 Dirve 拼错(正确是 Drive),所以没有重写任何基类函数,// 加了 override 编译器直接报错:// error C3668: "Benz::Drive": 包含重写说明符 "override" 的方法没有重写任何基类方法classCar{public:virtualvoidDirve(){}// 注意基类这里写成了 Dirve(老师故意挖坑)};classBenz:publicCar{public:virtualvoidDrive()override// 编译报错{cout<<"Benz-舒适"<<endl;}};// ============ final:禁止派生类重写 ============// error C3248: "Car::Drive": 声明为 "final" 的函数无法被 "Benz::Drive" 重写classCar2{public:virtualvoidDrive()final{}};classBenz2:publicCar2{public:virtualvoidDrive()// 编译报错{cout<<"Benz-舒适"<<endl;}};intmain(){return0;}7.纯虚函数 和 抽象类
- 纯虚函数写法:虚函数后面加上“ = 0”
- 抽象类定义:
- 含有纯虚函数的类是抽象类
- 子类继承的父类有纯虚函数,并且子类没有重写该函数,那么子类也是抽象类
- 比喻:
- 纯虚函数:父类给子类留下的一个必须完成的任务
- 抽象类:就像车这个东西,如果不加上车牌号等参数,那就并不完整,不能上路
classCar{public:virtualvoidDrive()=0;// TODO: 定义纯虚函数(在虚函数声明后写 = 0)};classBenz:publicCar{public:virtualvoidDrive(){// TODO: 输出 "Benz-舒适"cout<<"Benz-舒适"<<endl;}};classBMW:publicCar{public:virtualvoidDrive(){// TODO: 输出 "BMW-操控"cout<<"BMW-操控"<<endl;}};intmain(){// Car car; // TODO 思考:这行为什么编译报错?// TODO: 用基类指针指向 Benz 和 BMW,并调用 DriveCar*car=newBMW;car->Drive();return0;}多态的原理
1.虚函数表指针/虚表指针
- **是什么:**在含有虚函数的类创建的对象中,会在开头包含一个指针,这个指针指向一个函数指针数组的地址,通过这个指针就能找到这个类对象中的所有虚函数
- 有什么用:实现多态——>运行时,先找虚表,再根据虚表中的地址找到对应的虚函数
- 个数:可能有多个,
- 因为:一般继承下来的基类部分里已有虚表指针,派生类自己就不再单独生成一个;
- 继承多个类,就会继承多个虚表
1.1 规则
- 基类对象的虚表存「基类所有虚函数」的地址;
- 派生类由两部分组成:继承下来的基类部分 + 自己的成员。一般继承下来的基类部分里已有虚表指针,派生类自己就不再单独生成一个;
- 派生类重写了基类的虚函数,虚表中对应位置就被覆盖成派生类的函数地址;
- 派生类虚表 = 基类虚函数地址(未被重写的)+ 被覆盖的派生类函数地址 + 派生类自己新增的虚函数地址。
1.2动态绑定 和 静态绑定
- 满足多态条件时是「动态绑定」(运行时到虚表找地址),否则是「静态绑定」(编译时直接确定地址)。
| 静态绑定 | 动态绑定 | |
|---|---|---|
| 条件 | 不满足多态条件(如非虚函数、或对象直接调用) | 指针/引用 + 虚函数 |
| 时机 | 编译时确定函数地址 | 运行时到虚表查地址 |
| 汇编特征 | call Student::BuyTicket(直接地址) | mov eax,[edx]; call eax(先取虚表再间接调用) |