关于C++多态的复习总结

多态

简介: 面向对象的三大特性之一,多态顾名思义即具有多种形态,即去执行某个行为时,当不同的对象去执行时会产生不同的状态

构成多态的条件

条件一

必须通过基类(父类)的指针或者引用调用虚函数(函数被virtual所修饰)
tips:父类的指针或引用要指向或引用子类对象

virtual void test(){}

条件二

被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
tips:破坏任意一个条件都会导致无法构成多态

  • 虚函数的重写是接口继承(普通函数的重写是实现继承)
    ps:普通函数的重写是外壳不同,将函数体(实现)继承下来
  • 子类中的虚函数只是对父类的接口的一个声明(声明必须保持一致,故函数名,形参以及返回值都相同),重写只是将父类函数的“外壳”拿了下来,然后再在这个“外壳”内填充函数体(重写的是实现)

满足上述两个条件即构成多态:即通过基类(父类)的指针或者引用调用虚函数
子类没重写也会进行运行时决议(多态),但是由于没有进行覆盖,仍旧调用的是父类的虚函数

虚函数重写/覆盖的条件

虚函数+三同(函数名,形参以及返回值都相同),不符合重写的条件即构成隐藏

  • tips1:子类重写虚函数时,是否添加virtual修饰对多态不影响

  • tips2:重写的协变,协变即返回值可以不同,但要求父子函数的返回值必须分别是父子关系的指针或者引用
    如下述两种方式都是可以构成多态(此种用途并不多,了解即可)

    class Person
    {
    public://virtual void BuyTicket(char) { cout << "买票-全价" << endl; }/*virtual Person* BuyTicket(int) { cout << "买票-全价" << endl;return this;}*///假设A是B的父类,即下述也构成多态virtual A* BuyTicket(int){cout << "买票-全价" << endl;return nullptr;}
    };class Student : public Person {
    public:// 虚函数重写/覆盖条件 : 虚函数 + 三同(函数名、参数、返回值)// 不符合重写,就是隐藏关系// 特例1:子类虚函数不加virtual,依旧构成重写 (实际最好加上)// 特例2:重写的协变。返回值可以不同,要求必须时父子关系的的指针或者引用/*virtual Student* BuyTicket(int){cout << "买票-半价" << endl;return this;}*/virtual B* BuyTicket(int){ cout << "买票-半价" << endl;return nullptr;}
    };
    

构成多态的原理

虚表(虚函数表)

  • 虚表内存储着所有的虚函数(函数地址),虚表本质是一个数组,数组内存着的都为函数指针

    • 父类对象和子类对象里各自都有各自的虚表
      子类对象的虚表是拷贝父类对象得来
    • 当子类重写虚函数时,则修改了子类自己的虚表对应的函数(覆盖成子类重写后的函数)

虚表指针

当类内存在了virtual修饰的虚函数,则该类内会默认生成一个虚表指针(__vfptr)指向一张虚表
虚表指针在vs环境下,默认是在对象的头4个字节或者头8个字节(可以通过取出该字节的内容所指向的地址来打印虚表)
ps: 虚函数表是编译时即生成的,在构造函数中进行初始化虚表指针,对象中存储的为虚表指针,虚表存储位置大致在常量区(编译阶段即生成好了)

  • tips:可以按下述方式尝试打印虚表内的内容

    class Person 
    {
    public:virtual void BuyTicket() { cout << "Person::买票-全价" << endl;}virtual void Func1(){cout << "Person::Func1()" << endl;}
    };class Student : public Person {
    public:virtual void BuyTicket() { cout << "Student::买票-半价" << endl;}virtual void Func2(){cout << "Student::Func2()" << endl;}
    };typedef void(*VFPTR)();//void PrintVFTable(VFPTR table[])
    //void PrintVFTable(VFPTR* table, size_t n)
    void PrintVFTable(VFPTR* table)
    {//vs下,虚表末尾会加上空指针作为标识for (size_t i = 0; table[i] != nullptr; ++i)//for (size_t i = 0; i < n; ++i){printf("vft[%d]:%p->", i, table[i]);//table[i]();VFPTR pf = table[i];pf();}cout << endl;
    }
    int main()
    {// 同一个类型的对象共用一个虚表Person p1;Person p2;// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个Student s1;Student s2;//取到对象头四个字节的虚表指针中的函数地址,再强转成函数指针(因为本身就是函数地址)PrintVFTable((VFPTR*)*(int*)&s1);PrintVFTable((VFPTR*)*(int*)&p1);
    }
    

普通多继承下的情况

  • 多继承中,子类新增的虚函数会被放到多继承下来的第一个对象的虚表里
    多继承的对象有几个,则子类中有多少个虚表(从父类继承得来)
    • 多继承下,子类自身的虚函数会被放到多继承第一个对象的虚表中
    • 多继承下,不同的父类指针指向子类对象并调用虚函数时,底层实现略有不同,不过到底也是相同的
      因为调用函数时,本质也要传入指向对象的地址,继承的两个基类所在的地址不同,故此底层跳转步骤不尽相同
      • 如果是第一个继承的对象,则是直接进行call函数地址然后jump到函数实现
      • 如果是第二个继承的对象,则会先call指令,然后会先偏移到子类对象的首地址处,再进行jump

菱形继承下的情况

  • 最开始菱形继承中,如果菱形继承的两个父类没有额外的虚函数,则是共用基类的虚表(通过虚基表指向)

  • 如果菱形继承下,两个父类还有额外的虚函数,则父类其还会拥有自己的虚表指针(指向虚表)

  • 虚基表:虚继承中产生的虚基类表(解决数据冗余和二义性)

    • 虚基表的记录的内容其一是当前派生类的虚基表与其虚表的偏移量(如果不存在额外的虚表则偏移量为0)
      为了让派生类能够找到其虚表的位置
    • 其二是记录虚基类与其派生类在当前对象模型中的偏移地址
  • 只要是虚函数,函数地址都会放入虚表中,无论是否被重写,子类的虚表中既有父类的虚函数,也有子类的虚函数
    tips:同一个类型的对象共用一个虚表,(vs环境)子类和父类的虚表不管是否完成重写,二者虚表都不是同一个

总结

多态的本质即当符合多态的两个条件,调用时则会到指向对象的虚表中找到对应的函数地址进行调用
故此多态的调用时运行时才通过虚表确定了函数的地址,编译时并不知道会自身会指向父类还是子类的对象,运行到了才会到实际指向对应对象的虚表内找到函数地址再进行调用
(普通函数的调用是调用call指令,在编译链接时确定了函数的地址(声明+定义),运行时直接调用)


析构函数的重写

父类的析构函数在继承中建议添加virtual修饰,完成虚函数的重写

class Person{public:virtual ~Person(){cout << "~Person()" << endl;}
}
class Student{public:virtual ~Student(){cout << "~Student()" << endl;}
}
int main(){Person* ptr1 = new Person();delete ptr1;//如果不构成多态,则是什么类型即调用什么类型的析构函数,不符合预期Person* ptr2 = new Student();delete ptr2;//构成多态则指向父类调用父类析构,指向子类调用子类析构,子类析构后再自动调用父类的析构函数
}
  • 编译器生成的析构函数的作用(同上)
    自己调用自己的析构,父类对象去调用父类的析构
    • 由于多态的需要,析构函数的名字会被统一处理成destructor()
      所以析构函数也会与父类构成隐藏
    • 由于语法与编译器要求,构造时需要先构造父类,再构成子类,析构则需要保证先析构子类,再析构父类,所以自定义析构函数时不需要显式调用父类的析构,编译器会在析构完子类后自动调用父类的析构

override和final关键字

  • 当一个虚函数不想被重写,则使用final进行修饰(使用场景极少)
  • override用于修饰子类的虚函数,其对子类的虚函数是否重写进行了强制性语法检查

重载、覆盖(重写)、隐藏(重定义)的对比

  • 重载
    • 两个函数在同一作用域
    • 函数名相同,参数不同(个数,类型,顺序)
  • 重写(覆盖)
    • 两个函数分别在基类和派生类的作用域
    • 函数名,参数,返回值都必须相同(协变属于例外)
    • 两个函数必须都是虚函数(用virtual修饰)
  • 重定义(隐藏)
    • 两个函数分别在基类和派生类的作用域
    • 函数名相同
    • 基类与派生类的同名函数不构成重写即为重定义(隐藏)

抽象类

在虚函数后面写上=0,则这个函数为纯虚函数,包含这个纯虚函数的类即为抽象类(也被成为接口类)

  • 抽象类基类是无法实例化出对象的
  • 子类继承了纯虚类后,子类必须得进行虚函数的重写,否则也无法实例化对象

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/11578.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

宁夏银川市起名专家的老师颜廷利:死神(死亡)并不可怕,可怕的是...

在中国优秀传统文化之中&#xff0c;汉语‘巳’字与‘四’同音&#xff0c;在阿拉伯数字里面&#xff0c;通常用‘4’来表示&#xff1b; 湖南长沙、四川成都、重庆、宁夏银川最靠谱最厉害的起名大师的老师颜廷利教授指出&#xff0c;作为汉语‘九’字&#xff0c;倘若是换一个…

FreeRTOS中断管理

FreeRTOS中断管理 基于STM32_stm32 freertos 按键中断-CSDN博客 更加详情请看以上链接↑ 中断优先级 任何中断的优先级都大于任务! 在我们的操作系统,中断同样是具有优先级的,并且我们也可以设置它的优先级,但是他的优先 级并不是从 0~15 ,默认情况下它是从 5~15 ,…

[ACTF新生赛2020]SoulLike

没见过的错误&#xff1a; ida /ctg目录下的hexrays.cfg文件中的MAX_FUNCSIZE64 改为 MAX_FUNCSIZE1024 然后就是一堆数据 反正就是12个字符 from pwn import * flag"actf{" k0 for n in range(12):for i in range(33,127):pprocess("./SoulLike")_flag…

94.二叉树的中序遍历

刷算法题&#xff1a; 第一遍&#xff1a;1.看5分钟&#xff0c;没思路看题解 2.通过题解改进自己的解法&#xff0c;并且要写每行的注释以及自己的思路。 3.思考自己做到了题解的哪一步&#xff0c;下次怎么才能做对(总结方法) 4.整理到自己的自媒体平台。 5.再刷重复的类…

Python爬虫入门:网络世界的宝藏猎人

今天阿佑将带你踏上Python的肩膀&#xff0c;成为一名网络世界的宝藏猎人&#xff01; 文章目录 1. 引言1.1 简述Python在爬虫领域的地位1.2 阐明学习网络基础对爬虫的重要性 2. 背景介绍2.1 Python语言的流行与适用场景2.2 网络通信基础概念及其在数据抓取中的角色 3. Python基…

今日总结2024/5/13

今日学习了01背包求具体方案的方法 Acwing.12 背包问题求具体方案 由于背包是从小到大枚举物品&#xff0c;只能从后往前判断是从哪个状态递推过来的&#xff0c;而该题要求按字典序顺序输出字典序最小的最优方案 因此要将物品从大到小枚举&#xff0c;判断时从小到大判断是…

在Windows上有哪些好用的网络抓包工具?

2024年5月12日&#xff0c;周日上午 在Windows上&#xff0c;有多种好用的网络抓包工具&#xff0c;以下是一些常见的选项&#xff1a; Wireshark&#xff1a; Wireshark 是一款功能强大的网络协议分析工具&#xff0c;它可以捕获并分析计算机网络上的数据包。它支持广泛的协议…

ssm+vue的公务用车管理智慧云服务监管平台查询统计(有报告)。Javaee项目,ssm vue前后端分离项目

演示视频&#xff1a; ssmvue的公务用车管理智慧云服务监管平台查询统计&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&…

求阶乘n!末尾0的个数溢出了怎么办

小林最近遇到一个问题&#xff1a;“对于任意给定的一个正整数n&#xff0c;统计其阶乘n&#xff01;的末尾中0的个数”&#xff0c;这个问题究竟该如何解决&#xff1f; 先用n5来解决这个问题。n的阶乘即n!5!5*4*3*2*1120&#xff0c;显然应该为2个数相乘等于10才能得到一个结…

软件测试自动化:加速测试,提升效率

目录 测试自动化的内涵 测试自动化的原理 测试工具的分类和选择 自动化测试的引入 在当今的软件开发中&#xff0c;测试自动化已经成为提升效率和确保软件质量的关键环节。测试自动化是指使用软件工具和脚本来执行重复的测试任务&#xff0c;从而减轻人工测试的负担&#x…

量化交易包含些什么?

我们讲过许多关于量化交易的内容&#xff0c;但是量化交易具体可以做些什么&#xff1f;很多朋友都还不清楚&#xff0c;我们详细来探讨下&#xff01; 第一&#xff1a;什么是量化交易&#xff1f; 量化交易是一种利用先进的数学模型和计算机技术&#xff0c;从大量的历史数…

制造业精益生产KPI和智慧供应链管理方案和实践案例分享

随着工业4.0的推进和国家对制造业高质量发展的重视&#xff0c;工业数据已跃升为生产经营活动中不可或缺的核心要素&#xff0c;同时&#xff0c;工业数据也是形成新质生产力的优质生产要素&#xff0c;助力企业实现高效精益生产。 工业数据在制造业中的作用不可忽视&#xff…

常见地图坐标系间的转换算法JavaScript实现

文章目录 🍉 不同的地图厂商使用不同的坐标系来表示地理位置。以下简述:🍉 前置常量和方法:🍉 BD-09转GCJ-02(百度转谷歌、高德)🍉 GCJ-02转BD-09(谷歌、高德转百度)🍉 WGS84转GCJ-02(WGS84转谷歌、高德)🍉 GCJ-02转WGS84(谷歌、高德转WGS84)🍉 BD-09转wgs84坐…

Linux: 默认进程介绍

进程名称介绍systemdSystemd 可以管理所有系统资源。不同的资源统称为 Unit&#xff08;单位&#xff09;。 Unit 一共分成12种。 systemctl list-units命令可以查看当前系统的所有 Unitkthreaddkthreadd进程由idle通过kernel_thread创建&#xff0c;并始终运行在内核空间, 负责…

H5利用微信开放标签唤起用户手机APP

APP壳子分享网页到微信&#xff0c;被分享人在微信打开网页后&#xff0c;利用公众号配置微信开放标签[wx-open-launch-app]&#xff0c;实现唤起APP 一、Vue2.x&#xff08;2.6.11&#xff09; 1. main.js // main.jsimport Vue from vue;Vue.config.ignoredElements [wx-o…

Hbase基础操作Demo(Java版)

一、前置条件 HBase服务&#xff1a;【快捷部署】023_HBase&#xff08;2.3.6&#xff09;开发环境&#xff1a;Java&#xff08;1.8&#xff09;、Maven&#xff08;3&#xff09;、IDE&#xff08;Idea 或 Eclipse&#xff09; 二、相关代码 代码结构如上图中①和② pom.x…

IO—消息队列+管道

使用消息队列实现的2个终端之间的互相聊天 并使用信号控制消息队列的读取方式: 当键盘按ctrlc的时候&#xff0c;切换消息读取方式&#xff0c;一般情况为读取指定编号的消息&#xff0c;按ctr1c之后&#xff0c;指定的编号不读取&#xff0c;读取其他所有编号的消息 wftok.c …

vue项目中使用websocke即时通讯实现系统公告实时获取并提醒

一、使用场景 发布者设置需要发布的公告内容、公告接收用户和发布时间&#xff0c;到达发布时间时及时通知提醒已登录系统用户&#xff0c;使用websocke来实现前端与服务器保持长连接&#xff0c;以便实时过去公告信息。 WebSocket是一种在单个TCP连接上进行全双工通信的协议…

调用Mertc的接口

概述 metaRTC5.0版本 API进行了重构&#xff0c;本篇文章将介绍webrtc传输调用流程和例子。 metaRTC5.0版本提供了C和纯C两种接口。 ICE设置 iceCandidateType参数可以在配置文件yang_config.ini中配置&#xff0c;也可以在程序中赋值。 iceCandidateType0 //0:host 1:stun 2…

2024最新大厂C++面试真题合集,大厂面试百日冲刺 bay9

腾讯实习 指针常量和常量指针 常量指针&#xff08;const Type* ptr&#xff09;&#xff1a;指针指向的内容不能被改变&#xff0c;但指针本身可以改变指向。 指针常量&#xff08;Type* const ptr&#xff09;&#xff1a;指针自身的值即内存地址不能改变&#xff0c;但指向…