当前位置: 首页 > news >正文

【C++】—— 多态(下)

【C++】—— 多态(下)

  • 4 多态的原理
    • 4.1 虚函数表指针
    • 4.2 多态的原理
    • 4.3 动态绑定和静态绑定
    • 4.4 虚函数表

4 多态的原理

4.1 虚函数表指针

  我们以一道题来引入多态的原理

下面编译为 32 位程序的运行结果是什么()
A、编译报错  B、运行报错  C、8  D、12

class Base
{
public :virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
{Base b;cout << sizeof(b) << endl;return 0;
}

  按照我们之前的知识,这题答案应该选:C

  但我们不妨多留一个心眼:这题如果是考察内存对齐,为什么要加一个虚函数呢?是不是没有这么简单。

我们来看下运行结果:

在这里插入图片描述

  为什么呢? b b b类 中除了 _ b b b 和 _ c c c 成员,还多一个 _ v f p t r vfptr vfptr成员 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针 v v v 代表 v i r t u a l virtual virtual f f f 代表 f u n c t i o n function function)。一个含有虚函数的类中至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表

class Base
{
public :virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}void Func3(){cout << "Func3()" << endl;}protected:int _b = 1;char _ch = 'x';
};

在这里插入图片描述

  虚函数表其实是一个数组该数组中存放着该类中所有虚函数的地址。虚函数表本质是一个函数指针数组,而 _ v f p t r vfptr vfptr 则是指向这个数组的指针
  通过图片我们也可以看到:虚函数表中放着虚函数 F u n c 1 ( ) Func1() Func1() F u n c 2 ( ) Func2() Func2() 的地址,因为 F u n c 3 ( ) Func3() Func3() 不是虚函数,并没有放进去。

在这里插入图片描述

  
  

4.2 多态的原理

  认识到了虚表指针的存在,我们就可以进一步来了解多态的原理啦
  
  我们结合具体的样例来学习

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:string _name;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
protected:int _id;
};
class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
protected:string _codename;
};
void Func(Person* ptr)
{ptr->BuyTicket();
} int main()
{Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}

  上述代码中有三个类,每个类都有一个虚表指针

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

  可以看到,三个类中虚函数表的 B u y T i c k e t ( ) BuyTicket() BuyTicket() 函数指针的地址都是不同的
  
  多态是怎么做到指向谁就去调用谁的呢?
  在编译阶段,编译器检查语法,看满不满足多态的条件。如果满足多态,在编译这段指令时,底层不再是编译时通过调用对象确定函数的地址,而是变成:在运行时,到指向对象的虚函数表中去找对应虚函数的地址,进行调用
  这样就实现了指针引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数
  对 F u n c ( ) Func() Func() 函数的 p t r ptr ptr 来说,不论传递的是父类对象还是子类对象,在它眼里都是父类对象,不同的是子类需要进行切片, p t r ptr ptr 看到是是子类切片后剩下的父类对象。但是没关系,如果满足多态条件, p t r ptr ptr 会进入这个父类的虚函数表中查找对应的虚函数的地址,找到谁就调用谁
  
  满足多态时的汇编代码:

在这里插入图片描述

  前面的 m o v mov mov 指针简单来说就是:找到 _ v f p t r vfptr vfptr 指针,再找到对应的虚函数表,再找到对应的函数指针,最后将指针给 e a x eax eax 寄存器,寄存器去 c a l l call call 函数地址

  下面,我将父类的 v i r t u a l virtual virtual 去掉,他们就不满足多态的条件了,再来看看他们的汇编代码

class Person {
public:void BuyTicket() { cout << "买票-全价" << endl; }
protected:string _name;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
protected:int _id;
};
class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
protected:string _codename;
};

在这里插入图片描述

  两句代码搞定, p t r ptr ptr 是父类的指针,直接调用父类的 B u y T i c k e t ( ) BuyTicket() BuyTicket() 函数,与指向的对象无关。
  

4.3 动态绑定和静态绑定

  • 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就叫做动态绑定
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax, dword ptr[ptr]
00EF2004 mov edx, dword ptr[eax]
00EF2006 mov esi, esp
00EF2008 mov ecx, dword ptr[ptr]
00EF200B mov eax, dword ptr[edx]
00EF200D call eax// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();      
00EA2C91 mov ecx, dword ptr[ptr]
00EA2C94 call Student::Student(0EA153Ch)

从运行效率上来说,静态绑定更高一点,毕竟只有两句指令。
  
  

4.4 虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址
    • 这一点我们前面已经讲过了
        

      
  • 同一个类的对象虚函数表共用,不同类型对象虚表各自独立
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}protected:int _b = 1;char _ch = 'x';
};
int main()
{Base b1;Base b2;Base b3;return 0;
}

在这里插入图片描述

  这也解释了为什么虚函数不放在对象中,而是放在一个数组之中,因为不同的对象好共享
  如果不把虚函数地址放在虚函数表中,而是放在对象之中,那么每个对象都要存一份,太过冗余。像这样放在一个公共的地方,无论有几个虚函数,都只需多 4 个字节来存储指针就行
  


  

  • 派生类由两部分构成,继承下来的基类自己的成员,一般情况下继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但需要注意的是,这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个就像基类对象的成员和派生类对象中的基类对象成员也独立的
      

  

  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址
  • 派生类的虚函数表包含:基类的虚函数地址派生类重写的虚函数地址派生类自己的虚函数地址

什么意思呢?

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};
class Derive : public Base
{
public :// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};

  现在 基类 B a s e Base Base 中有两个虚函数,派生类 D e r i v e Derive Derive 中重写了 f u n c 1 ( ) func1() func1(),并且有一个自己的虚函数 f u n c 3 ( ) func3() func3()

派生类的虚函数表生成逻辑是这样的:

  • 先将基类的虚函数表拷贝一份
  • 看有无完成重写/覆盖。派生类 D e r i v e Derive Derive 重写了 f u n c 1 func1 func1 函数,就会用重写的 f u n c 1 func1 func1 将基类的 f u n c 1 func1 func1 进行覆盖
  • f u n c 2 func2 func2 并没有完成重写,不管
  • 最后再加上自己的虚函数

  


  

  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个 0x00000000 标记。(这个 C++ 并没有明确规定,各个编译器自行定义的,VS 系列编译器会在后面放个 0x00000000 标记,g++ 系列编译器不会放)
      

  

  • 虚函数存在哪?虚函数和普通函数一样的,编译好后是一段指令,都是存在 代码段(常量区) 的,只是虚函数的地址又存到了虚表中
  • 虚函数表存在哪?这个问题严格来说并没有标准答案,C++ 标准并没有规定,我们写下面的代码可以对比验证一下。VS下是存在代码段(常量区)
int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}

运行结果:

在这里插入图片描述

  可以看到,虚表的地址和常量区的最接近。我们可以大致判定 VS 下虚函数表是放在代码段

  
  
  
  


  好啦,本期关于 多态 的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 C++ 的学习路上一起进步!

相关文章:

【C++】—— 多态(下)

【C】—— 多态&#xff08;下&#xff09; 4 多态的原理 4.1 虚函数表指针4.2 多态的原理4.3 动态绑定和静态绑定 4.4 虚函数表 4 多态的原理 4.1 虚函数表指针 我们以一道题来引入多态的原理 下面编译为 32 位程序的运行结果是什么&#xff08;&#xff09; A、编译报错  B…...

idea 2023 配置 web service

前言 能在网上查到的资料,都是比较老的,搞了一上午才配置好了环境 因此记录一下,服务你我他 我的环境: java 1.8,Idea2023.1 配置web service 服务端 直接新建一个java新项目 下载插件 添加框架支持 启动项目 配置web service 客户端 新建项目,下载三个插件的步骤和上面服务…...

MYSQL数据库SQL+DQL

关于MySQL数据库中的SQL和DQL&#xff0c;以下是一些关键信息&#xff1a; SQL概述 SQL&#xff08;Structured Query Language&#xff0c;结构化查询语言&#xff09;是用于操作关系型数据库的编程语言。它定义了一套操作关系型数据库的统一标准。SQL语句可以单行或多行书写…...

Java中的异常Throwable

原文链接https://javaguide.cn/java/basis/java-basic-questions-03.html#%E5%BC%82%E5%B8%B8 Java 异常类层次结构图 Exception 和 Error 的区别 在 Java 中&#xff0c;所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类: Excep…...

Day4顺序表c++代码实现

代码中实现&#xff0c;顺序表的初始化&#xff0c;插入&#xff0c;查找&#xff0c;删除 废话不多说&#xff0c;直接上 #include<iostream> using namespace std; #define eleType int struct SequentialList {eleType* elements;//指针int size;int capacity;//容量…...

将图片转换成base64格式

1.先创建一个canvas对象&#xff0c;然后给canvas对象设置图片对象的宽高&#xff0c;再调用canvas的getContext获取2d上下文对象&#xff0c;再调用上下文对象的drawImage方法&#xff0c;再通过canvase对象的toDataURL方法&#xff0c;将图片转换成base64格式的字符串 2.将b…...

征服ES(ElasticSearch)的慢查询实战

在 Elasticsearch&#xff08;ES&#xff09;中&#xff0c;进行大数据查询时&#xff0c;常常会由于多种因素而导致性能显著下降。接下来&#xff0c;我们将深入探讨几种常见情况及其相应的解决方案。 一、常见问题分析 深分页、大排序 大量数据扫描与多分片上的多次排序会严…...

如何才能从普通程序员转行AI大模型?

人工智能已经成为一个非常火的方向。作为一名普通的程序员&#xff0c;该如何转向AI大模型方向。以程序员为例&#xff0c;看看普通程序员如何开启AI大模型之路。 接下来给大家分享一下程序员转大模型的一些注意点&#xff1a; 作为一名程序员&#xff0c;在考虑转行至大模型领…...

【番外】软件设计师中级笔记关于数据库技术更新笔记问题

提问 由于软件设计师中级笔记中第九章数据库技术基础的笔记内容太多&#xff0c;我应该分几期发布呢&#xff1f;还是一期一次性发布完成。 如果分为一期发布&#xff0c;可能需要给我多一些时间&#xff0c;由于markdown格式有所差异&#xff0c;所以我需要部分进行修改与调…...

【代码】约瑟夫问题——故事背景

Hello&#xff01;大家好&#xff0c;我是学霸小羊&#xff0c;今天先来讲讲约瑟夫问题的背景。 在古罗马时期&#xff0c;犹太历史学家约瑟夫斯领导犹太人反对罗马帝国的统治&#xff0c;并与罗马军队进行激烈的战斗。然而&#xff0c;在罗马军队的围困下&#xff0c;约瑟夫与…...

什么是事件冒泡和事件捕获

文章目录 1. 事件传播机制2. 事件冒泡&#xff08;Event Bubbling&#xff09;3. 事件捕获&#xff08;Event Capturing&#xff09;4. 事件冒泡和事件捕获的区别5. 阻止事件传播总结 事件冒泡和事件捕获是两种处理网页中事件传播的机制&#xff0c;特别是在 JavaScript 中处理…...

高端优质建站公司具备哪些优势?2024高端建站公司哪家好

从某种程度上讲&#xff0c;一个出色的建站公司需具备将无形的品牌价值巧妙转化为直观视觉元素的能力&#xff0c;这一转化过程极为考究&#xff0c;涵盖了设计的精细程度、色彩运用的巧妙以及空间布局的智慧&#xff0c;这些要素均不容忽视。 接下来考察网站的内容策划能力同…...

word删除空白页 | 亲测有效

想要删掉word里面的末尾空白页&#xff0c;但是按了delete之后也没有用 找了很久找到了以下亲测有效的方法 1. 通过鼠标右键在要删除的空白页面处显示段落标记 2. 在字号输入01&#xff0c;按ENTER&#xff08;回车键&#xff09; 3.成功删除了&#xff01;&#xff01;...

YashanDB学习-服务启停

YashanDB学习-服务启停 1、查看YashanDB 当前实例状态和数据库名称2、使用 yasboot 工具启停YashanDB3、服务器重启后无法通过yasboot命令运维管理数据库4、正常关闭数据库的方式 数据库安装过程中将实例自动切换成OPEN阶段&#xff0c;并创建名为yashandb的数据库。 1、查看Ya…...

在未排序的整数数组找到最小的缺失正整数

&#x1f381;&#x1f449;点击进入文心快码 Baidu Comate 官网&#xff0c;体验智能编码之旅&#xff0c;还有超多福利&#xff01;&#x1f381; &#x1f50d;【大厂面试真题】系列&#xff0c;带你攻克大厂面试真题&#xff0c;秒变offer收割机&#xff01; ❓今日问题&am…...

TCP连接管理机制:三次握手四次挥手

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 连接管理机制三次握手三次握手的目的三次握手的步骤第一次握手第二次握手第三次握手注意&#xff1a; 为什么建立连接是三次握手&…...

1022. 宠物小精灵之收服

思路 双层dp 代码 #include <bits/stdc.h> using namespace std;const int N 1010, mod 1e9 7;int n, m, k, x, y, z, ans, t; int w[N], f[N][N];void solve() {cin >> n >> m >> k;for (int i 1; i < k; i ){cin >> x >> y;f…...

人工智能生成内容(AI-Generated Content)

此外&#xff0c;ALGC还在影视剧本创作、音乐创作、设计与创意、虚拟助手与聊天机器人、教育与培训、新闻报道与文学创作等领域发挥着重要作用。 三、技术架构 ALGC产业生态体系通常呈现为上中下三层架构&#xff1a; 四、优势与挑战 优势&#xff1a; 挑战&#xff1a; 一、…...

深度学习:强化学习(Reinforcement Learning, RL)详解

强化学习&#xff08;Reinforcement Learning, RL&#xff09;详解 强化学习是机器学习的一个重要分支&#xff0c;它涉及到智能体&#xff08;agent&#xff09;通过与环境&#xff08;environment&#xff09;的交互学习如何做出决策。在强化学习中&#xff0c;智能体在不断…...

C语言笔记20

指针运算 #include <stdio.h>int main() {char ac[] {0,1,2,3,4,5,6,7,8,9,};char *p ac;printf("p %p\n", p);printf("p1%p\n", p1);int ai[] {0,1,2,3,4,5,6,7,8,9,};int *q ai;printf("q %p\n", q);printf("q1%p\n", q1)…...

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周&#xff0c;有很多同学在写期末Java web作业时&#xff0c;运行tomcat出现乱码问题&#xff0c;经过多次解决与研究&#xff0c;我做了如下整理&#xff1a; 原因&#xff1a; IDEA本身编码与tomcat的编码与Windows编码不同导致&#xff0c;Windows 系统控制台…...

Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?

Golang 面试经典题&#xff1a;map 的 key 可以是什么类型&#xff1f;哪些不可以&#xff1f; 在 Golang 的面试中&#xff0c;map 类型的使用是一个常见的考点&#xff0c;其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

【人工智能】神经网络的优化器optimizer(二):Adagrad自适应学习率优化器

一.自适应梯度算法Adagrad概述 Adagrad&#xff08;Adaptive Gradient Algorithm&#xff09;是一种自适应学习率的优化算法&#xff0c;由Duchi等人在2011年提出。其核心思想是针对不同参数自动调整学习率&#xff0c;适合处理稀疏数据和不同参数梯度差异较大的场景。Adagrad通…...

无法与IP建立连接,未能下载VSCode服务器

如题&#xff0c;在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈&#xff0c;发现是VSCode版本自动更新惹的祸&#xff01;&#xff01;&#xff01; 在VSCode的帮助->关于这里发现前几天VSCode自动更新了&#xff0c;我的版本号变成了1.100.3 才导致了远程连接出…...

解锁数据库简洁之道:FastAPI与SQLModel实战指南

在构建现代Web应用程序时&#xff0c;与数据库的交互无疑是核心环节。虽然传统的数据库操作方式&#xff08;如直接编写SQL语句与psycopg2交互&#xff09;赋予了我们精细的控制权&#xff0c;但在面对日益复杂的业务逻辑和快速迭代的需求时&#xff0c;这种方式的开发效率和可…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

生成 Git SSH 证书

&#x1f511; 1. ​​生成 SSH 密钥对​​ 在终端&#xff08;Windows 使用 Git Bash&#xff0c;Mac/Linux 使用 Terminal&#xff09;执行命令&#xff1a; ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" ​​参数说明​​&#xff1a; -t rsa&#x…...

鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/

使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题&#xff1a;docker pull 失败 网络不同&#xff0c;需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...