【C++】-多态的底层原理

news2024/11/18 13:58:11

> 提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、虚函数表
  • 二、多态的原理
  • 三、解决疑惑
  • 四、多继承中的虚函数表
  • 五、总结


前言

今天我们开始讲解多态的底层原理,相信这篇博客会让你对多态的理解会更加的透彻,话不多说我们开始进入讲解


一、虚函数表

我们在多态语法的时候一直强调要构成虚函数,我们来看看虚函数在内存是怎么存储的?

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	 virtual void Func1()
	 {
	 	cout << "Func1()" << endl;
	 }
private:
 int _b = 1;
};

在这里插入图片描述
我们发现我们对象里面存放两个内容:成员变量和一个指针地址,刚好大小就是8。====

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析


我们往这这个Base里面增加先的内容:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
			cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,成员包括自身的变量和虚表里面的虚函数。还有一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
    在这里插入图片描述

但是最好每一次都重新生成一下解决方案,不然你在是之前的基础上修改在调试的可能就看不到效果

  1. 有虚函数的类创建不同对象,共有的是同一张虚表
    在这里插入图片描述

  2. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

在这里插入图片描述

我们猜想这可能是vs监视窗口的一个bug,一会验证内存当中多出来的地址是不是我们特有的虚函数。

二、多态的原理

为什么需要的是基类的指针和引用调用虚函数

(1)为什么是基类的??
我们在继承的第二节讲到,继承的赋值,子类对象可以赋值给父类的对象指针和引用,父类可以接收自身的,也可以接收子类的,而子类只能接收自己的,如果是强转,可能会造成一系列问题,所以多态规定只能是基类的
(2)为什么是指针和引用去调用?
在这里插入图片描述>是将地址赋值给父类指针变量,引用底层也就是指针道理是一样的,指向什么对象就去调用什么哪个对象的函数了,这样就实现同一行为,展现出不同的形态
3)为什么虚函数要进行重写
如果不重写,就达不到覆盖的效果,那么子类的虚表还是存的是父类里面的虚函数,虽然你指向子类的虚表,但是虚表里面指向的函数地址还是父类的虚函数,重写了就完成了覆盖,重写就相当于对父类虚函数的重新定义放在了子类的虚表里面了。

那么对象为什么不行??

在这里插入图片描述原因是对象赋值时要调用拷贝构造或者赋值运算符的,子类会进行切片将父类的那一部分拷贝给父类对象,此时子类的虚表指针如果也拷贝过去了,会影响父类对象里面的虚表指针,那样就乱套了,指针和引用并不会改变父类对象里面的变量和虚表指针的。所以就不允许这样的赋值,就算拷贝过来,也是属于父类里面本身的属性拷贝过来,就把d1里面的_b给拷贝过去了,虚表还是父类本身的虚表,那么调用的时候就还是父类的函数,就调不到子类的。

三、解决疑惑

1. 虚函数存在哪的?虚表存在哪的?

答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

证明一下:
我们猜想有四个位置:栈,堆,数据段(静态区),代码段(常量区)
我们通过代码来演示:

在这里插入图片描述
通过测试我们发现虚表的地址离常量区最近,也就是代码段,有的书上说是在静态区,但是自己测试之后才知道应该离常量区最近。

2. 为什么监视窗口没有特有的虚函数,内存当中多出来的地址是不是我们猜想的结果:

在上面第二节的第六小点,我们发现,子类特有的属性居然不在虚表里面,二内存中却多出来了一个地址,我们猜测是那个特有的虚函数,但是也不能确定,所以我们只能想办法验证:
在这里插入图片描述
我们需要写一个函数,将函数数组指针里面的地址取出来,然后再调用

在这里插入图片描述
我们确实把地址取出来,接下来直接通过地址来调用:
在这里插入图片描述

确实和我们猜想的是一样的

测试代码:

class A
{
public:
    virtual void fun1() { cout << "A::fun1" << endl; }
};
class B :public A
{
public:
    virtual void fun1() { cout << "B::fun1" << endl; }
    virtual void fun2(){ cout << "B::fun2" << endl; }
};

typedef void(*Fun_c)();

void Adderss(Fun_c arr[])
{
    for (int i = 0; arr[i] != nullptr; i++)
    {
        printf("[第%d个地址]:%p\n", i + 1, arr[i]);
        arr[i]();
    }
}
int main()
{
    B b;
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
    Adderss((Fun_c*)(*(int*)&b));
    return 0;
}

四、多继承中的虚函数表

刚才我们说的都是单继承中的,把单继承的原理搞懂,多继承的处理方法其实思路是一样的,有虚表,但是因为是多继承,继承下来的不是一个类的成员,所以再处理方面还是有所不同的,接下来我来给大家介绍,我们的多继承的虚表是什么样的
我们来看测试代码:

class A
{
public:
    virtual void funa1() { cout << "A::funa1" << endl; }
    virtual void funa2() { cout << "A::funa1" << endl; }//基类特有的虚函数
};
class B
{
public:
    virtual void funb1(){ cout << "B::funb1" << endl; }
    virtual void funb2() { cout << "B::funb1" << endl; }//基类特有的虚函数
};
class C :public A,public B
{
public:
    virtual void funa1() { cout << "C::funa1" << endl; }//重写A类的虚函数
    virtual void funb1(){ cout << "C::funb1" << endl; }//重写B类的虚函数
    virtual void func1() { cout << "C::func1" << endl; }//派生类特有的虚函数
            void func2() { cout << "C::func2" << endl; }//不是虚函数
};
int main()
{
    C c;
    return 0;
}

在这里插入图片描述
通过上面图的结果来看,我们多继承的派生类中有两张虚表,而且猜想派生类特有的虚函数是放在第一张表中,此时我们还是按照上面方法去验证:
在这里插入图片描述

果然和我们猜想是一样的

我们再来看看下面的案例:

class A
{
public:
    virtual void fun1() { cout << "A::fun1" << endl; }
    virtual void fun2() { cout << "A::fun2" << endl; }
};
class B
{
public:
    virtual void fun1(){ cout << "B::fun1" << endl; }
    virtual void fun2() { cout << "B::fun2" << endl; }

};
class C :public A,public B
{
public:
    virtual void fun1() { cout << "C::fun1" << endl; }
    virtual void fun3(){ cout << "C::fun3" << endl; }//特有的虚函数
};


int main()
{
    C c;
    //因为C类的一个fun1是重写了A和B类的虚函数,所以指向谁就调用谁
    A* a = &c;
    a->fun1();
    B* b = &c;
    b->fun1();
    return 0;
}

在这里插入图片描述
通过这个案例我们又可以猜想,是不是地址实际就一个,其中以恶搞是直接找到的,另一个做了一下修改最后也能找到,因为实际想想同一分代码用两个地址存,显然有点浪费空间了,所以编译器也不允许这样的事情发生,带着这个疑问,我们通过汇编来看看是什么样的:
在这里插入图片描述

因为fun1的真正地址只有一份,有三种调用fun1的方式,其中两种就是上面画图演示的多态调用,还有一种是c对象自己去调用,而多态调用是去虚表中找,自己就直接调用,最终都需要指向c对象才能去调用,a和c对象的指向刚好重叠了,所以也类似于直接调用,而b对象需要修正,才能去调用。

上面我们说的都不是菱形继承的多继承,前面也说过,尽量不要设计出菱形继承,所以我们去研究也没有什么意义,所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。

五、总结

今天讲解的知识还是以往大家下来自己去测试一样,尽量在测试前清理一下解决方案,不然会有影响。相信大家知道了底层原理之后,对于多态的时候应该不在陌生了,就是由于这一系列的底层要求,多态的形成条件才有那么多,也明白了为什么要哪些条件了,这篇博主花了很长时间,帮助自己梳理了一遍知识,也把知识分享给大家啊,希望大家多多支持
请添加图片描述

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

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

相关文章

电机故障诊断(python程序)

代码运行环境要求&#xff1a;TensorFlow版本>2.4.0&#xff0c;python版本>3.6.0 运行效果视频&#xff1a;电机故障诊断&#xff08;python代码&#xff09;_哔哩哔哩_bilibili 1.电机常见的故障类型有以下几种&#xff1a; 轴承故障&#xff1a;轴承是电机运转时最容…

文心一言大模型测评

访问地址 文心一言服务请求地址&#xff1a;文心千帆大模型 (baidu.com) 新手可以先实名认证后再申请使用 测评 普通对话 这里先和他进行简单的问题讨论 编程相关 询问他有关代码的内容 确实可以生成代码&#xff0c;但不像chatgpt那样提供复制按钮 我们接下来让他生成…

小狐狸AI付费创作系统chatgpt管理后台-前端界面vue源码增加新的登录方式配置

前面我们已经搭建好了小狐狸AI付费创作系统chatgpt管理后台vue前端环境 现在&#xff0c;就修改一下设置地方&#xff0c;使其可以配置使用哪种登录方式 查看路由找界面 一般我们找界面源码&#xff0c;先看一下路由部分&#xff0c;就能知道在哪个界面里 /src/router/index.js…

电赛学习2 使用qtdesigner

1.下载qtdesigner与PyUIC (7条消息) 如何在Pycharm中安装QT DesignerPyUIC_pycharm qt_Trouble..的博客-CSDN博客 2.使用qtdesigner设计界面 &#xff08;1&#xff09;点击创建&#xff0c;生成的这个Form窗口就是我们最后生成的窗口 &#xff08;2&#xff09;预览设计的结…

字节跳动软件测试岗,前两面过了,第三面HR天坑竟然跟我说……

阎王易见&#xff0c;小鬼难缠。我一直相信这个世界上好人居多&#xff0c;但是也没想到自己也会在阴沟里翻船。我感觉自己被字节跳动的HR坑了。 在这里&#xff0c;我只想告诫大家&#xff0c;offer一定要拿到自己的手里才是真的&#xff0c;口头offer都是不牢靠的&#xff0…

【全面的数据科学教程——《Python数据科学项目实战》】

数据科学项目有很多灵活的部分&#xff0c;需要练习和技巧才能让所有代码、算法、数据集、格式和可视化协调工作。本书将引导你完成5个真实项目&#xff0c;包括根据新闻标题跟踪疾病暴发、分析社交网络以及在广告点击数据中寻找相关模式。 《Python数据科学项目实战》并不止于…

abp(net core)+easyui+efcore实现仓储管理系统——ABP升级7.3

第四个问题 升级过程中碰到的第四个问题&#xff1a;Value cannot be null. (Parameter unitOfWork) 在Visual Studio 2022 的解决方案资源管理器中&#xff0c;找到ABP.TPLMS.Application项目中的Modules文件夹中的ModuleAppService.cs文件&#xff0c;是这个文件中的GetAll()…

临时文档4

Redis有哪些数据类型 Redis主要有5种数据类型&#xff0c;包括String&#xff0c;List&#xff0c;Set&#xff0c;Zset&#xff0c;Hash&#xff0c;满足大部分的使用要求 Redis的应用场景 总结一 计数器 可以对 String 进行自增自减运算&#xff0c;从而实现计数器功能。…

区间预测 | MATLAB实现QRLSTM长短期记忆神经网络分位数回归多输入单输出区间预测

区间预测 | MATLAB实现QRLSTM长短期记忆神经网络分位数回归多输入单输出区间预测 目录 区间预测 | MATLAB实现QRLSTM长短期记忆神经网络分位数回归多输入单输出区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 MATLAB实现QRLSTM长短期记忆神经网络分位数回…

Rancher 安装部署

1、执行如下docker 命令 rootubuntu:~# docker run --namerancher -d --privileged --restartunless-stopped -p 30040:80 -p 30050:443 rancher/rancher:latest 这里将80端口映射成了30040端口&#xff0c;将443端口映射成了30050端口&#xff0c;所以在执行上面的名字之前&…

VSPD虚拟串口软件安装及使用

文章目录 前言一、软件安装1、Configure Virtual Serial Port Driver(VSPD)2、串口调试助手 二、使用步骤1、模拟串口2、配置串口3、收发测试①、COM8&#xff08;发&#xff09; ---> COM9&#xff08;收&#xff09;②、COM9&#xff08;发&#xff09; ---> COM8&…

DevExpress WPF Tree List组件,让数据可视化程度更高!(一)

DevExpress WPF Tree List组件是一个功能齐全、数据感知的TreeView-ListView混合体&#xff0c;可以把数据信息显示为REE、GRID或两者的组合&#xff0c;在数据绑定或非绑定模式下&#xff0c;具有完整的数据编辑支持。 DevExpress WPF 拥有120个控件和库&#xff0c;将帮助您…

XCP详解「3.3·A2L信号添加和更新」

返回 XCP详解「总目录」 目录 1、ELF/MAP文件发生变更 2、添加新信号 1、ELF/MAP文件发生变更 elf文件更新后&#xff0c;状态栏会有图标闪烁提示 ​ 选个要更新的map文件即可 ​ 2、添加新信号 在Offlin的状态下进入编辑A2L文件&#xff0c;编辑过程参照3.1 ​ AS…

华为数通HCIA-网络模型

TCP 网络通信模式 作用&#xff1a;指导网络设备的通信&#xff1b; OSI七层模型&#xff1a; 7.应用层&#xff1a;由应用层协议&#xff08;http、FTP、Telnet.&#xff09;为应用程序产生对应的数据&#xff1b; 6.表示层&#xff1a;将应用层产生的数据转换成网络设备看…

Java毕业设计—爱宠医院管理系统设计与实现【含源码、论文】

爱宠医院管理系统 获取数论文、代码、答辩PPT、安装包&#xff0c;可以查看文章底部 一、 如何安装及配置环境 要运行整个爱宠医院管理系统需要安装数据库&#xff1a;MySQL 5.5&#xff0c;开发工具&#xff1a;JDK 1.8&#xff0c;开发语开发平台&#xff1a;Eclipse&…

使用mydumper进行数据库备份还原

1.使用school数据库 create database school; use school;2.创建student&#xff0c;score表 student表 CREATE TABLE student ( id INT(10) NOT NULL UNIQUE PRIMARY KEY , name VARCHAR(20) NOT NULL , sex VARCHAR(4) , birth YEAR, department VARCHAR(20) , address VAR…

产教融合与校企合作

1. 什么是产教融合&#xff1f; 产教融合是指职业学校根据所设专业积极开班专业产业&#xff0c;把产业与教学密切结合&#xff0c;相互支持&#xff0c;相互促进&#xff0c;把学校办成集人才培养、科学研究、科技服务为一体的产业性经营实体&#xff0c;形成学校与企业浑然一…

SpringBoot登陆+6套前端主页-【JSB项目实战】

SpringBoot系列文章目录 SpringBoot知识范围-学习步骤【JSB系列之000】 文章目录 SpringBoot系列文章目录本系列校训 SpringBoot技术很多很多环境及工具&#xff1a;上效果图主页登陆 配置文件设置导数据库项目目录如图&#xff1a;代码部分&#xff1a;控制器过滤器详细的解…

社区发现快速入门

社区发现快速入门 一、图&#xff1a;关系型数据的一种描述方式 关系型数据&#xff0c;又名“表格数据”&#xff0c;指关系数学模型中以二维表的形式来描述的数据&#xff0c;它以表格形式组织&#xff0c;使用行和列来存储和表示数据之间的关系。 关系型数据可以用实体、…

【leetcode】206.反转链表

思路1&#xff1a;取原链表的节点&#xff0c;依次头插到新链表&#xff0c;返回新链表的头 示例一反转步骤如下&#xff1a; &#x1f4d6;Note: 当cur ! NULL时&#xff0c;将cur指向的节点头插入新链表&#xff0c;首先要更改cur->next的值为newhead->next&#xff0c…