C++多态的底层原理

目录

1.虚函数表

(1)虚函数表指针

(2)虚函数表

2.虚函数表的继承--重写(覆盖)的原理

3.观察虚表的方法

(1)内存观察

(2)打印虚表

        虚表的地址

        函数

        传参

(3)虚表的位置

4.多态的底层过程

5.几个原理性问题

(1)虚表中函数是公用的吗?(2)为什么必须传入指针或引用而不能使用对象?

(3)为什么私有虚函数也能实现多态?

(4)VS中的虚表中存的是指令地址?

6.多继承中的虚表

7.总结

1.虚函数表

(1)虚函数表指针

首先我们在基类Base中定义一个虚函数,然后 观察Base类型对象b的大小:

class Base
{
public:virtual void Func1(){cout << "Func1" << endl;}virtual void Func2(){cout << "Func2" << endl;}void f(){cout << "f()" << endl;}
protected:int b = 1;char ch = 1;
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}

我们发现,如果按照对齐原则来计算b的大小时,得到的结果是8,而我们打印的结果是:

这说明带有虚函数的类所定义的对象中,除了成员变量之外还有其他的东西被加入进去了(成员函数默认不在对象内,在代码段)。

我们可以通过调试来观察b中的内容:

我们发现对象中多了一个——vfptr,即为虚函数表指针。简称为虚表指针。

(2)虚函数表

仍然看上图,我们发现虚函数表指针下方的两个地址,这两个地址分别对应的就是Base中两个虚函数的地址,构成了一个虚函数表。所以虚函数表本质是一个指针数组,数组中的每一个元素都是一个虚函数的地址。

VS2019封装更为严密,在底层的汇编代码中,虚函数表中的地址并不一定是虚函数的地址,可能存放的是跳转到虚函数的地址的指令的地址。这个在后面会加以演示。

因此当我们调用普通函数和虚表函数时,它们的本质是不同的:

 Base* bb = nullptr;bb->f();bb->Func1();

其中bb调用f()的过程没有发生解引用的操作,非虚函数在公共代码段中,直接对其进行调用即可。而bb调用Func1()的过程中,需要通过虚表指针类来找到Func1(),而拿到虚表指针,找到虚函数的地址,这个时候需要对这个地址进行解引用操作,而bb是空,因此程序会崩溃。

//类指针访问成员变量的时候,会解引用:因为成员变量在类中
//类指针访问成员函数的时候,不解引用:因为成员函数不在类中,具体位置在编译的时候确定(具体见C++类和对象

我们知道对象中只存储成员变量,成员函数存储在公共代码段中,其实虚函数也是一样存储在公共代码段,只不过寻找虚函数需要通过虚表来确定位置。普通函数在编译时直接就可以确定位置。

2.虚函数表的继承--重写(覆盖)的原理

还拿上一节中买票的例子举例,其中父类中有两个虚函数,子类重写了其中的一个,子类中还有字节的函数。

class Person
{
public:virtual void BuyTicket(){cout << "全价" << endl;}virtual void Func1(){cout << "Func1" << endl;}
protected:int _a;
};
class Student :public Person
{
public:virtual void BuyTicket(){cout << "半价" << endl;}virtual void Func2(){cout << "Func2" << endl;}
protected:int _b;
};int main()
{Person a;Student b;return 0;
}

我们可以通过调试来观察以下他们的虚表和虚表指针。

显然父类对象_vfptr[0]中存放的是BuyTicket的地址,_vfptr[1]中存放的是Func1()的地址。子类对象中_vfptr[0]中存放的是继承并重写的BuyTicket的地址,_vfptr[1]存放的是继承下来但没有进行重写的Func1()的地址。通过对比我们发现:对于没有进行重写的Func1()的地址,通过对比我们发现:对于没有进行重写的Func1()来说,子类中虚表中的地址和父类中的是一样的,可以说是直接拷贝下来的。而对于进行了重写的BuyTicket来说,子类中虚表的地址与父类中明显不一样,其实是在拷贝了父类的地址后又进行了覆盖的。因此重写在底层的角度来说又叫做覆盖。

同时我们又发现了一个问题,那就是子类对象的虚表中为什么没有些它自己的虚函数地址Func2()呢?其实是写了的,只不过通过VS的监视窗口并不能看到,我们可以通过内存来进行观察:

3.观察虚表的方法

(1)内存观察

我们可以通过观察内存来观察虚函数表的情况,这里观察的是父类对象,会发现在虚函数指针的地址存放的是父类中两个虚函数的地址。

我们也可以观察一下子类对象:

与父类对象中存储的相同,唯一有区别的地方就是紫色的部分,存放的其实是子类虚函数Func2()的地址。这说明Func2()也在虚表中只不过在监视窗口没有看不到而已。

(2)打印虚表

        虚表的地址

通过观察内存,对于单继承来说,我们只需要打印对象的首元素的地址即可找到虚表,并进行打印。

我们发现对象的前四个字节存储的就是虚表的地址。可以通过这一点来打印虚表。

我们关闭一下调试来重新写一下代码(关闭调试后在进行运行地址会发生变化但是规律是不变的)

typedef void(*vfptr)();
void Printvfptr(vfptr* table)
{for (int i = 0; table[i] != nullptr; i++){printf("%d:%p\n", i, table[i]);}cout << endl;
}
int main()
{Person a;Student b;Printvfptr((vfptr*)*(void**)&a);Printvfptr((vfptr*)*(void**)&b);return 0;
}

下面来解释以下如何打印的虚表,分为两部分,一部分是函数,一部分是传参:

        函数

首先我们明确,虚函数指针是一个函数指针,因此为了简便我们可以将函数指针重命名为vfptr。通过接收虚表指针,并以此打印指针数组中的内容(虚函数的地址)。

        传参

拿父类对象a举例,我们要找到a的前四个字节的内容,即虚表指针,然后再传入函数中。

首先使用(void**)对a的地址进行强制类型转换,这其中发生了切割。使用(void**)的原因在于,由于不知道是使用32位还是64位系统,但我们可以通过指针的大小来判断。首先将&a转换成一个指针,再将其转换成一个指针类型,再进行解引用就得到了a的前4或者8个字节。但同时我们需要传递的是一个vfptr类型的函数指针,所以还需要进行(*vfptr)类型的强制转换。

有了前面的解释,我们就可以理解打印虚表的原理了,我们把这一段代码运行一下:

发现分别打印出了a和b的虚函数表。

如果打印的虚函数数量不对,这是VS编译器的bug,我们可以重新生成解决方案,再重新运行代码。

(3)虚表的位置

我们还可以观察一下虚表的位置,在哪个区域:

使用其他区域的变量进行对比:

Person per;
Student std;
int* p = (int*)malloc(4);
printf("堆:%p\n", p);
int a = 0;
printf("栈:%p\n", &a);
static int b = 1;
printf("数据段:%p\n", &b);
const char* c = "aaa";
printf("常量区:%p\n", &c);
printf("虚表:%p\n", *(void**)&std);
return 0;

打印的结果是:

我们发现虚表的位置在数据段和常量区之间。大致属于数据段。

4.多态的底层过程

class Person
{
public:virtual void BuyTicket(){cout << "全价" << endl;}virtual void Func1(){cout << "Func1" << endl;}
protected:int _a;
};
class Student :public Person
{
public:virtual void BuyTicket(){cout << "半价" << endl;}virtual void Func2(){cout << "Func2" << endl;}
protected:int _b;
};
void F(Person& p)
{p.BuyTicket();
}
int main()
{Person per;Student std;F(per);F(std);return 0;
}

我们还使用这一段代码来举例,首先复习一下多态:使用父类的指针或引用去接收子类或者父类的对象,使用该指针或者引用调用虚函数,调用的是父类或子类中不同的虚函数。

下面来分析原理:

父类对象原理:

首先用父类引用p来接收父类对象per,此时p中的虚表和per中的虚表一摸一样,只需要访问_vfptr中的BuyTicket地址。此时的p不是新创建了一个父类对象,而是子类对象std切片后构成的,其中男就将重写之后的BuyTicket()的地址也随之切入了p。可以把怕堪称原std的包含_vfptr的一部分。

总结:基类的指针或者引用,指向谁就去谁的虚函数表中找到对应位置的虚函数进行调用。

5.几个原理性问题

了解了多态原理之后,就可以分析出在上一节中出现的一些现象规律。

(1)虚表中函数是公用的吗?

虚表中的函数和类中的普通函数一样是放在代码段的(虚表在数据段),只是虚函数还需要将地址存一份到虚表,方便实现多态。这也说明同一类型的不同对象的虚表指针是相同的,我们还可以通过调试观察:

	Person per;Person pper;


(2)为什么必须传入指针或引用而不能使用对象?

当我们使用父类对象去接收时,父类对象本身就具有一个虚表了,当子类对象传给父类对象的时候,其他内容会发生拷贝,但是虚表不会,C++这样处理的原因在于,如果虚表也会发生拷贝的化,那么该父类对象的虚表就存了子类对象的虚表,这是不合理的。

我们同样可以通过调试来进行观察:

void F(Person p)
{p.BuyTicket();
}
int main()
{Person per;Student std;F(std);
}

这是std中的虚表内容。

这是p中虚表内容,而且在调试过程中,程序是进入父类中进行调用函数的。

(3)为什么私有虚函数也能实现多态?

这是因为编译器调用了父类的public接口,由于是父类的引用或者指针,因此编译器 发现是public之后就不再进行检查了,只要在虚表中可以找到就能调用函数。

(4)VS中的虚表中存的是指令地址?

在VS2019中,为了封装严密,其实虚表中存入的是跳转指令,我们可以通过反汇编进行观察:

我们将虚表中的地址输入反汇编,看到的是这样的一条语句:

这是一条跳转指令,会跳转到BuyTicket()的实际地址处。

6.多继承中的虚表

谈到多继承就要谈到零星虚拟继承,这是一个庞大而复杂的问题,这里只介绍多继承中虚表的内容:

class Base1
{
public:virtual void Func1(){cout << "Func1" << endl;}virtual void Func2(){cout << "Func2" << endl;}
protected:int _a;
};
class Base2
{
public:virtual void Func3(){cout << "Func3" << endl;}virtual void Func4(){cout << "Func4" << endl;}
};
class Derive :public Base1, Base2
{
public:virtual void Func5(){cout << "Func5" << endl;}
};
int main()
{Derive a;
}

我们可以使用调试来观察a中的虚表内容:

通过调试我们可以看到a中有两个虚表指针分别存放的是Base1中的虚函数的地址和Base2中虚函数的地址,那么a中特有的类Func5()存放哪个虚表呢?这需要通过内存进行观察:

我们发现它被存放在了第一个虚表指针指向的虚表中。

我们知道打印第一个虚表指针指向虚表的方法,那么第二个虚表指针的该怎么处理呢:

Printvfptr((vfptr*)*(void**)((char*)&a+sizeof(Base1));

注意需要先将&a转换成char*类型,这样对其加一,才代表加一个字节。

7.总结

实际中我们不建议设定出菱形继承或者菱形虚拟继承,在实际中很少使用。

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

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

相关文章

SpringBoot添加密码安全配置以及Jwt配置

Maven仓库&#xff08;依赖查找&#xff09; 1、SpringBoot安全访问配置 首先添加依赖 spring-boot-starter-security 然后之后每次启动项目之后&#xff0c;访问任何的请求都会要求输入密码才能请求。&#xff08;如下&#xff09; 在没有配置的情况下&#xff0c;默认用户…

【python】python基于 Q-learning 算法的迷宫游戏(源码+论文)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

ctfshow-web入门-php特性(web137-web141)

目录 1、web137 2、web138 3、web139 4、web140 5、web141 1、web137 直接调用 ctfshow 这个类下的 getFlag 函数&#xff0c;payload&#xff1a; ctfshowctfshow::getFlag 查看源码&#xff1a; 拿到 flag&#xff1a;ctfshow{dd387d95-6fbe-4703-8ec5-9c8f9baf2bb5} 在…

【Linux】远程连接Linux虚拟机(MobaXterm)

【Linux】远程连接Linux虚拟机&#xff08;MobaXterm&#xff09; 零、原因 有时候我们在虚拟机中操作Linux不太方便&#xff0c;比如不能复制粘贴&#xff0c;不能传文件等等&#xff0c;我们在主机上使用远程连接软件远程连接Linux虚拟机后可以解决上面的问题。 壹、软件下…

MySQL_JDBC

目录 一、JDBC常用的接口和类 1.1 数据库连接 Connection 1.2 Statement 对象 二、JDBC的使用 总结 【Java 的数据库编程】 JDBC 即 Java Database Connectivity (Java数据库连接)&#xff0c;是一种用于执行 SQL 语句的 Java API。这个 API 由 java.sql.*,javax.sql.* …

软件测试:Postman 工具的使用。开发及测试均需要掌握的测试工具

工具介绍 各个模块功能的介绍如下&#xff1a; 1、New&#xff1a;在这里创建新的请求、集合或环境&#xff1b;还可以创建更高级的文档、Mock Server 和 Monitor以及API。 2、Import&#xff1a;这用于导入集合或环境。有一些选项&#xff0c;例如从文件&#xff0c;文件夹导…

Linux环境下(DeepinV20+)使用docker安装和使用mysql、redis、minio等各类中间件(后续用到其他中间件会继续更新)

docker安装&#xff1a;https://blog.csdn.net/HXBest/article/details/140702265 本人环境放置路径为&#xff1a;/env/中间件名称/&#xff0c;实际改为你自己的&#xff01;&#xff01;&#xff01; 一、mysql安装和使用 docker run -itd --name mysql -p 3306:3306 \ -d …

用 apifox cli 命令行运行本地接口出现TypeError:Invalid IP address: undefined

用 apifox cli 命令行运行本地接口出现TypeError:Invalid IP address: undefined&#xff0c;客户端运行是通过的但命令行运行会报错 修改端口也是一样报错&#xff0c;地址修改为127.0.0.1会报错connect ECONNREFUSED 127.0.0.1:8080 解决方法&#xff1a;不用localhost&…

PHP家政系统自营+多商户独立端口系统源码小程序

家政行业的新篇章 引言&#xff1a;家政行业的数字化转型 近年来&#xff0c;随着科技的飞速发展和人们生活节奏的加快&#xff0c;家政服务行业也迎来了数字化转型的浪潮。为了提升服务效率、优化用户体验&#xff0c;越来越多的家政公司开始探索“家政系统自营多商户小程序…

Ubuntu24.04安装

1. 系统安装 1.1 引导界面 开机进入grub引导界面后&#xff0c;会有安装服务和测试内存两个选择&#xff0c;选择第一个进行安装。 1.2 语言选择 这里的语言选择的是安装过程中的语言&#xff0c;根据个人偏好选择即可&#xff0c;不过没有中文&#xff0c;所以默认使用英文…

【C++】选择结构- 嵌套if语句

嵌套if语句的语法格式&#xff1a; if(条件1) { if(条件1满足后判断是否满足此条件) {条件2满足后执行的操作} else {条件2不满足执行的操作} } 下面是一个实例 #include<iostream> using namespace std;int main4() {/*提示用户输入一个高考分数&#xff0c;根据分…

计算机实验室排课查询小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;实验室信息管理&#xff0c;实验室预约管理&#xff0c;取消预约管理&#xff0c;实验课程管理&#xff0c;实验报告管理&#xff0c;报修信息管理&#xff0…

qt总结--翻金币案例

完成了一个小项目的在qt5.15.2环境下的运行,并使用NSIS editNSIS打包完成.有待改进之处:增加计时功能,随机且能通关功能,过关后选择下一关功能.打包后仅仅有安装包有图标 安装后应用图标并未改变 在qt .pro中有待改进对qt的基本操作和帮助文档有了基本的认识.对C制作小游戏有了…

在jeesite开源平台上写了一个SQL命令中心的功能

实现目的: 这个SQL命令中心,是因为老项目就有这个页面,主要的功能是根据写出的SQL语句查询数据,并且在查出的数据基础上直接修改更新,还有新增和删除的功能,这么一说跟plsql就一样一样的了;这页面本来是给运维的同事来用,而且他们还会用plsql和Navicat等SQL语言操作工…

VS2019编译和使用gtest测试(C++)

目录 一、首先下载gtest开源 二、使用gtest 一、首先下载gtest开源 https://pan.baidu.com/s/15m62KAJ29vNe1mrmAcmehA 提取码&#xff1a;vfxz 下载下来解压到文件夹&#xff0c;再在文件夹里面新建一个build文件夹&#xff0c;如下&#xff1a; 再安装cmake&#xff0c;…

WEB集群-Tomact集群

linux云计算中小企业规模集群架构设计图----总结 在写今天内容前&#xff0c;小编绘制一个图&#xff1a;我设计了linux云计算中小企业规模集群架构设计图&#xff08;也可根据业务需求&#xff0c;增加业务变成大型企业架构设计图&#xff09; 知识补充–故障案例-https no s…

C#高级:枚举(Enum)从索引、值到注释的完整使用技巧

目录 一、推荐的枚举写法 二、获取注释的封装代码 三、已知【枚举】&#xff0c;获取注释、索引 四、已知【索引】&#xff0c;获取枚举值、注释 五、已知【注释】&#xff0c;获取枚举值、索引 六、创建一个【枚举字典】&#xff0c;key索引&#xff0c;value(枚举值&am…

Java 8 中 20 个高频面试题及答案

文章目录 前言20 道高频题问题 1&#xff1a;给定一个整数列表&#xff0c;使用 Stream 函数找出列表中所有的偶数&#xff1f;问题 2&#xff1a;给定一个整数列表&#xff0c;使用 Stream 函数找出所有以 1 开头的数字&#xff1f;问题 3&#xff1a;如何使用 Stream 函数在给…

光伏电站气象站:现代光伏系统的重要组成部分

光伏电站气象站&#xff0c;作为现代光伏系统的重要组成部分&#xff0c;集成了气象学、电子信息技术、数据处理与分析等多学科技术于一体&#xff0c;能够实时监测并记录包括温度、湿度、风速、风向、太阳辐射强度、降雨量在内的多种气象参数。这些数据不仅是评估光伏板发电效…