C++当中的多态(三)

news2024/11/25 0:35:05

  (六)虚表的本质

  其实我们大家应该都已经猜到了:我们虚表的本质就是一个函数指针数组。通过访问这个函数指针数组就可以得到我们想要的虚函数的地址,之后通过这个地址就可以调用我们相应的虚函数。我们这个函数指针数组是以nullptr结尾的,也就是全0。跟上图中显示的效果也完全相同。

  那么我们就可以根据这个特定编写出如下的代码进行验证我们最后一个地址到底是不是我们在派生类当中新加入的虚函数的地址了。所示代码如下:

  经过这种方式就可以打印出我们虚函数表当中所有存储的虚函数了。很明显之后的新加入的指针就是我们在派生类当中创建的虚函数。但是我们在验证的时候需要进行注意:实际上我们的验证是不符合规范的。对于我们类当中成员函数的访问都是通过this指针进行的,但是我们是通过函数的地址进行强制访问,有的编译器会产生报错。提示我们访问的规则不符合规范。在这里我们仅作为了解即可。

  需要我们注意的是:在派生类对基类进行继承的时候,我们的虚函数表也会一同被继承下来。但是这个继承所进行的操作是先创建一个函数指针数组,之后将我们基类当中虚函数表所存储的虚函数的地址全部复制到我们新创建的函数指针数组当中。

  之后经过派生类对构成重写的虚函数地址的覆盖以及新创建虚函数地址的添加操作之后就得到了一张新的虚函数表。我们需要注意的是:虚函数表当中存储的数据可能相同,但是虚函数表的地址并不相同,所以并不是共用一张表。

  对于不同的类来说,虚函数表是不相同的,虚函数表当中存储的数据可能相同。对于一个类所创建的多个不同对象来说,其公用同一张虚函数表。

  (七)虚表在内存当中存储的位置

  那么我们创建好的新的虚函数表存储在什么位置呢?我们先进行思考:存放在栈区?由于我们的很多数据都是存放在栈区的,在栈区当中存储的数据也会进行压栈,很难进行扩大容量等行为。很明显存放在栈区并不是我们想要的结果。

  存放在堆区吗?也不是没有可能,如果是存放在堆区上,那么我们创建虚表的时候进行的操作大概就是向堆区申请一块空间,之后有新的虚函数产生就重新申请一块更大的空间进行存储我们虚函数的地址。但是反复的重新申请又会增加很大的系统负担,如果一下子开辟很多空间又会造成内存空间浪费的现象,还有更好的选择吗?

  存放在静态区吗?存放在静态区的概率似乎比存放在堆区的概率还大。因为我们的成员函数就是存放在一块公共的空间便于我们进行调用的。如果存放在堆区就跟我们成员函数的性质相一致,还不会有堆区跟栈区的困扰。

  存放在常量区吗?对于我们不允许进行修改的数据我们可以将数据存储在常量区当中。思考一下:我们的虚表可以进行修改吗?似乎我们没有对虚表进行修改过,但是在派生类进行重写的时候系统自动改变了我们虚表当中的内容,这样算不算是修改了呢?虚表会存储在常量区吗?

  我们经过代码对上述的疑问进行验证:

  经过验证我们会发现,我们虚表的地址跟我们常量区存储数据的地址更加接近所以虚表的地址应该是存储在常量区上的。

  (八)静态绑定和动态绑定

  所谓的动态绑定跟静态绑定实际上就是我们多态的形式。多态的形式可以分为两种:一种是静态的多态,一种是动态的多态。

  1.静态多态

  对于我们静态的多态来说实际上就是函数的重载。静态的多态在编译的时候就已经确定好了想要调用的函数的地址。多态的形式表现为我们调用相同的函数名,通过传不同参数的形式实现不同的效果。

  2.动态多态

  对于我们动态多态来说其实就是我们本章节讲的多态的形式。动态的多态在运行的时候通过查找相应的虚函数表调用相应的函数地址,进而实现多态的作用。其主要的实现方式就是虚函数的重写。

  (九)纯虚函数和抽象类

  所谓的纯虚函数其实就是在我们设置的虚函数的后面加上=0的形式,这种类型的函数就叫做纯虚函数。含有纯虚函数的类叫做抽象类,抽象类不能实例化产生相应的对象,因此只能通过继承之后才可以使用。所以纯虚函数和抽象类其实是共同存在的,作用就是强制我们进行虚函数的重写。通常情况下我们会将一个没有实际实例的基类设置成为抽象类。所示代码如下:

  我们会发现当我们将基类设置成为抽象类之后,我们定义的全虚函数就会被编译器强制要求重写,如果不进行重写系统就会产生报错。我们尝试将上述的函数进行重写:

  我们会发现经过重写之后代码就可以正常运行了。

  实际上抽象类的作用经常被用来定义某种事务的基本参数,也就是这种事物一定要具有的属性和参数。例如:我们的汽车类就规定了我们不同类型的汽车在创建对象的时候就一定要具有自己的速度信息和容量信息等等。

  (十)多继承当中虚函数的重写

  之前我们说到的继承都是单继承的形式,对于我们的派生类来说只有一个基类,但是我们的继承不仅仅有单继承还有多继承。那么多继承当中的虚函数重写是什么样的呢?

  在单继承当中派生类继承基类之后就会创建一张自己的虚函数表,之后将基类虚函数表当中的数据复制一份,再对我们复制下来的虚函数表进行适当的覆盖和添加,之后就形成了派生类当中的虚函数表。其实再多继承当中进行的操作跟我们单继承当中所进行的操作很相似。

  当我们不对多继承当中的虚函数进行重写的时候就会发现多继承的继承模式实质上跟我们单继承的继承模式完全相同。对于A类我们会创建一张属于A类的虚函数表,对于B类我们会创建一张属于B类的虚函数表。当我们使用C类同时继承A类和B类的时候,我们这两张虚函数表也同时被继承下来。编译器会自动将两张虚函数表当中的虚函数复制一份添加到我们的派生类当中相对应的虚函数表当中。

  当我们对独属于类A或类B当中的虚函数进行重写的时候实际上就是进行两份单继承多态重写的方式。编译器会自动将继承A的虚函数表当中对应的函数地址进行覆盖得到一个完整的派生类虚函数表A,同样的对于读书与B类的虚函数进行重写的时候,也会对相应的函数地址进行覆盖得到一个完整的派生类虚函数表B。对于没有进行重写的虚函数会保持原本基类当中的函数地址不变。

  但是如果多继承基类当中存在多个相同函数名的虚函数,并且我们在派生类当中对该虚函数进行了重写操作的时候就会产生不同的作用。

  首先我们对类A和类B进行相同函数名的虚函数进行重写操作。 之后使用两个两个基类的指针尝试访问构成多态的函数。

  使用基类A的指针进行访问C类重写的函数,调用的应该是C类当中的重写之后的虚函数,使用基类B的指针进行访问C类重写的函数,调用的应该也是C类当中重写之后的虚函数。这两次调用的函数应该相同。我们运行的结果也正如我们预期中的那样产生了两次相同的结果。

  但是通过观察C类当中的虚函数表我们会有新的疑问,为什么我们虚函数表当中对应的位置函数的地址不相同呢?不是调用的是同一个虚函数吗?

  我们可以通过反汇编的形式进行分析出现上述问题的原因。

  通过反汇编的形式我们可以发现:当我们使用类A进行调用虚函数的时候,我们只需要通过一次call指令和一次jump指令就可以跳到我们重写虚函数的位置。但是对于使用类B进行调用虚函数的时候我们得需要通过两次jump函数才可以找到我们重写虚函数的地址。中间的一次jump操作我们进行了一个寄存器当中的减操作。

  根本原因其实是因为在多继承当中类存放的位置不同。

  我们可以发现在C类对象当中由于我们先继承A类所以会先在C类当中构建出一个A类,之后才会构建出我们的B类对象。这也就造成了对于我们的C类对象来说其中的初始地址跟我们C类中的A类的初始地址是相同的。

  当我们使用A类指针去接受一个C类的对象的指针的时候,我们是可以直接进行使用的。但是对于我们的B类来说就不行了,我们需要先计算出前面A类的大小,之后跳过A类的地址才可以得到我们B类的地址。

  而我们中间一次jump操作所带来的寄存器的减操作,实际上就是调整好我们传入的地址,即传入B类当中的this指针。eax是保存this指针的寄存器。我们可以进行验证一下。A类当中仅仅只存储有一个虚函数表,这个是一个指针类型的数据。所以占4个字节。减去刚好等于我们B类在C类当中的位置。如果我们对A类加上一个成员变量的话,我们寄存器的见操作数值也会相应的变化。

  我们会发现经过测试程序运行的现象跟我们预期的结果刚好相符。

  所以在多继承的多态当中对同名虚函数进行重写所对应的虚函数的地址不同,这是为了更正我们传入的对象的地址所进行的封装。当然我们也可以在底层完全隐藏,这样的话也有可能会在窗口上显示相同的虚函数的地址,这仅仅是不同编译器所带来的不同的效果而已。

  (十一)菱形虚拟继承当中的虚函数

  实际上菱形虚拟继承当中的虚函数很简单,仅仅是将两个看起来很复杂的对象进行了嵌套而已。回顾一下菱形继承的存储方式:为了解决数据冗余和二义性的问题。我们将中间的派生类设置为虚继承的形式。被设计成虚继承的类会将基类部分分离出来,由一张虚基表进行管理。而在我们中间的派生类当中仅仅保存着新加的数据和我们的虚基表的地址。在虚基表第二个指针的位置上会记录派生类和基类地址的相对偏移量。

  所以我们的重合的派生类当中会存储两张虚基表,加上一些新添加的数据。

  当我们加上多态的概念之后就变成了复合的形式。

  但是需要我们注意的是:在中间类进行重写的时候,我们的重合类也需要进行重写操作。如果不进行重写的话,那么我们在通过重合的派生类调用函数的时候就会产生歧义。因为我们有两个平等的调用对象,也就是中间类当中的B和C类。因此在中间派生类进行重写的时候,我们重合节点对应的类一定要进行重写,否则系统就会进行报错。

  当我们对重合的派生类进行重写之后程序恢复正常。

  那么我们的虚继承的虚函数表是怎样进行存储的呢?

  当我们在中间的派生类对虚函数进行重写的时候由于新创建的虚函数主体并不相同,所以会创建一个单独的虚函数表进行相应数据的记录。所以中间会生成两张虚函数表。同时我们A的虚函数表被D类所继承,如果D类对A类进行虚函数的重写操作,会直接使用新的虚函数地址覆盖我们原有的虚函数的地址。那么我们进行存储的一共有三张虚函数表。

  综上所述,对于菱形虚拟继承当中的多态来说我们一共需要创建五张表。对于这一部分的知识在实际的生活当中几乎不会被使用,所以我们仅仅作为了解即可。

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

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

相关文章

开发者的噩梦:如何在抄袭狂潮中杀出一条血路?

开发者的噩梦:如何在抄袭狂潮中杀出一条血路? 作为一个独立开发者,辛辛苦苦打磨出的产品,一旦被别人抄袭,心中往往会涌现出无数的愤怒与无奈。看到自己的创意被别人肆意剽窃,并以此获利,谁能不…

【HTML】Html标签

目录 结构盒子div 标签语义化标签 文本p 段落标签h 标题标签span 行内标签a 超链接标签br 换行标签、hr水平线标签sub 下标字、sup 上标字strong 或 b 加粗、em 或 i 斜体、del 或 s 删除线、ins 或 u 下划线 列表ul 无序列表ol 有序列表dl 自定义列表列表嵌套 表格table 标签合…

【Gateway】网关服务快速上手

微服务的接口都是直接对外暴露的,可以直接通过外部访问,为了保证对外服务的安全性服务端实现的微服务接口通常都带有一定的权限校验机制,由于使用了微服务,原本一个应用的的多个模块拆分成了多个应用,我们不得不实现多次校验逻辑,当这套逻辑需要修改时,我们需要修改多个应用,加…

【4.2】图搜索算法-DFS和BFS解单词拆分

一、题目 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分 为一个或多个在字典中出现的单词。 说明: 拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。 示例 1: 输入: s "le…

【2024-09-12】某极验4流程分析-滑块验证码

声明:该专栏涉及的所有案例均为学习使用,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!如有侵权,请私信联系本人删帖! 文章目录 一、前言二、流程分析三、参数分析三、代码一、前言 极验四代滑块没有了滑动轨迹的验证,来看一下 网址:aHR0cHM6Ly9n…

机器学习中的内存优化

随着机器学习模型的复杂性不断增加,内存使用量也随之增长,因此,内存优化变得尤为重要。 机器学习内存足迹 机器学习模型通常由数据结构如张量和矩阵组成。例如,一个形状为(1000, 1000),每个元素为32位浮点数的二维张量…

TypeScript中 any和unknown 的区别

1、给其他变量赋值 any可以给其他类型的变量重新赋值; 但unknown是不行的,unknown 可以保持类型安全,从而减少潜在的错误; 2、使用类型上的方法 any可以用类型上的方法,unknown是不可以的

golang 字符串浅析

go的字符串是只读的 测试源代码 package mainimport ("fmt""unsafe" )func swap(x, y string) (string, string) {return y, x }func print_string(obj *string, msg string) {string_ptr : (*[2]uintptr)(unsafe.Pointer(obj))first_obj_addr : string_…

AI在医学领域:医学AI的安全与隐私全面概述

随着技术的进步,软件系统在商业产品中扮演着越来越重要的角色,并在医疗领域变得不可或缺。人工智能(AI)和机器学习(ML)的发展已经彻底改变了现代医疗系统,为通过病人诊断、监测和医疗保健研究收…

[机器学习]KNN算法

1 KNN算法简介 KNN算法思想:如果一个样本在特征空间中的K个最相似的样本中的大多数属于某一个类别,则该样本也属于这个类别。 K值过小:用较小领域中的训练实例进行预测。 容易受到异常点的影响K值的减小意味着整体模型变得复杂,容…

Tableau学习日记

Day1:Tableau简介、条形图与直方图 1.Tableau绘制条形图 1.1 条形图1:各地区酒店数量 1.2 条形图2:各地区酒店均价 1.3 堆积图:价格等级堆积图 2.Tableau绘制直方图 2.1创建评分直方图 Day2:Tableau简介、条形图与直…

c# resource en-US

这里主要是做中英文语言包切换的,非常简单

Java教程:SE进阶【十万字详解】(上)

✨博客主页: https://blog.csdn.net/m0_63815035?typeblog 💗《博客内容》:.NET、Java.测试开发、Python、Android、Go、Node、Android前端小程序等相关领域知识 📢博客专栏: https://blog.csdn.net/m0_63815035/cat…

c++11新特性——endable_shared_from_this

文章目录 一.解决场景代码示例原因 二.解决办法代码 三.底层原理 一.解决场景 一个share_ptr管理的类&#xff0c;如果从类的函数里返回类对象&#xff08;this指针&#xff09;&#xff0c;导致share_ptr引用计数错误&#xff0c;析构时异常问题 代码示例 #include <mem…

最近试用了FunHPC-AI宝箱-ComfyUI-Plus,使用了dreamshaperXL全能模型,生成了几张国风图,效果真的让人惊叹!

最近试用了FunHPC-AI宝箱-ComfyUI-Plus&#xff0c;使用了dreamshaperXL全能模型&#xff0c;生成了几张国风图&#xff0c;效果真的让人惊叹&#xff01;&#x1f338; https://www.funhpc.com/#/ 通过简单的提示词&#xff0c;我就能轻松生成出充满古韵的图像&#xff1a;汉服…

linux查看外网ipv4地址

在Linux系统中&#xff0c;可以使用以下几种方法来查看外网IPv4地址&#xff0c;并确保强制使用IPv4。 前言 特别感谢浪浪云对本文的大力支持。浪浪云作为领先的云计算服务提供商&#xff0c;凭借其卓越的性能和可靠性&#xff0c;帮助无数企业和开发者实现了业务的快速部署和…

苹果宣布iOS 18正式版9月17日推送:支持27款iPhone升级

9月10日消息&#xff0c;在苹果秋季发布会结束后&#xff0c; 苹果宣布将于9月17日(下周二)推送iOS 18正式版系统。 苹果官网显示&#xff0c;iOS 18正式版将兼容第二代iPhone SE及之后的所有机型&#xff0c;加上刚发布的iPhone 16系列&#xff0c;共兼容27款iPhone。 iOS 18升…

为拖延症量身定制的AI工具,让Kimi做我的《每日信息整理助手》

AI不止对传统行业带来巨大的改变&#xff0c;对日常生活也便利了不少&#xff0c;现在这个时代获取信息的方式太简单了。 我们每天都会接受大量的信息&#xff0c;难免一天下来会忘记很多事情&#xff0c;有时候突然想起了一个点子&#xff0c;有时候突然有一件急事、一件待办事…

基于SpringBoot+Vue的校园失物招领系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的校园失物…

2024年黑龙江事业单位考试报名保姆级流程

黑龙江事业编考试报名须知 一、报名安排 1.报名时间&#xff1a;9月18日9&#xff1a;00-9月24日17&#xff1a;00 2.报名网址&#xff1a;黑龙江省事业单位公开招聘服务平台 二、报名操作流程 本次招考采取网上报名的方式。 1.网上报名。应聘人员可于2024年9月18日9&…