C++中多态的原理

文章目录

  • 前言
  • 多态的原理
  • 多态的条件要求
  • 虚函数表
  • 用程序打印虚表
  • 多继承的虚函数表
  • 静态多态和动态多态
  • 菱形虚拟继承

前言

上篇讲解了多态的原理,这篇文章来详细讲解一下多态的原理。

这里有一道常考笔试题:sizeof(Base)是多少?
在这里插入图片描述

在这里插入图片描述
为什么不是8?
可以调试带大家看一下。
仔细看,对象的头部多了一个指针。
在这里插入图片描述
这个指针叫做虚函数表指针。

上面不重要,重要的是下面的东西,多态的原理。
这个指针指向的表里到底有什么东西呢?

多态的原理

看下面,这里有两个对象,一个是mike,一个是johnson,这两个对象都有表指针。

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person mike;Func(mike);Student johnson;Func(johnson);return 0;
}

我们之前讲过,构成多态跟什么有关。
跟指针或者这个引用指向的对象有关。

为什么?怎么实现的?
在这里插入图片描述
这个指针指向父类调用父类的虚函数,指向子类调用子类的虚函数。
怎么做到的呢?

大家看父类对象的虚表存的是父类的虚函数,子类对象的虚表存的是子类的虚函数。
编译器是怎么做的呢?
编译器也是判断构不构成多态,如果不构成多态,它就编译时确定调用的地址。

怎么确定呢?
看person是什么类型。那它去person里面找到这个函数,确定这个地方的地址。

如果是多态
它就会去指向的对象的虚表里去找。
编译器也很简单,就是严格的去卡这个多态的条件满不满足。

带大家来调试看一下
在这里插入图片描述
在这里插入图片描述

构成多态的情况:
p.BuyTicket();这个指令执行不知道调用的是谁。为甚么?
这个person对象有两种情况

上面这段汇编代码的本质就是,跟调用的指针对象或引用对象的类型已经无关,
看指向的对象,指向的父类调用父类的,指向子类调用子类的。

多态就是转换成汇编的问题
不构成多态直接确定地址,构成多态,转成对应的汇编指令。
这段指令干嘛?无法确定地址,不知道调用谁的,那引用指向的是父类,
它找到父类头四个字节,找到虚表的指针,找到虚表,找到虚函数,靠的就是这个虚函数。
在这里插入图片描述

指向子类就会切割或者切片,
在这里插入图片描述

单看p.BuyTicket();这个指令,它不知道指的是子类还是父类。
汇编指令一样,为什么调用结果不一样?
因为传不同的对象,不同的对象虚表是不一样的。

虚函数的另外一个名字为什么叫做覆盖?
如果在子类里面,重写虚函数以后,子类里面对应的虚表位置,会把它拷贝过来,
覆盖成我的虚函数一样。

你可以这样认为,重写是语法层的概念,覆盖是原理层的概念。

多态的条件要求

现在可以反过来思考多态的条件
1.多态的条件为什么是重写?
因为要覆盖虚表那个虚函数的位置。

2.为什么指针或者引用呢?
因为指针和引用既可以指向父类对象也可以指向子类对象。

为什么不把虚函数直接存到对象的头上呢?
因为他可能有多个虚函数,都存到对象里面不合适。
其次,同类型的虚表一样。

虚函数表:本质是一个虚函数指针数组

如果有多个虚函数
在这里插入图片描述

再来感受什么叫覆盖。
在这里插入图片描述
第一个虚函数完成了重写,可以这样认为,子类对象先把父类对象的表拷贝过来。
然后重写那个覆盖成我自己的。没有重写就不覆盖。

虚函数表其实是在编译的时候就确定好 ,没有重写是一个样子,
完成了重写是另外一个样子。

虚函数表里可能有多个地址,那具体调用哪一个呢?
看函数的声明顺序是第几个。

3.如果是父类的对象能不能实现多态?
父类的指针或者引用在这里可以切片。父类的对象也可以切片。
对象为什么不能实现多态,从原理上看?
它转换成指令就是编译的时候,如果是对象peron直接去调person的就可以了。

它也可以实现切片,为什么不往多态去实现?
如果是指针和引用与对象的区别是什么,它们的切片有点不一样。

如果是指针和引用的切片?
如果是指针是指向这个父类或者引用这个父类。
子类呢?把子类对象父类那部分切出来。然后指向或引用切出来的那部分。
子类这部分的虚表还是子类的。

如果是对象呢?
如果是个父类没什么问题,如果是子类呢?
子类给父类的切片,成员会拷贝过去,它会调用拷贝构造。
这里涉及一个问题?虚表会不会拷过去?
如果不会拷过去,父类的对象的虚表里面永远是父类的虚函数。
它不敢拷贝虚表,因为拷贝有一个很大的问题。
因为拷贝了就乱了。假设对虚表进行深拷贝,父类对象的虚表到底是子类的虚函数,
还是父类的虚函数完全分不清楚。

所以对象的切片,只拷贝成员不拷贝虚表。

感受一下,虚表没有变
在这里插入图片描述

只有虚函数的地址才会存进虚表。

再来一个问题,
虚函数存在虚表这句话对不对?
不对,虚函数跟普通函数一样,都是放在代码段的。
但是虚函数的地址会别放进虚函数表。

这里涉及到linux操作系统的知识。大家可以去了解一下。
在这里插入图片描述

大家可以看一下同类的对象是不是构成同一张虚表
在这里插入图片描述
在这里插入图片描述
父类和子类的虚表不一样,因为子类要重写要有独立的虚表。

监视窗口看到的是被修饰过的,监视窗口看到的不一定是最真实的。

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 b1;Base b2;Base b3;Derive d;b1.Func1();b1.Func3();return 0;
}

虚函数表

记住不是虚函数进入虚表,而是虚函数的地址进入虚表。
虚表的全称是虚函数表。
虚表的本质就是一个指针数组。

在这里插入图片描述

基类的虚表
在这里插入图片描述

派生类的虚表
派生类的虚表也有两个虚函数的地址
不同的是,你可以这样认为子类的虚表是把父类的虚表给拷贝过来,
拷贝过来以后做什么是呢?
重写虚函数,重写的那个位置会完成覆盖,覆盖成我重写的虚函数。
在这里插入图片描述

多态的本质就是依靠虚表来实现的。
比如有个父类的指针或者引用,可以指向父类对象也可以指向
子类对象,指向子类对象是把子类对象父类那部分给切出来。
你可以这样认为,对于这个指针而言,看到的都是父类对象。
只是一个本身就是父类对象,一个是子类对象里切出来的父类对象。

ptr->Func1();底层的汇编都是一样的,代码的本质都是转换成汇编。
它不管你是什么,它都去虚表里面找那个虚函数的地址。
所以指向父类调用父类的,指向子类调用子类的。

假设在派生类里面加了一个Func4();
在这里插入图片描述

现在Func1();完成了重写,Func4();没有完成重写。
我们现在看一下Func4();在不在虚表里呢?
在这里插入图片描述
没有看到,Func4();哪去了呢?
Func4();是虚函数,怎么没在虚表呢?

我们去看一下内存窗口。
在这里插入图片描述
Func1()和Func2()都在,那这个是Func4();吗?
怎样验证一下呢?
可以打印Func4();的地址对比一下吗?可以是可以,但是后面还有其他更复杂的情况。
现在是一个单继承,那多继承呢,还有菱形继承。

接下来,我们就会讲到一个新玩法,用程序打印虚表。

用程序打印虚表

怎么打印?
假设我已经有虚表的地址了,这个函数指针的数组的地址,
现在怎么打印。

它是一个函数指针,处理起来比较麻烦。
在这里插入图片描述
这句话是什么意思。这里typedef一个函数指针。
函数指针本身很特殊,应该是这样的。
在这里插入图片描述
但是函数指针typedef不能前面是类型后面是重命名的名字。
函数指针定义变量或者typedef都应改放到中间。
在这里插入图片描述

打印数组很简单, 但是不确定这个数组有多大,因为不同
对象的虚表是不一样的,g++下就只能写死,比如知道有三个,
就只能打印三个。但是vs系列给了一个遍历。

vs系列在存储虚表的时候,在数组的最后放了一个nullptt,
g++没有。

如果自己vs的编译器没有看到nullptr,清理一下解决方案,然后再重新生成解决方案
就可以了。
在这里插入图片描述

在这里插入图片描述

再接着往下看,我现在要把虚表的地址给取出来。
在这里插入图片描述
怎么样把虚表的地址取出来呢?
这个指针在对象的头4个字节或者头8个字节。
如何去取对象的头4个字节?

可以回顾一下学大小端的时候,想取低位的值。
假设给你一个整型,我想取这个整型的第一个字节是怎么取的。
1.定义联合体(这里再定义一个联合体加不进来了)
2.将int的地址jint强转成char再解引用。

这里我们用第二种玩法。
在这里插入图片描述
但是这里是int,函数传参传不过去。int在强转为对应的类型。
在这里插入图片描述
在这里插入图片描述

传参的时候不会直接转吗?
不会,直接转是隐式类型转换,C++只有相近类型才支持隐式类型转换。
比如int, double, char.

指针都是一个地址,但是指针的类型决定了指针接引用的时候看多大。

注意,不能用sizeof()去算数组,只要传参都会出问题。
还有这个不是我们平时用的那种数组,只有我们定义的静态的数组才能算数组的大小0。
其他地方都不行。

还有一种更直接的方式
在这里插入图片描述
这还是简化过的,如果直接把函数指针套进来,就变成天书了。

带大家理解一下。
在这里插入图片描述

在这里插入图片描述

为甚么不直接这样呢?
在这里插入图片描述
先说结论,这样是不行的。
首先你要传过去的地址在哪?在对象的头4个字节或8个字节。
必须有解引用才能把对象的头4个字节或8个字节取出来。
&b是指向对象的指针,你要传1号位置的指针还是2号位置的指针。2号。
而你现在传的是1号,2号位置的指针在对象的头4个字节上,怎么取出来?

强转成VF_PTR**, 指针解引用在32位看4个字节,在64为看8个字节。
在这里插入图片描述

这两种写法的差异是什么?
第一种写法具有一定的局限性,局限性在于它只能在32位跑,
64位下就跑不通了。
第二种写法都适应,VF_PTR**解引用是看VF_PTR*,VF_PTR*在
32位4个字节,64位8个字节。

现在已经可以打印出来虚表里虚函数的地址了,但是怎么确认就是这个呢,
再教大家一招。

在这里插入图片描述
在这里插入图片描述

有个疑问,父类没有Func4();怎么能进入虚表呢?
这个虚表已经不仅仅属于父类了,它被继承了。只是生长点是子类对象父类的一部分。
Func4();是子类的,其次这个虚表严格来说是属于子类的。

父类的虚表和子类的虚表不是同一个,子类继承了以后,子类把虚表拷贝了一份,
然后子类对其重写,自己的虚函数也会进入这个虚表。

虚表是在什么阶段生成的?
编译的时候就生成了,因为编译的时候就有这些函数的地址,就可以组成父类的虚表和子类的虚表。

对象中虚表什么时候初始化?
它是在构造函数的初始化列表的时候初始化。自己可以单独通过调试看一下。

虚表存在哪里?
首先虚表不在对象里面,对象里面的是虚表指针。
它有没有可能在栈上?
绝对不可能,因为多个对象存指向同一张虚表。栈里面只有栈帧,函数调用结束了,然后销毁了,不可能。
在这里插入图片描述
有没有可能在堆上呢?
有可能,但是不合理。堆一般是动态申请的。不可能。

在这里插入图片描述
接下来我们可以验证一下是在静态区还是常量区?
打印几个地址来对比一下就可以了。
在这里插入图片描述
对比地址的远近,虚表的地址跟常量区最接近。

其实大家可以仔细想想虚表被编译好了会不会改?
虚表在编译的过程中可能被改,尤其是子类的虚表。
运行的时候不会被改,所以放在常量区更合适。

其实看下面这个也能看出来
在这里插入图片描述
编译好的函数是一串指令,这串指令的地址就是函数的地址,函数的地址是放到代码段
常量区的。

多继承的虚函数表

在这里插入图片描述

对于Base1和Base2没什么,关键就是看多继承的Derive;
先看监视窗口。

Derive应该有两张虚表,因为它同时继承了Base1和Base2
在这里插入图片描述
两张虚表里重写了func1();func2();没动。

现在有一个问题,子类的func3();放在哪里呢?
我们这里借助虚表打印看一下。
在这里插入图片描述
现在有一个问题,第一张虚表在第一个位置,打印第二张虚表怎么大?
在这里插入图片描述

两张虚表是放在两个对象里的,无法确定它是不是连续。
因为Base1除了这张虚表还有其他成员变量。

1.跳过Base1,加上sizeof(Base1);
2.用切片,借助指针的偏移。(Base2的指针会自动偏移)
在这里插入图片描述
但是这样不对,&d是Derive*, Derive*+1跳过Derive,强转成char*,char*+1跳过一个字节。

它放到第一张表去了
在这里插入图片描述

要理解指针的偏移。
我们先看一下下面这道题。
在这里插入图片描述
这道题理解了切片就能做。
p1虽然跟p3的地址一样,但是意义不一样。
在这里插入图片描述

谁先继承谁就先声明,谁先声明谁就在前面。

在这里插入图片描述
func1会完成重写。它会重写两份,覆盖两个位置,覆盖Base1的虚表也会覆盖Base2的虚表。
但是这里面有一个非常奇怪的现象。这才是真正的大难题,十个学C++的9个都会翻车。

大家有没有发现,重写的func1的地址不一样?
首先问大家一个问题,这个函数是不是func1?是不是重写的fuc1的地址?
是,因为我们后面的字符串是去调用这个函数打印的。
在这里插入图片描述

但是这个地址为什么不一样呢?
这个问题很深很不容易理解,我们只有看汇编才能看懂。

在这里插入图片描述

这两个都会转成汇编代码,这两段汇编代码一不一样。这两个地方调用的是不是同一个函数?
这里是不是call的同一个地址?
很多人都认为是一样的,因为这里符合多态的条件。
**你可以认为,第二个地址被封装过。**因为不封装完成不了调用,因为有些条件我们理解有一些偏差。
这里有深层次的原因。

在这里插入图片描述
重新运行地址不变,不利于比较,因为这涉及进程加载的一些原因。它要进行重定位。

ptr1是正常调用。
大家看ptr2连续jmp了好几次,为甚么?
jmp就相当于封装。
右边有一段指令非常特殊
在这里插入图片描述
ecx存的是this指针,然后配着这个图大家就能看懂
在这里插入图片描述

去调用子类的这个函数的时候。
ptr1没有处理的原因是因为它恰好指向子类对想的开始。
调用子类的函数,this指针应该指向子类对象。

ptr2去调用子类的这个函数的时候,this指针不对。

这个指令的作用就是修正this指针的位置。
这里不一定减8,它减的是一个Base1的大小。

这里还涉及一个问题,如果先继承Base2,base1就要修正。

静态多态和动态多态

有些地方会分静态多态和动态多态。

那什么是静态多态呢?
函数重载。

一般语言层面,说静态都是指的编译时。

函数重载就是通过编译时实现的。

什么是动态多态呢?
对应运行时。

这两个本质都是写死了。

菱形虚拟继承

在这里插入图片描述
A有一个虚函数func,B有一个虚函数func,C有一个虚函数func,
D没有虚函数,这是不行的。
D如果不重写,会报错。它说不明确,为什么?
在这里插入图片描述

现在B重写了,C重写了,现在有一个问题,A的虚表里放谁的虚函数?
这个问题感兴趣的可以自己去了解一下,我这里就先不回答了。

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

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

相关文章

【CF闯关练习】—— 800分段

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;cf闯关练习 &#x1f48c;其他专栏&#xff1a; &#x1f534;每日一题 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓…

推荐一个vscode看着比较舒服的主题:Dark High Contrast

主题名称&#xff1a;Dark High Contrast &#xff08;意思就是&#xff0c;黑色的&#xff0c;高反差的&#xff09; 步骤&#xff1a;设置→Themes→Color Theme→Dark High Contrast 效果如下&#xff1a; 感觉这个颜色的看起来比较舒服。

腾讯云发布升级版金融音视频解决方案,提供全新架构、安全和特性

远程银行、视频尽调、全媒体客服、路演直播……近年来&#xff0c;音视频技术支撑下的非接触式金融服务&#xff0c;成为了金融机构数字化转型和探索服务创新的重要方向。 12月21日&#xff0c;腾讯云正式发布升级版金融级音视频解决方案。新方案在架构、安全和特性上进行全面…

【数字图像处理】实验二 图像变换

图像变换 一、实验内容&#xff1a; 1&#xff0e; 熟悉和掌握利用Matlab工具进行数字图像的读、写、显示等数字图像处理基本步骤。 2&#xff0e; 熟练掌握各种图像变换的基本原理及方法。 3&#xff0e; 能够从深刻理解图像变换&#xff0c;并能够思考拓展到一定的应用领域。…

Ubuntu 常用命令之 less 命令用法介绍

&#x1f4d1;Linux/Ubuntu 常用命令归类整理 less命令是一个在Unix和Unix-like系统中用于查看文件内容的命令行工具。与more命令相比&#xff0c;less命令提供了更多的功能和灵活性&#xff0c;例如向前和向后滚动查看文件&#xff0c;搜索文本&#xff0c;查看长行等。 les…

ChatGPT一周年:开源语言大模型的冲击

自2022年末发布后&#xff0c;ChatGPT给人工智能的研究和商业领域带来了巨大变革。通过有监督微调和人类反馈的强化学习&#xff0c;模型可以回答人类问题&#xff0c;并在广泛的任务范围内遵循指令。在获得这一成功之后&#xff0c;人们对LLM的兴趣不断增加&#xff0c;新的LL…

阿里云ECS配置IPv6后,如果无法访问该服务器上的网站,可检查如下配置

1、域名解析到这个IPv6地址,同一个子域名可以同时解析到IPv4和IPv6两个地址&#xff0c;这样就可以给网站配置ip4和ipv6双栈&#xff1b; 2、在安全组规则开通端口可访问&#xff0c;设定端口后注意授权对象要特殊设置“源:::/0” 3、到服务器nginx配置处&#xff0c;增加端口…

Qt之QWidget 自定义倒计时器

简述 Qt提供的带进度显示的只有一个QProgresBar,这个控件要么是加载进度从0~100%,要么是持续的两边滚动;而我想要是倒计时的效果,所以QProgresBar并不满足要求,而Qt重写控件相对于MFC来说简直是轻而易举,所以就整了两种不同的倒计时控件; 效果 代码 QPushButton的绘制部…

2023 英特尔On技术创新大会直播 | AI魅力的生活化

目录 前言正文 前言 依稀记得去年的直播大会&#xff0c;主要展现了其灵活、加速和半集成化的独特优势&#xff0c;广泛应用于人工智能、5G通信、边缘计算以及视觉图像处理等领域&#xff0c;不断提供领先的性能、能效和可编程性的创新。 如今又带来一些不一样的特色&#xf…

通过U盘:将电脑进行重装电脑

目录 一.老毛桃制作winPE镜像 1.制作准备 2.具体制作 下载老毛桃工具 插入U盘 选择制作模式 正式配置U盘 安装提醒 安装成功 具体操作 二.使用ultrasio制作U盘 1.具体思路 2.图片操作 三.硬盘安装系统 具体操作 示例图 ​编辑 一.老毛桃制作winPE镜像 1.制作准…

【Pytorch】学习记录分享6——PyTorch经典网络 ResNet与手写体识别

【Pytorch】学习记录分享5——PyTorch经典网络 ResNet 1. ResNet &#xff08;残差网络&#xff09;基础知识2. 感受野3. 手写体数字识别3. 0 数据集&#xff08;训练与测试集&#xff09;3. 1 数据加载3. 2 函数实现&#xff1a;3. 3 训练及其测试&#xff1a; 1. ResNet &…

Bash 脚本学习

文章目录 1、脚本编程基础2. 变量2.1 参数变量的引用2.2 环境变量 3 条件判断语句3.1 if 语句3.1.1 语法3.1.2 案例 3.2 case 语句3.2.1 语法3.2.2 案例 3.3 判断参数说明 4 循环语句4.1 for 循环4.1.1 语法4.1.2 案例 4.2 while循环4.2.1 语法4.2.2 案例4. 3 循环总结 5. 函数…

Prompt-to-Prompt:基于 cross-attention 控制的图像编辑技术

Hertz A, Mokady R, Tenenbaum J, et al. Prompt-to-prompt image editing with cross attention control[J]. arXiv preprint arXiv:2208.01626, 2022. Prompt-to-Prompt 是 Google 提出的一种全新的图像编辑方法&#xff0c;不同于任何传统方法需要用户指定编辑区域&#xff…

微信小程序开发系列-01创建一个最小的小程序项目

本文讲述了通过微信开发者工具&#xff0c;创建一个新的小程序项目&#xff0c;完全从零开始&#xff0c;不依赖开发者工具的模板。目的是为了更好的理解小程序工程项目的构成。 文章目录 创建一个空项目app.json全局配置pagessitemapLocation app.js 创建一个空项目 打开微信…

新型智慧视频监控系统:基于TSINGSEE青犀边缘计算AI视频识别技术的应用

边缘计算AI智能识别技术在视频监控领域的应用有很多。这项技术结合了边缘计算和人工智能技术&#xff0c;通过在摄像头或网关设备上运行AI算法&#xff0c;可以在现场实时处理和分析视频数据&#xff0c;从而实现智能识别和分析。目前来说&#xff0c;边缘计算AI视频智能技术可…

aws-waf-cdn 基于规则组的永黑解决方案

1. 新建waf 规则组 2. 为规则组添加规则 根据需求创建不同的规则 3. waf中附加规则组 &#xff08;此时规则组所有规则都会附加到waf中&#xff0c;但是不会永黑&#xff09; 此刻&#xff0c;可以选择测试下规则是否生效&#xff0c;测试前确认保护资源绑定无误 4. 创建堆…

02 - Kbuild子系统(整理中)

1. Kbuild简介 Kernel build&#xff0c;用来编译 Linux 内核基于 GNU make 设计&#xff0c;对 Makefile 进行扩充 菜单式配置&#xff1a;Kconfig预定义目标和变量&#xff1a;xx_defconfig、menuconfig、obj-y跨平台工具、递归式 Makefile Linux 模块化设计、高度可以裁剪 …

java开发面试:常见业务场景之单点登录SSO(JWT)、权限认证、上传数据的安全性的控制、项目中遇到的问题、日志采集(ELK)、快速定位系统的瓶颈

单点登录&#xff08;SSO&#xff09; 单点登录&#xff0c;Single Sign On&#xff08;简称SSO&#xff09;,只需要登录一次&#xff0c;就可以访问所有信任的应用系统。 如果是单个tomcat服务&#xff0c;session可以共享&#xff0c;如果是多个tomcat&#xff0c;那么服务s…

tcp 的限制 (TCP_WRAPPERS)

#江南的江 #每日鸡汤&#xff1a;青春是打开了就合不上的书&#xff0c;人生是踏上了就回不了头的路&#xff0c;爱情是扔出了就收不回的赌注。 #初心和目标&#xff1a;拿到高级网络工程师 TCP_WRAPPERs Tcp_wrappers 对于七层模型中是位于第四层的安全工具&#xff0c;他…

微信小程序 动态设置状态栏样式

onLoad(options) {//修改状态栏标题wx.setNavigationBarTitle({title: 页面标题, //页面标题success: () > {}, //接口调用成功的回调函数fail: () > {}, //接口调用失败的回调函数complete: () > {} //接口调用结束的回调函数&#xff08;调用成功、失败…