【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】—— 多态(下) 4 多态的原理 4.1 虚函数表指针4.2 多态的原理4.3 动态绑定和静态绑定 4.4 虚函数表 4 多态的原理 4.1 虚函数表指针 我们以一道题来引入多态的原理 下面编译为 32 位程序的运行结果是什么() A、编译报错 B…...

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

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

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

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

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

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

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

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

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

什么是事件冒泡和事件捕获
文章目录 1. 事件传播机制2. 事件冒泡(Event Bubbling)3. 事件捕获(Event Capturing)4. 事件冒泡和事件捕获的区别5. 阻止事件传播总结 事件冒泡和事件捕获是两种处理网页中事件传播的机制,特别是在 JavaScript 中处理…...

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

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

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

在未排序的整数数组找到最小的缺失正整数
🎁👉点击进入文心快码 Baidu Comate 官网,体验智能编码之旅,还有超多福利!🎁 🔍【大厂面试真题】系列,带你攻克大厂面试真题,秒变offer收割机! ❓今日问题&am…...

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

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

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

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)…...

基于SSM+微信小程序的房屋租赁管理系统(房屋2)
👉文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于SSM微信小程序的房屋租赁管理系统实现了有管理员、中介和用户。 1、管理员功能有,个人中心,用户管理,中介管理,房屋信息管理ÿ…...

selenium案例——爬取哔哩哔哩排行榜
案例需求: 1.使用selenium自动化爬虫爬取哔哩哔哩排行榜中舞蹈类的数据(包括视频标题、up主、播放量和评论量) 2.利用bs4进行数据解析和提取 3.将爬取的数据保存在本地json文件中 4.保存在excel文件中 分析: 1.请求url地址&…...

HTML5教程(三)- 常用标签
1 文本标签-h 标题标签(head): 自带加粗效果,从h1到h6字体大小逐级递减一个标题独占一行 语法 <h1>一级标题</h1><h2>二级标题</h2><h3>三级标题</h3><h4>四级标题</h4><h5…...

【HCIE-Datacom考试战报】2024-08-21 深圳 SRv6
8月21日深圳考试战报(SRV6) 前言 大家好呀,我是来自誉天的学员---,我是今年4月份开始看集训、备考实验的,但是专业课比较多,又还有其他比赛,所以我刚开始的进度很慢,六月底才进入冲…...

【京准电钟】“安全卫士”:卫星时空安全隔离防护装置
【京准电钟】“安全卫士”:卫星时空安全隔离防护装置 【京准电钟】“安全卫士”:卫星时空安全隔离防护装置 当前,我国电力系统普遍采用北斗卫星或者GPS卫星授时来实现时间同步,但不加防护的授时装置存在卫星信号被干扰或欺骗的风险…...

优先级队列(2)_数据流中第k大元素
个人主页:C忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C忠实粉丝 原创 优先级队列(2)_数据流中第k大元素 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌 目…...

【CSS】纯CSS Loading动画组件
<template><div class"ai-loader-box"><!-- AI loader --><div class"ai-loader"><div class"text"><p>AI智能分析中....</p></div><div class"horizontal"><div class&quo…...

rootless模式下istio ambient鉴权策略
环境说明 rootless模式下测试istio Ambient功能 四层鉴权策略 这里四层指的是网络通信模型的第四层,主要的传输协议为TCP和UDP。 用于限制服务间的通信,比如下面的策略应用于带有 app: productpage 标签的 Pod, 并且仅允许来自服务帐户 clus…...

超详细的总结!最新大模型算法岗面试题(含答案)来了!
大模型应该是目前当之无愧的最有影响力的AI技术,它正在革新各个行业,包括自然语言处理、机器翻译、内容创作和客户服务等,正成为未来商业环境的重要组成部分。 截至目前大模型已超过200个,在大模型纵横的时代,不仅大模…...

vmware-17pro全网最细安装教程(图文讲解,不需注册账户)
文章目录 一、下载安装包: 二、安装教程: 三、检查是否安装成功 四、许可证密匙 vmware安装教程 一、下载安装包: 链接:https://pan.baidu.com/s/1yC610SU1-O9Jtk7nUrZuSA?pwdsKBy 提取码:sKBy 二、安装教程&…...