C++多态(下)

news2024/9/23 23:21:28

大家好!上一篇文章,主要是说了多态的概念和使用。这篇文章就会说一下多态的底层原理,如果对多态的使用和概念不清的可以看一下上篇文章(多态概念)
在这里插入图片描述

文章目录

  • 1. 多态的原理
    • 1.1 虚函数表
    • 1.2 多态的原理
    • 1.3 动态绑定与静态绑定
  • 2. 多继承关系的虚函数表
  • 3. 一些其余问题

1. 多态的原理

1.1 虚函数表

首先,我们先看下面的例子:
在这里插入图片描述
可能有的同学会很疑惑,成员函数不是不在类里面吗?为什么这里不是4个字节,而是8个字节。我们调试来看一下:
在这里插入图片描述
除了_a成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),其实是一个指针数组,里面存的是虚函数指针。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
那么派生类中这个表放了些什么呢?我们接着往下分析
在这里插入图片描述
从上图,我们可以看出:
1. 派生类对象中也有一个虚表指针,派生类对象由两部分构成,一部分是父类继承下来的成员,一部分是自己的成员
在这里插入图片描述
子类和父类间Func2函数的地址是一样的,但是Func1函数的地址是不一样的,原因是子类把Func1虚函数重写了。
2. 基类对象和派生类对象虚表是不一样的,Func1完成了重写,所以子类的虚表中存的是重写的B::Func1,所以虚函数的重写也叫作覆盖。覆盖就是指虚表中虚函数的覆盖

重写是语法的叫法(派生类对继承基类虚函数实现进行了重写),覆盖是原理层的叫法(子类的虚表,拷贝父类的虚表进行修改,覆盖那个虚函数)

3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

派生类自己新增加的虚函数放在哪里呢
在这里插入图片描述
我们调试来看一下:
在这里插入图片描述
你会发现找不到虚函数Func3,我们可以看一下内存窗口:
在这里插入图片描述
我们可以看到第三个是有一个地址的,但是我们不确定它是不是Func3。我们可以这样去确定:取内存值,打印并调用,确认是否是func3
那么我们该如何找到这个虚表呢
首先,为了找到这个虚表,我们必须先找到vfptr。而这个vfptr是一个指向函数数组的指针。那么现在就有一个问题:如何去取出这个地址呢
因为这个vfptr地址在vs编译器下是在类里面首位,而且地址是4个字节,我们可以先这样:

B b;
(int*)&b)

这样的话我们就能取出4个字节,但这里我们不能强转成int,因为B类型和int类型没有任何关系。但地址间它们都是一串数字,是可以相互强转的。
然后我们在解引用:

B b;
*((int*)&b)

这样就能取出4个字节了。但是它解引用是一个int,我们需要再次强转成函数数组指针类型。
当我们取到这个vfptr(函数指针数组),我们就可以打印这个虚表了。由于在VS下数组最后面放了一个nullptr,我们可以这样:
在这里插入图片描述
需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

然后,有了函数地址,我们再去调用一下:
在这里插入图片描述
然后,我们来看一下结果:
在这里插入图片描述
所以,在VS编译器监视窗口下,它是优化过的现象,有点不准确。因为Func3没有形成多态,它就没有显示出来。但通过验证,虚函数一定是放在虚表里的。

可能有的同学会问:虚函数存在哪的?虚表存在哪的
虚函数存在虚表,虚表存在对象中。注意上面的回答的错的
注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。而虚表是存在代码段的
我们可以来验证一下:
在这里插入图片描述
这里的虚函数地址,首先函数名就是函数地址,我们需要指定类域,然后要加上这个&,这是规定。从上图可以看出,虚表和虚函数比较接近代码段。

1.2 多态的原理

那么多态又是如何实现的呢
在这里插入图片描述
我们来看一下这个运行结果:
在这里插入图片描述
虚函数的调用产生了多态的效果,而普通函数的调用只调用基类的函数。这是为什么呢?
原因是:
多态调用:运行时决议——运行时,去指向的对象虚表中确定调用函数的地址。
普通调用:编译时决议——编译时,确定调用函数的地址

我们可以看一下反汇编:
在这里插入图片描述
看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

那么引用行不行呢?
在这里插入图片描述
引用也可以实现多态。

那么对象赋值也可以切片,为什么不能形成多态呢
在这里插入图片描述
原因是:对象切片的时候,子类只会拷贝成员给父类对象,不会拷贝虚表指针
如果拷贝的话,会怎么样呢?
在这里插入图片描述
那么子类和父类都会指向子类的虚表。如果我们在遇到这样的代码:
在这里插入图片描述
此时,ptr去找Func1,它就不知道找的是父类的虚函数,还是找子类的虚函数了。就会发生混乱了。所以,它不允许对象赋值实现多态。

1.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。比如:函数重载,模板。
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

2. 多继承关系的虚函数表

上面我们已经说过单继承关系的虚函数表,下面我们就说一下多继承的虚函数表吧。
看下面的例子:
在这里插入图片描述
因为是多继承,那么肯定会继承Base1一个,Base2一个。然后我们看一下调试结果:
在这里插入图片描述
我们可以看到,是存在两个Base,每个Base都存在一个虚表。但现在有一个问题:子类的Func3存在那个虚表里呢
我们现在需要把两个虚表里的内容都打印出来:那么Base1我们已经会打印了,Base2我们该如何打印呢?
在这里插入图片描述
这里我们不能直接加8个字节,因为可能会有内存对齐啥的。我们可以直接加一个sizeof(Base1)。
在这里插入图片描述
但是这样还不行,因为&d加1是加上整个d,我们需要强转成char*类型。
在这里插入图片描述
然后我们在看一下运行结果:
在这里插入图片描述
可以看到继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
在这里插入图片描述
但是这里存在一个问题:
在这里插入图片描述
Func1函数的地址不一样。原因是可能不是真正的函数,它是被封装过的。
在这里插入图片描述
这里我们用printf,因为cout不能识别函数指针,所以无法配对。但是,我们发现这里的函数地址和两个都不一样。这是为什么呢?
在这里插入图片描述
原因是:在VS下,它进行了一些封装,我们看到的虚表里的地址,并不是真正的函数地址,而是另外一些指令的地址,而通过这些调用指令去完成这个函数的内容,可能都不会直接去调用函数地址去使用。至于到底为什么这样做,很复杂,感兴趣可以研究。

3. 一些其余问题

1. inline函数可以是虚函数吗
可以,不过在多态调用时,编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
注意:一定是在多态调用时,才会忽略内联函数,其它情况下调用都还是内联

2. 静态成员可以是虚函数吗
不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数,所以静态成员函数无法放进虚函数表

3. 构造函数可以是虚函数吗
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
注意:虚表是在编译的时候就出现了,但是虚函数表指针还是一个随机值。而虚函数的意义是多态,多态调用时到虚函数表里找,但构造函数之前还没初始化,如何去找呢?

4. 析构函数可以是虚函数吗
可以,并且最好把基类的析构函数定义成虚函数

5. 对象访问普通函数快还是虚函数更快
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找

6. 虚函数表是在什么阶段生成的,存在哪的
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的

7. C++菱形继承的问题?虚继承的原理
注意这里不要把虚函数表和虚基表搞混了。虚函数表存的是虚函数地址是为了实现多态,虚基表存的是偏移量是为了解决数据冗余和二义性

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

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

相关文章

第四章 MergeTree原理分析

一、存储结构 1.1 表引擎语法结构 CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster] (name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],...INDEX index_name1 expr1 TYPE type1(...) GRANULARITY va…

【遇见青山】项目难点:解决超卖问题

【遇见青山】项目难点:解决超卖问题1.乐观锁方案2.悲观锁方案1.乐观锁方案 原始实现下单功能的方法: /*** 秒杀实现** param voucherId 秒杀券的ID* return Result*/ Override Transactional public Result seckillVoucher(Long voucherId) {// 查询优…

备战蓝桥杯【高精度加法和高精度减法】

🌹作者:云小逸 📝个人主页:云小逸的主页 📝Github:云小逸的Github 🤟motto:要敢于一个人默默的面对自己,强大自己才是核心。不要等到什么都没有了,才下定决心去做。种一颗树,最好的时间是十年前…

IDEA中使用自定义的maven

步骤 1.下载maven maven下载 2.配置maven 2.1设置环境变量 1.在“环境环境“–“系统环境“–“path”中加入(设置到bin目录下) 2.测试环境变量是否成功 C:\Users>mvn -v //在控制台输入mav -v,看是否输出以下结果 Apache Maven 3.9.0 (9b…

《MySQL系列-InnoDB引擎23》文件-InnoDB存储引擎文件-重做日志文件

InnoDB存储引擎文件 之前介绍的文件都是MySQL数据库本身的文件,和存储引擎无关。除了这些文件外,每个表存储引擎都有其自己独有的文件。本节将具体介绍与InnoDB存储引擎密切相关的文件,这些文件包括重做日志文件、表空间文件。 重做日志文件…

Docker的资源控制管理

目录 一、CPU控制 1、设置CPU使用率上限 2、设置CPU资源占用比(设置多个容器时才有效) 3、设置容器绑定指定的CPU 二、对内存使用进行限制 1、创建指定物理内存的容器 2、创建指定物理内存和swap的容器 3、 对磁盘IO配额控制(blkio&a…

使用Docker容器部署java运行环境(java8 + mysql5.7 + redis5.0 + nginx1.14.1

环境:阿里云ECS服务器一.Docker环境安装1.1 安装工具sudo yum install -y yum-utils device-mapper-persistent-data lvm21.2 为yum源添加docker仓库位置yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo1.3 将软件…

【微信小游戏开发笔记】第二节:Cocos开发界面常用功能简介

Cocos开发界面常用功能简介 本章只介绍微信小游戏开发时常用的功能,其他功能不常用,写多了记不住(其实是懒 -_-!): 层级管理器,用于操作各个节点。资源管理器,用于操作各种文件资源。场景编辑…

SpringMVC--简介和入门案例

SpringMVC简介 什么是MVC MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分 M:Model,模型层,指工程中的JavaBean,作用是处理数据 JavaBean分为两类: 一类称为实体类Bean:专门存储业务数据的,如 Studen…

西湖论剑初赛web wp

Node Magical Login 简单的js代码审计。 Flag分成了两部分。 第一部分: 这里就简单的判断了一下user是否等于admin,直接绕过。 第二部分: checkcode ! “aGr5AtSp55dRacer”,让其为真,利用数组绕过。 Flag为&#x…

家政服务小程序实战教程03-创建自定义应用

我们上一篇讲解了创建模型应用,模型应用是给管理员使用的。普通用户日常办理业务还是在小程序完成。 微搭中的小程序需要通过创建自定义应用来创建,进入控制台,点击应用,点击新建应用,选择新建自定义应用 输入应用的名…

微信小程序 java家校通Springboot中小学家校联系电子作业系统

小程序前端框架:uniapp 小程序运行软件:微信开发者 后端技术:javaSsm(SpringSpringMVCMyBatis)vue.js 后端开发环境:idea/eclipse 数据库:mysql 通过对各种资料的收集,了解到“校讯通”是联系社会的窗口,是实现家校联系工作和学校…

【参加CUDA线上训练营】零基础cuda—矩阵转置实现及其优化

【参加CUDA线上训练营】零基础cuda—矩阵转置实现及其优化1.不使用Shared Memory2.使用Shared Memory3.使用Shared Memory,并加入No Bank Conflicts4.效果对比参考文献本文参考Nvidia官方blog[An Efficient Matrix Transpose in CUDA C/C及其对应的github代码transp…

可视化图表的思路

数据表达 excel — 小量级一次性的数据处理 Tableau等BI — 批量的数据读取与分析 python — 复杂的数据清洗、爬虫和算法建模 图表展示原则:客观,高效,直观 表达格式:观点数据补充信息图表 图表选择思路 规模、趋势、占比、关…

RabbitMQ-延迟队列

一、介绍延迟队列,队列内部是有序的,最重要的特性就体现在他的延迟属性上,延时队列中的元素是希望在指定时间到了或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。 二、sprin…

TCP的协议格式 --- 20字节固定长度 + 40字节可选数据

目录 一、 20字节的固定长度 16位源端口和16位目的端口号,32位序号,32位确认属序号,4位首部长度(需要乘4) 保留(6位) 16位窗口大小 16位的校验和16位的紧急指针 二、40字节可选数据 1.2.1、…

软件设计师教程(六)计算机系统知识-操作系统知识

软件设计师教程 软件设计师教程(一)计算机系统知识-计算机系统基础知识 软件设计师教程(二)计算机系统知识-计算机体系结构 软件设计师教程(三)计算机系统知识-计算机体系结构 软件设计师教程(…

最新中文版FL Studio21水果软件下载安装图文教程

FL Studio是目前流行广泛使用人数最多音乐编曲制作软件,这款软件相信广大网友并不陌生,今天带来的是FL中文版本,所有的功能都能在线编辑,用户直接就能操作,同时因为是21水果是最新版,所以增加了新的功能&am…

【Spring Cloud总结】1、服务提供者与服务消费者快速上手

目录 文件结构 代码 1、api 1.1实体类(Dept ) 1.2数据库 2、provider 2.1 DeptController 2.2 DeptDao 2.3 DeptService 2.4 DeptServiceImpl 2.5 application.yml 3、consumer 3.1 ConfigBean 3.2 DeptConsumerController 测试 1.启动…

创建阿里云物联网平台

创建阿里云物联网平台 对云平台设备创建过程做记录,懒得再看视频 文章参考视频:https://www.bilibili.com/video/BV1jP4y1E7TJ?p26&vd_source50694678ae937a743c59db6b5ff46c31 阿里云:https://www.aliyun.com 1.物联网平…