news 2026/9/29 10:11:03

【C++】多态——面向对象3大特性之一

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++】多态——面向对象3大特性之一

为什么多态

  • 继承:实现代码的复用
  • 多态:实现父子函数在调用(名字)相同的函数时实现不同的作用





分类

  • 编译时多态(静态多态):函数重载和函数模板
  • 运⾏时多态(动态多态):我们今天讲的多态





实现条件

  1. 基/父 类的指针或引用
  2. 调用虚函数

为什么传引用:

传引用或者指针不会被“切片”

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)。逐步拆解:

  1. p->test():test是A的虚函数,B没重写它,所以调A::test();
  2. 在A::test()内部,func()等价于this->func(),this的静态类型是A*,但真实指向B对象;
  3. func是虚函数 → 动态绑定 → 实际调B::func()(打印B->);
  4. 默认参数在编译期确定:this静态类型是A*,所以默认值取A::func的1,不是B的0;
  5. 结果: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(先取虚表再间接调用)





版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 10:10:20

计算机毕业设计 | SpringBoot+vue的图书馆管理系统(附源码)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 10:10:01

用Dify构建AI复盘助手:从事实提取到行动项生成的工作流实践

做项目管理第十年&#xff0c;我越来越觉得 Hindsight 这个词很妙。它的英文原意是"事后的理解"&#xff0c;落到工作里就是我们常说的复盘、回顾、后见之明。你没法在项目进行时拥有它&#xff0c;但它往往比任何事前计划都值钱。为了把这种"事后视角"变得…

作者头像 李华
网站建设 2026/9/29 10:09:59

Altium Designer工程实战:约束驱动设计与可制造性闭环

1. 这不是“软件安装教程”&#xff0c;而是一份Altium Designer真实工程现场的生存指南Altium Designer&#xff0c;这五个字在PCB设计圈里&#xff0c;几乎等同于“吃饭喝水”一样的日常存在。但凡你做过哪怕一块四层板&#xff0c;跟Layout工程师开过一次评审会&#xff0c;…

作者头像 李华
网站建设 2026/9/29 10:09:01

ARM-Linux交叉编译工具链安装与Qt/Boost/chrony避坑指南

ARM-Linux 交叉编译工具链安装这件事&#xff0c;说简单也简单&#xff0c;apt 一条命令就能把 gcc-arm 拉下来&#xff1b;说麻烦也麻烦&#xff0c;真到 Qt、Boost、chrony 这些依赖上&#xff0c;工具链选错一个 ABI&#xff0c;后面全是坑。我这几年前后在 x86 笔记本、Ubu…

作者头像 李华
网站建设 2026/9/29 10:07:29

Android自动化触发GC的原理与安全实践

1. 项目概述&#xff1a;为什么在Android上“主动触发GC”是个既常见又危险的操作&#xff1f;“Android 自动化触发GC”这个标题&#xff0c;乍看像是个技术小技巧&#xff0c;但背后藏着整个Android内存管理生态里最微妙、最常被误解的实践之一。我从2013年开始做Android性能…

作者头像 李华