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

【C++】多态,虚函数表相关问题解决

文章目录

  • 多态概念及其触发条件
  • 重写和协变
    • (考点1)
    • (考点2)
  • 虚函数表及其位置
    • (考点3)
  • 多继承中的虚函数表

多态概念及其触发条件

  多态的概念:通俗来说,就是多种形态。具体点就是去完成某个行为,当不同的对象去完成时,会产生出不同的状态

多态的构成条件:
1.必须通过基类的指针或者引用调用虚函数(即被virtual修饰的类成员函数称为虚函数)
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重写和协变

  虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数类型),称子类的虚函数重写了基类的虚函数

虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
  派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
2. 析构函数的重写(基类与派生类析构函数的名字不同)
  如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

override和final两个关键字

(考点1)

这里强调一下,重写重写的是实现。看以下这个场景:(考点)

class A
{
public:A(){}virtual void func(int val = 1) {std::cout << "A->" << val << std::endl; }virtual void test() {func(); }
};
class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{A* p = new B();p->test();return 0;
}

  打印结果为B->1,说明调的是子类的func函数,但是缺省值用的却是父类,返回值,函数名,参数类型相同即构成重写,重写重写的是实现,壳子用的是父类的,写的内容自己控制

(考点2)

那为什么要把析构函数构成重写呢?看以下这个场景:(考点)


class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:~Student() {cout << "~Student()" << endl;delete[] ptr;}
protected:int* ptr = new int[10];
};int main()
{Person* p = new Person;delete p;p = new Student;delete p; return 0;
}

  当我们用父类指针,指向子类对象时,期望析构的是子类对象,而不是父类对象。不构成重写的话,无论父类指针是指向子类对象还是父类对象,析构的都是父类对象,导致下面的 ptr 动态开辟的空间没有释放而内存泄漏





  当我们给父类析构函数加上 virtual,让其构成重写后。同时注意这里析构玩~Student后还会析构继承父类,照应上面的构造先父后子,"析构先子后父"



抽象类:
  在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承




动态绑定与静态绑定:

虚函数表及其位置

  一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,通过下面这个例子来看对象模型



(考点3)

这时候我们再反过来思考,为什么一定是父类的指针或者引用,而不能是父类对象?

  首先我们可以看出,子类对象会先拷贝父类虚函数表,然后再对需要重写的虚函数进行地址修改。
  假如我们把子类对象赋值给父类对象,那么子类对象的虚函数表要不要拷贝给父类?如果虚函数表不拷贝,那么还是调用父类的函数,没有构成多态。
  如果拷贝了,那么父类对象的虚函数表存的是子类对象修改后的虚函数,如下图:此时我们无法再调用父类本身被重写的函数,因为无论我们传子类还是父类对象,调用的都是子类对象的函数,不能构成多态。
  因此多态的条件,一定是父类的指针或者引用,这样可以避免像下面这样拷贝带来的错误。

虚表位置

class Person {
public:virtual	void BuyTicket() const { cout << "成人-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() const { cout << "学生-半价" << endl; }
};
int main()
{Person ps;Student st;int a = 0;printf("栈:%p\n\n", &a);static int b = 0;printf("静态区:%p\n\n", &b);int* p = new int;printf("堆:%p\n\n", p);const char* str = "hello world";printf("常量区:%p\n\n", str);printf("虚表1:%p\n", *((int*)&ps));printf("虚表2:%p\n", *((int*)&st));return 0;
}

  虚表存放在哪里呢?首先排除堆,虚表由编译器生成,不会自己去动态申请空间。其次排除栈,同类型对象公用一张虚表,栈都是伴随栈帧走的,不能函数调用结束,栈帧销毁,虚表就销毁了吧。我们用打印的方式来看一下虚表是存在哪里的
  看下面的代码和输出结果,我们可以发现,虚表是存在常量区的

多继承中的虚函数表

// 打印函数指针数组
typedef void(*FUNC_PTR) ();
void PrintVFT(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);FUNC_PTR f = table[i];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;int vft1 = *((int*)&d);Base2* ptr = &d;int vft2 = *((int*)ptr);printf("第一张虚表:\n");PrintVFT((FUNC_PTR*)vft1);printf("第二张虚表:\n");PrintVFT((FUNC_PTR*)vft2);return 0;
}

  先看上面这段代码,首先d对象有几张虚表呢?看下面的监视窗口,很明显发现d对象有两张虚表,但是d对象自己的虚函数func3去哪里了,其实它在第一张虚表中,我们可以通过上面的代码打印观察出来,f()这个地址可以调用,说明它一定是函数。这里是可以认为是编译器的监视窗口故意隐藏了func3函数,也可以认为是它的一个小bug



  可是细心一点发现,两张表中的func1地址不一样,它们不是都重写了func1函数吗?而且用父类指针调用会发现,它们调的是同一个函数,那么这里为什么地址不一样呢?



看下面这个场景


  
注意:这里你要调用的是派生类d对象的func1函数,this指针应该指向d对象,而这里的ptr1指针恰好指向d对象,不需要改动。而ptr2指向的却是Base2对象。调用d对象的func1函数要传d对象的this指针, 而不是Base2对象的this指针。所以这里第二张表的地址其实是"虚地址",多封装了几层是为了修正this指针

接下来我们通过汇编来看看ptr1和ptr2调用的区别,更好理解Base2的"虚地址"
ptr1调用




ptr2调用

相关文章:

【C++】多态,虚函数表相关问题解决

文章目录 多态概念及其触发条件重写和协变&#xff08;考点1&#xff09;&#xff08;考点2&#xff09; 虚函数表及其位置&#xff08;考点3&#xff09; 多继承中的虚函数表 多态概念及其触发条件 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态。具体点就是去完成…...

探索大型语言模型的开源人工智能基础设施:北京开源AI Meetup回顾

原文参见Explore open source AI Infra for Large Language Models: Highlights from the Open Source AI Meetup Beijing | Cloud Native Computing Foundation 背景介绍&#xff1a; 最近&#xff0c;在 ChatGPT 的成功推动下&#xff0c;大型语言模型及其应用程序的流行度激…...

Langchain 的 Conversation buffer window memory

Langchain 的 Conversation buffer window memory ConversationBufferWindowMemory 保存一段时间内对话交互的列表。它仅使用最后 K 个交互。这对于保持最近交互的滑动窗口非常有用&#xff0c;因此缓冲区不会变得太大。 我们首先来探讨一下这种存储器的基本功能。 示例代码&…...

电流源电路

3.3.3电流源电路 镜像电流源 电路 分析 仿真 比例电流源 电路 分析 仿真 加射极输出器的电流源1 电路 分析 仿真 加射极输出器的电流源2 电路 分析 仿真 威尔逊电流源 电路 分析 仿真...

iOS开发-CMMotionManager传感器陀螺仪

iOS开发-CMMotionManager传感器陀螺仪 之前开发中遇到需要使用陀螺仪判断是否拍照时候水平判断&#xff0c;如果没有水平拍照&#xff0c;则给出提示。方便用户拍照合适的题目图片。 一、CMMotionManager CMMotionManager是什么 CMMotionManager 是 Core Motion 库的核心类&…...

影刀下载,插件安装

1、下载 在影刀官网下载&#xff1a;www.yingdao.com 2、谷歌插件安装 参考&#xff1a; 影刀插件安装各种方式 浏览器安装插件说明 - 影刀帮助中心 安装说明&#xff1a;驱动外置 Chrome 需要安装插件&#xff0c;并且保证此插件处于开启状态 方式一&#xff1a;用户头…...

Linux的tcpdump命令详解

tcpdump 一款sniffer工具&#xff0c;是Linux上的抓包工具&#xff0c;嗅探器 补充说明 tcpdump命令 是一款抓包&#xff0c;嗅探器工具&#xff0c;它可以打印所有经过网络接口的数据包的头信息&#xff0c;也可以使用-w选项将数据包保存到文件中&#xff0c;方便以后分析。…...

springboot运行报错Failed to load ApplicationContext for xxx

Failed to load ApplicationContext for报错解决方法 报错Failed to load ApplicationContext for 报错Failed to load ApplicationContext for 网上找了一堆方法都尝试了还是没用 包括添加mapperScan&#xff0c;添加配置类 配置pom文件 [外链图片转存失败,源站可能有防盗链机…...

[SQL挖掘机] - 内连接: inner join

介绍: 内连接是一种多表连接方式&#xff0c;用于将两个或多个表中的数据通过共同的列值进行匹配&#xff0c;并返回满足连接条件的匹配行。简单来说&#xff0c;内连接能够将相关联的数据组合在一起&#xff0c;以便进行更复杂和全面的数据分析。 内连接的工作原理如下&…...

mysql(四)数据备份

目录 前言 一、概述 二、备份的类型 &#xff08;一&#xff09;物理与逻辑角度 &#xff08;二&#xff09;数据库备份策略角度 三、常见的备份方法 四、完整备份 &#xff08;一&#xff09;打包数据库文件备份 &#xff08;二&#xff09;备份工具备份 五、增量备份 六、操…...

Spring 拦截器

上篇博客链接&#xff1a;SpringAOP详解 上篇博客我们提到使用AOP的环绕通知来完成统一的用户登陆验证虽然方便了许多&#xff0c;但随之而来也带来了新的问题: HttpSession不知道如何去获取&#xff0c;获取困难登录和注册的方法并不需要拦截&#xff0c;使用切点没办法定义哪…...

【libevent】http客户端3:简单封装

LibEventHttp: 适用于简单的http请求 LibEventHttp/* Copyright (c) MediaArea.net SARL. All Rights Reserved.** Use of this source code is governed by a BSD-style license that can* be found in the License.html file in the root of the source tree.*///--------…...

JavaScript的函数中this的指向

JavaScript的函数中this的指向 JavaScript 语言之所以有 this 的设计&#xff0c;跟内存里面的数据结构有关系。 以下例子来简单描述this在不同情况下所指向的对象。 var obj {aa: function(){console.log(this.num)},num: 5 };var aa obj.aa; var num 10;obj.aa(); // …...

Caddy 中实现自动 HTTPS

要在 Caddy 中实现自动 HTTPS&#xff0c;您可以按照以下步骤进行操作&#xff1a; 步骤 1&#xff1a;安装 Caddy 首先&#xff0c;您需要安装 Caddy 服务器。您可以从 Caddy 的官方网站&#xff08;https://caddyserver.com/&#xff09;下载适用于您的操作系统的最新版本。…...

SK5代理(socks5代理)在网络安全与爬虫应用中的优势与编写指南

一、SK5代理&#xff08;socks5代理&#xff09;的基本概念 SK5代理是一种网络代理协议&#xff0c;它允许客户端通过代理服务器与目标服务器进行通信。相较于HTTP代理&#xff0c;SK5代理在传输数据时更加高效且安全&#xff0c;它支持TCP和UDP协议&#xff0c;并且能够实现数…...

【LeetCode-简单】剑指 Offer 06. 从尾到头打印链表(详解)

题目 输入一个链表的头节点&#xff0c;从尾到头反过来返回每个节点的值&#xff08;用数组返回&#xff09;。 题目地址&#xff1a;剑指 Offer 06. 从尾到头打印链表 - 力扣&#xff08;LeetCode&#xff09; 方法1&#xff1a;栈 思路 题目要求我们将链表的从尾到投打印一…...

【LeetCode】114.二叉树展开为链表

题目 给你二叉树的根结点 root &#xff0c;请你将它展开为一个单链表&#xff1a; 展开后的单链表应该同样使用 TreeNode &#xff0c;其中 right 子指针指向链表中下一个结点&#xff0c;而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同。 示例 1&…...

DAY3,Qt(完成闹钟的实现,定时器事件处理函数的使用)

1.完成闹钟的实现&#xff0c;到点播报文本框的内容&#xff1b; ---alarm.h---头文件 #ifndef ALARM_H #define ALARM_H#include <QWidget> #include <QTimerEvent> //定时器处理函数类 #include <QTime> //时间类 #include <QPushButton> //按钮…...

TL-ER3220G设置vlan

TL-ER3220G是企业宽带路由器。 自带5个RJ45接口。 其中接口1到接口4都可以接入宽带线路。最多可以并接4路。 本例由接口1接入宽带&#xff0c;默认接口2到接口4组成1个vlan&#xff0c;名称vlan。其中接口5特殊&#xff0c;带宽最大100M。 计划将接口2和接口4组成第一个vlan&…...

PHPWord 实现合并多个word文件

PHPWord 本来想着当调包侠呢&#xff0c;结果翻了一遍文档&#xff0c;没有这种操作支持&#xff0c;阿这&#x1f602; GPT 不出意外的一顿胡扯&#xff0c;给&#x1f468;‍&#x1f9b3;气的要中风啦 思路 word 也就是docx结尾的文件本质上就是xml字符串&#xff0c; …...

[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解

突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 ​安全措施依赖问题​ GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

什么是EULA和DPA

文章目录 EULA&#xff08;End User License Agreement&#xff09;DPA&#xff08;Data Protection Agreement&#xff09;一、定义与背景二、核心内容三、法律效力与责任四、实际应用与意义 EULA&#xff08;End User License Agreement&#xff09; 定义&#xff1a; EULA即…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2

每日一言 今天的每一份坚持&#xff0c;都是在为未来积攒底气。 案例&#xff1a;OLED显示一个A 这边观察到一个点&#xff0c;怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 &#xff1a; 如果代码里信号切换太快&#xff08;比如 SDA 刚变&#xff0c;SCL 立刻变&#…...

LeetCode - 199. 二叉树的右视图

题目 199. 二叉树的右视图 - 力扣&#xff08;LeetCode&#xff09; 思路 右视图是指从树的右侧看&#xff0c;对于每一层&#xff0c;只能看到该层最右边的节点。实现思路是&#xff1a; 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...

基于Springboot+Vue的办公管理系统

角色&#xff1a; 管理员、员工 技术&#xff1a; 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能&#xff1a; 该办公管理系统是一个综合性的企业内部管理平台&#xff0c;旨在提升企业运营效率和员工管理水…...

windows系统MySQL安装文档

概览&#xff1a;本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容&#xff0c;为学习者提供全面的操作指导。关键要点包括&#xff1a; 解压 &#xff1a;下载完成后解压压缩包&#xff0c;得到MySQL 8.…...

渗透实战PortSwigger靶场:lab13存储型DOM XSS详解

进来是需要留言的&#xff0c;先用做简单的 html 标签测试 发现面的</h1>不见了 数据包中找到了一个loadCommentsWithVulnerableEscapeHtml.js 他是把用户输入的<>进行 html 编码&#xff0c;输入的<>当成字符串处理回显到页面中&#xff0c;看来只是把用户输…...

Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解

文章目录 1. 题目描述1.1 链表节点定义 2. 理解题目2.1 问题可视化2.2 核心挑战 3. 解法一&#xff1a;HashSet 标记访问法3.1 算法思路3.2 Java代码实现3.3 详细执行过程演示3.4 执行结果示例3.5 复杂度分析3.6 优缺点分析 4. 解法二&#xff1a;Floyd 快慢指针法&#xff08;…...

车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...