C++修炼之路之多态---多态的原理(虚函数表)

目录

一:多态的原理 

1.虚函数表

 2.原理分析

3.对于虚表存在哪里的探讨

4.对于是不是所有的虚函数都要存进虚函数表的探讨

二:多继承中的虚函数表

三:常见的问答题 

接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧 

接上篇的多态的介绍后,接下来介绍多态的原理以及虚函数表的相关知识

一:多态的原理 

1.虚函数表

这里从一道经典笔试题引入

对于这道题我们可能想到的是计算类 大小的对齐规则,结果为4,但结果为8,这是因为有虚函数的类要多考虑一指针

在32位系统下是8

如果这里再添加几个虚函数呢?

 

所以在这里不管类里面有多少个虚函数 ,只要是包含虚函数的类计算大小都要考虑添加一指针,再考虑对齐

但这里的一指针是什么呢?

但这里我们就看到在b1中除了_b还存有 一个_vfptr的指针在对象的前面,这个指针就叫做虚函数表指针,其中v代表virtual,f代表funcation

每一个含有虚函数的类都至少有一个虚函数表指针,他的类型为函数指针数组,而虚函数的地址是存放在虚函数表中的,虚函数表也叫虚表

 2.原理分析

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}private:int _b = 1;
};
class Derived : public Base
{virtual void Func1(){cout << "Func()" << endl;}
private:int _a = 0;
};int main()
{Base b1;Derived d1;return 0;
}

 

解释多态调用的两个条件

对于条件一:必须是父类的指针或引用来调用函数

1.父类的指针指向父类对象时,依据虚函数表指针(vfptr),在虚函数表中找到函数的地址,再call这个地址来执行接下来的操作

2.父类的指针指向子类对象时,先完成切片,找到父类的那一部分,依据虚函数表指针(vfptr),在虚函数表中找到函数的地址,再call这个地址来执行接下来的操作

3.由于经过虚函数的重写后,虚函数的地址是不相同的,所以结果是不相同的,这是就形成了多态

对于编译器来说上面的两个调用是执行的同样的操作,都只是取对象的头四个字节,就是虚函数表指针,然后去虚表中找到对应调用函数的地址,然后执行接下来的操作

4.如果是父类的对象调用函数的话这时就要分析可能会总成的结果

这时尤其是这样的场景,Person* ptr=new Person,Student s;   *ptr=s ,这样如果支持能拷贝虚函数表指针的话,这时delete  ptr,就调用的是 Student类的析构函数,导致直接错误的

5.对于多态调用是在运行时,去虚表里面找到函数指针,确定函数指针后,调用函数;

对于普通调用是在编译链接时,确定函数地址

6.派生类中只有一个虚表指针(菱形继承除外),同一个类的对象共用一张虚表

7.虚函数也是也是和成员函数一样存在代码段的,不同的是虚函数会将自己的地址存在虚表中

对于条件二:虚函数的重写

从上面就可以看出虚函数的重写也叫覆盖,覆盖了原先虚函数的地址,重写是语法层的叫法,而覆盖是原理层的叫法

三:派生类的虚表生成

1.先将基类中的虚表内容拷贝一份到派生类的虚表中

2.如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数的地址来覆盖虚表中基类的虚函数地址

3.派生类自己新增的虚函数按其在派生类中的声明顺序增加到派生类虚表的最后

3.对于虚表存在哪里的探讨

对于栈和堆是不可能的,只有代码段或者静态区,但我们可以自己验证是存在哪里的

验证代码

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
void func()
{cout << "void func()" << endl;
}
int main()
{Base b1;Base b2;static int a = 0;int b = 0;int* p1 = new int;const char* p2 = "hello world";printf("静态区:%p\n", &a);printf("栈:%p\n", &b);printf("堆:%p\n", p1);printf("代码段:%p\n", p2);printf("虚表:%p\n", *((int*)&b1));printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", func);return 0;
}

对于这里的取虚表地址

 

可以这样来理解,&b1是整个类的地址,然后强转为(int*),再解引用取得就是头四个字节,即虚表地址 

 

我们发现 和虚表地址最接近的为代码段的地址,所以可以确定虚表是存在代码段的

4.对于是不是所有的虚函数都要存进虚函数表的探讨

首先确定答案 一定都是存在虚函数表的

接下来我们在vs上监视窗口来查看

分析代码

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }void func5() { cout << "Derive::func5" << endl; }
private:int b;
};class X :public Derive {
public:virtual void func3() { cout << "X::func3" << endl; }
};int main()
{Base b;Derive d;X x;Derive* p = &d;p->func3();p = &x;p->func3();return 0;
}

  

对于这里监视窗口的显示,在这里对于b是只有两个虚函数都存进了虚函数表中,但对于d和x都应该是四个虚函数存进虚函数表的,但在这里都只存了两个虚函数,但验证多态调用的话,结果为

结果是多态调用, 这时我们就不得不质疑此时监视窗口 的结果了

为了进一步的证明。我们可以调用内存窗口来查看

在内存中我们就会发现后两个地址与前两个虚函数的地址很接近,所以我们暂时可以认为虚函数是都存在虚函数表中的,

为了确定结果,我们可以使用打印虚表来验证猜想

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }void func5() { cout << "Derive::func5" << endl; }
private:int b;
};class X :public Derive {
public:virtual void func3() { cout << "X::func3" << endl; }
};typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];f();//(*f)();}printf("\n");
}int main()
{Base b;PrintVFT((VFUNC*)(*((long long*)&b)));//32位的话,可以采用intDerive d;X x;// PrintVFT((VFUNC*)&d);PrintVFT((VFUNC*)(*((long long*)&d)));PrintVFT((VFUNC*)(*((long long*)&x)));return 0;
}

 

这样看,只要是虚函数,都会将地址存到类的虚函数表里面的

 

二:多继承中的虚函数表

同样的我们可以采用例子来介绍

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl;}virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main()
{Derive d;Base1* p1 = &d;p1->func1();Base2* p2 = &d;p2->func1();return 0;
}

采用监视窗口的话 

就会发现对于基类的两张虚表中都没有存derived类的fun3() ,但我们可以使用多态的调用来验证下

所以的话,fun3是一定存在基类的两张 虚表中的其中一个里面,这样采用内存来看

所以最好的方式,我们还是来打印两个基类的虚函数表的 

typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{for (size_t i = 0; a[i] != 0; i++){printf("[%d]:%p->", i, a[i]);VFUNC f = a[i];f();//(*f)();}printf("\n");
}
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl;}virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main()
{Derive d;PrintVFT((VFUNC*)(*(int*)&d));//PrintVFT((VFUNC*)(*(int*)((char*)&d+sizeof(Base1))));Base2* ptr = &d;PrintVFT((VFUNC*)(*(int*)ptr));/*Base1* p1 = &d;p1->func1();Base2* p2 = &d;p2->func1();*/return 0;
}

 

所以此时我们就会知道,派生类的虚函数地址是存在第一个基类的虚函数表里面的 

三:常见的问答题 

 

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

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

相关文章

【架构】高可用:热备和冷备以及双活

热备与冷备技术培训文档 1. 引言 作为一名架构师&#xff0c;了解并掌握热备和冷备技术至关重要。本培训文档旨在为您提供关于热备和冷备技术的全面培训&#xff0c;帮助您更好地理解和应用这些技术。 2. 背景与需求&#xff08;Why&#xff09; 2.1 热备与冷备的定义 热备…

算法之前缀和和差分

一.前缀和 首先我们来看下面这道题&#xff1a; 假如给你一个数组从a1到an&#xff0c;接下来有m次询问,每次询问有两个参数l,r&#xff0c;请问al到ar之和&#xff0c;每次询问按行输出&#xff1f;&#xff1f;&#xff1f; 对于这个问题&#xff0c;如果你是没有这方面基…

Vue3+TS版本Uniapp:封装uni.request请求配置

作者&#xff1a;前端小王hs 阿里云社区博客专家/清华大学出版社签约作者✍/CSDN百万访问博主/B站千粉前端up主 封装请求配置项 封装拦截器封装uni.request 封装拦截器 uniapp的封装逻辑不同于Vue3项目中直接使用axios.create()方法创建实例&#xff08;在create方法中写入请求…

【探讨】RocketMQ消息灰度方案-消息逻辑隔离

vivo 鲁班平台 RocketMQ 消息灰度方案 - 稀土掘金分布式- vivo鲁班RocketMQ平台的消息灰度方案MQ消息在生产环境和灰度环境隔离一般怎么实现?消息隔离的原则 中心正常消费者,可以同时消费正常的消息和特定标签的消息(自动识别);特定标签的消费者,只能消费特定标签的消息。灰…

内存管理下及模板初阶

嗨喽&#xff0c;今天阿鑫给大家带来内存管理下以及模板初阶的博客&#xff0c;下面让我们开始今天的学习吧&#xff01; 内存管理下及模板初阶 new和delete的实现原理定位new表达式(placement-new)常见面试题泛型编程函数模板类模板 1. new和delete的实现原理 1.1 内置类型…

Logback:www.w3.org被qiang导致logback报错:Connect reset

稳定运行的系统中&#xff0c;突然报logback不能用的错误&#xff0c;如下&#xff1a; Reported exception: ch.qos.logback.core.joran.spi.JoranException: I/O error occurred while parsing xml file at ch.qos.logback.core.joran.event.SaxEventRecorder.recordEvents(…

用全连接对手写数字识别案例(附解决TensorFlow2.x没有examples问题)

数据集介绍 数据集直接调用可能出现问题&#xff0c;建议从官网直接下载下来&#xff0c;下载存在这四个文件 手写数字识别数据集下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1nqhP4yPNcqefKYs91jp9ng?pwdxe1h 提取码&#xff1a;xe1h 55000行训练数据集&a…

【树莓派Linux内核开发】入门实操篇(虚拟机Ubuntu环境搭建+内核源码获取与配置+内核交叉编译+内核镜像挂载)

【树莓派Linux内核开发】入门实操篇&#xff08;虚拟机Ubuntu环境搭建内核源码获取与配置内核交叉编译内核镜像挂载&#xff09; 文章目录 【树莓派Linux内核开发】入门实操篇&#xff08;虚拟机Ubuntu环境搭建内核源码获取与配置内核交叉编译内核镜像挂载&#xff09;一、搭建…

判断完数(C语言)

一、N-S流程图&#xff1b; 二、运行结果&#xff1b; 三、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h>int main() {//初始化变量值&#xff1b;int n 0;int i 1;int j 0;int result 1;//提示用户&#xff1b;printf("请输入一个…

每天学习一个Linux命令之join

每天学习一个Linux命令之join Linux系统是广泛应用的操作系统&#xff0c;为了更加高效地使用Linux系统&#xff0c;我们每天学习一个Linux命令。今天要学习的是join命令。 1. 简介 join命令在Linux系统中用于结合两个文件的共同字段&#xff0c;并输出它们的相应行。这个命…

目标检测网络YOLO进化之旅

yolo系列网络在目标检测领域取得了巨大的成功&#xff0c; 尤其是在工程实践中&#xff0c; 以其出色的性能优势获得了广泛的应用落地。 YOLO的前3个版本是由同一个作者团队出品&#xff0c; 算是官方版本。 之后的版本都是各个研究团队自己改进的版本&#xff0c; 之间并无明…

【布客技术评论】大模型开源与闭源:原因、现状与前景

在人工智能领域&#xff0c;大模型的开源与闭源一直是一个备受争议的话题。近期&#xff0c;某大厂厂长说了“开源模型永远超不过闭源模型”&#xff0c;结果&#xff0c;脸书就发布了开源模型Llama3&#xff0c;超过了OpenAI 的闭源模型 GPT4。本文将探讨大模型开源与闭源的原…

基础SQL DDL语句

MySQL的DDL&#xff08;Data Definition Language&#xff09;语句用于定义或修改数据库结构。 DDL数据库操作 查看所有的数据库 show databases; 红色圈起来的是系统数据库&#xff0c;是系统自带的 mysql&#xff1a;包含存储MySQL服务器运行时所需信息的表。这包括数据字典…

【Linux】MySQL的安装及配置(Ubuntu-18.04)

一、安装MySQL 分别安装MySQL服务器、MySQL客户端、C/C开发库 sudo apt-get install mysql-server sudo apt-get install mysql-client sudo apt-get install libmysqlclient-dev 二、配置MySQL 1.查看默认配置文件&#xff0c;此处的user和password为默认提供的&#xff0c;…

Ceph 分布式文件系统 搭建及使用

一、Ceph 介绍 在当今数据爆炸式增长的时代&#xff0c;企业对于可靠、可扩展的存储解决方案的需求日益迫切。Ceph 作为一种开源的、可伸缩的分布式存储解决方案&#xff0c;正逐渐成为企业级存储领域的热门选择。Ceph是一种由Radicalbit公司开发的开源分布式存储系统&#xf…

【数据结构】冒泡排序

冒泡排序是一种简单的排序算法。 它基于重复地交换相邻元素的位置。算法的每一步都会比较相邻的两个元素&#xff0c;如果它们的顺序错误&#xff08;即第一个元素比第二个元素大&#xff09;&#xff0c;则交换它们。这样&#xff0c;每经过一轮比较和交换&#xff0c;数组中…

ElasticSearch虚拟机安装(单机版)

1.下载7.10.2 下载链接&#xff0c;选择LINUX X86_64下载 2.创建用户 useradd es也可以使用系统默认用户&#xff08;非root&#xff09;,root用户会报错 3.解压 tar xvf elasticsearch-7.10.2-linux-x86_64.tar.gz假定目录在/home/es/elasticsearch-7.10.2-linux-x86_64 …

Spring Boot | Spring Boot 默认 “缓存管理“ 、Spring Boot “缓存注解“ 介绍

目录: 一、Spring Boot 默认 "缓存" 管理 :1.1 基础环境搭建① 准备数据② 创建项目③ 编写 "数据库表" 对应的 "实体类"④ 编写 "操作数据库" 的 Repository接口文件⑤ 编写 "业务操作列" Service文件⑥ 编写 "applic…

JavaCard学习笔记: CAP Component 之 Class Component

文章目录 整体结构tag和size字段signature_pool_length和signature_pooltype_descriptor结构导入类型编码导入项签名示例导入类导入数组导入远程方法 interfaces[]interface_info结构flagsinteface_countsuperinterfacesinterface_name class_info_compact classes[]结构flagsi…

稀碎从零算法笔记Day55-LeetCode:100291. 统计特殊字母的数量 II

今天可惜了&#xff0c;周赛第二题没看出来&#xff0c;导致第三题时间都不够&#xff0c;最后一题... 题目描述&#xff1a; 给你一个字符串 word。如果 word 中同时出现某个字母 c 的小写形式和大写形式&#xff0c;并且 每个 小写形式的 c 都出现在第一个大写形式的 c 之前…