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

embedded/2024/9/20 3:55:28/ 标签: 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.ppmy.cn/embedded/16484.html

相关文章

C++之类与对象(2)

目录 一、类的六个默认成员函数简介 二、构造函数 2.1 概念 2.2 特性 三、析构函数 3.1 概念 3.2 特性 四、拷贝构造函数 4.1 概念 4.2 特性 五、运算符重载 5.1 运算符重载 5.2 赋值运算符重载 5.3 前置和后置的重载 六、const成员 七、取地址及const取地址操作…

【ARMv9 DSU-120 系列 10 -- PMU 详细介绍】

请阅读【Arm DynamIQ™ Shared Unit-120 专栏 】 文章目录 DSU-120 PMUPMU features事件接口系统寄存器计数器PMU寄存器接口PMU eventsPMU interruptExternal cluster PMU registersDSU-120 PMU DynamIQ™共享单元-120(DSU-120)包括性能监视器,这些监视器使您能够在运行时收…

如何3分钟,快速开发一个新功能

背景 关于为什么做这个代码生成器&#xff0c;其实主要有两点: 参与的项目中有很多分析报表需要展示给业务部门&#xff0c;公司使用的商用产品&#xff0c;或多或少有些问题&#xff0c;这部分可能是历史选型导致的&#xff0c;这里撇开不不谈&#xff1b;项目里面也有很多C…

融合创新:LangChain、阿里云通义千问与SerpApi搜索的强强联合

随着人工智能技术的飞速发展,各类创新工具不断涌现,旨在为用户提供更高效、精准的信息获取与处理能力。本文将聚焦于三个颇具代表性的技术平台——LangChain、阿里云通义千问与SerpApi搜索,探讨它们各自的特性与优势,并展望它们如何相互融合,共同构建强大的智能解决方案。…

Android TV 桌面图标闪

<?xml version"1.0" encoding"utf-8"?> <!-- Copyright (C) 2014 The Android Open Source ProjectLicensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the Lice…

mongodb使用debezium

前置 服务器上需要安装jdk11 jdk下载地址 kafka安装 官网下载地址 安装教程 debezium 安装 运行 Debezium 连接器需要 Java 11 或更高版本 Debezium 并不是一个独立的软件&#xff0c;而是很多个 Kafka 连接器的总称。这些 Kafka 连接器分别对应不同的数据库&#xff0c;…

LoRa技术在物联网领域的安全性挑战与应对策略分享

随着物联网技术的飞速发展&#xff0c;LoRa技术作为一种新兴的无线通信技术&#xff0c;在物联网领域展现出了广阔的应用前景。然而&#xff0c;与此同时&#xff0c;其安全性问题也日益凸显&#xff0c;成为了制约其进一步发展的重要因素。本文将深入分析LoRa网络存在的安全漏…

【QEMU系统分析之启动篇(十七)】

系列文章目录 第十七章 QEMU系统仿真的显示初始化分析 文章目录 系列文章目录第十七章 QEMU系统仿真的显示初始化分析 前言一、QEMU是什么&#xff1f;二、QEMU系统仿真的启动分析1.系统仿真的初始化代码2.主循环数据初始化3. qemu_init_displays()init_displaystate()qemu_di…

区块链技术与应用学习笔记(8-9节)——北大肖臻课程

目录 8.挖矿 对于全节点和轻节点思考问题&#xff1f; ①全节点在比特币的主要作用&#xff1f; ②挖矿时当监听到别人已经挖出区块并且延申了最长合法链此时应该立刻放弃当前区块在 本地重新组装一个指向最后这个新合法区块的候选区块&#xff0c;重新开始挖矿。节点这么做…

Swift - Hello World

文章目录 Swift - Hello World1. Hello World Swift - Hello World 1. Hello World 不用编写main函数&#xff0c;Swift将全局范围内的首句可执行代码作为程序入口一句代码尾部可以省略分号&#xff08;;&#xff09;&#xff0c;多句代码写到同一行时必须用分号&#xff08;…

蚓链数字化营销系统与数字资产的关系

蚓链数字化营销系统是一种利用数字技术来实现营销目标的系统。它集成了多种数字营销工具和渠道&#xff0c;以收集、分析和利用客户数据&#xff0c;优化营销活动&#xff0c;并提高营销效果。 数字资产是一种新型的资产类别&#xff0c;它们以电子数据的形式存在&#xff0c;可…

基于Python实现心脏病数据可视化DEA+预测【500010103.1】

一、数据说明 该心脏病数据集是通过组合 5 个已经独立可用但以前未合并的流行心脏病数据集来策划的。在这个数据集中&#xff0c;5 个心脏数据集结合了 11 个共同特征&#xff0c;使其成为迄今为止可用于研究目的的最大心脏病数据集。 该数据集由 1190 个实例和 11 个特征组成…

Git工具的使用

文章目录 Git概述本地仓库命令远程仓库命令分支操作标签操作 IDEA上执行Git Git概述 一般工作流程如下&#xff1a; 从远程仓库中克隆 Git 资源作为本地仓库&#xff1b; 从本地仓库中checkout代码然后进行代码修改&#xff1b; 在提交本地仓库前先将代码提交到暂存区&#xff…

Qt——自定义富文本RichText

作者&#xff1a;小 琛 欢迎转载&#xff0c;请标明出处 文章目录 产品中的富文本富文本控件会面临的问题QTextBrowser例子&#xff1a;自定义富文本 产品中的富文本 用户界面中支持显示富文本格式&#xff08;如加粗、斜体、不同颜色、超链接等&#xff09;的文本内容。这种富…

政安晨:【Keras机器学习示例演绎】(十六)—— 用于图像分类的混合增强

目录 简介 设置 准备数据集 定义超参数 将数据转换为 TensorFlow 数据集对象 定义混合技术函数 可视化新的增强数据集 模型制作 1.使用混合数据集训练模型 2.在没有混合数据集的情况下训练模型 说明 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评…

C#鼠标拖拽无边框浮动窗体的方法:窗体控制

目录 (1)ReleaseCapture函数 (2)SendMessage函数 (3)实例 1.Resources.Designer.cs 2.Form1.Designer.cs 3.Form1.cs 一般情况下&#xff0c;在标题栏中按住鼠标左键不放即可实现拖动操作。 当做浮动窗体时&#xff0c;如果包含窗体边框&#xff0c;那么界面给使用者的感…

深度学习中的子空间、线性变换和矩阵概念应用

1.表示子空间 在深度学习中&#xff0c;“不同的表示子空间”通常是指模型通过不同的参数&#xff08;例如权重矩阵&#xff09;将输入数据映射到不同的高维空间&#xff0c;这些空间被称为表示子空间。每个子空间都能够捕获输入数据中不同的特征或模式。以下是一些详细解释&am…

php视图处理类

#简介 视图模版替换支持变量,for循环,foreach循环,if判断,if else判断 #调用 index.php <?php use ppt\tool\View; $haha 12; $id 111; $arr [x,y,3]; $pp [x>[id>1,name>12],y>[id>1,name>12],z>[id>1,name>12], ]; echo View::display…

Quarto Dashboards 教程 2:Dashboard Layout

「写在前面」 学习一个软件最好的方法就是啃它的官方文档。本着自己学习、分享他人的态度&#xff0c;分享官方文档的中文教程。软件可能随时更新&#xff0c;建议配合官方文档一起阅读。推荐先按顺序阅读往期内容&#xff1a; 1.quarto 教程 1&#xff1a;Hello, Quarto 2.qu…

diskMirror-backEnd-spring-boot | diskMirror 后端服务器 SpringBoot 版本!

diskMirror-backEnd-spring-boot diskMirror 后端服务器的 SpringBoot 版本&#xff0c;此版本中拓展了 DiskMirrorBackEnd&#xff0c;是一个完全的SpringBoot项目&#xff01; 目录 文章目录 diskMirror-backEnd-spring-boot目录我如何部署与配置docker 方式部署 diskMirro…