【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++】多态,虚函数表相关问题解决
文章目录 多态概念及其触发条件重写和协变(考点1)(考点2) 虚函数表及其位置(考点3) 多继承中的虚函数表 多态概念及其触发条件 多态的概念:通俗来说,就是多种形态。具体点就是去完成…...
探索大型语言模型的开源人工智能基础设施:北京开源AI Meetup回顾
原文参见Explore open source AI Infra for Large Language Models: Highlights from the Open Source AI Meetup Beijing | Cloud Native Computing Foundation 背景介绍: 最近,在 ChatGPT 的成功推动下,大型语言模型及其应用程序的流行度激…...
Langchain 的 Conversation buffer window memory
Langchain 的 Conversation buffer window memory ConversationBufferWindowMemory 保存一段时间内对话交互的列表。它仅使用最后 K 个交互。这对于保持最近交互的滑动窗口非常有用,因此缓冲区不会变得太大。 我们首先来探讨一下这种存储器的基本功能。 示例代码&…...
电流源电路
3.3.3电流源电路 镜像电流源 电路 分析 仿真 比例电流源 电路 分析 仿真 加射极输出器的电流源1 电路 分析 仿真 加射极输出器的电流源2 电路 分析 仿真 威尔逊电流源 电路 分析 仿真...
iOS开发-CMMotionManager传感器陀螺仪
iOS开发-CMMotionManager传感器陀螺仪 之前开发中遇到需要使用陀螺仪判断是否拍照时候水平判断,如果没有水平拍照,则给出提示。方便用户拍照合适的题目图片。 一、CMMotionManager CMMotionManager是什么 CMMotionManager 是 Core Motion 库的核心类&…...
影刀下载,插件安装
1、下载 在影刀官网下载:www.yingdao.com 2、谷歌插件安装 参考: 影刀插件安装各种方式 浏览器安装插件说明 - 影刀帮助中心 安装说明:驱动外置 Chrome 需要安装插件,并且保证此插件处于开启状态 方式一:用户头…...
Linux的tcpdump命令详解
tcpdump 一款sniffer工具,是Linux上的抓包工具,嗅探器 补充说明 tcpdump命令 是一款抓包,嗅探器工具,它可以打印所有经过网络接口的数据包的头信息,也可以使用-w选项将数据包保存到文件中,方便以后分析。…...
springboot运行报错Failed to load ApplicationContext for xxx
Failed to load ApplicationContext for报错解决方法 报错Failed to load ApplicationContext for 报错Failed to load ApplicationContext for 网上找了一堆方法都尝试了还是没用 包括添加mapperScan,添加配置类 配置pom文件 [外链图片转存失败,源站可能有防盗链机…...
[SQL挖掘机] - 内连接: inner join
介绍: 内连接是一种多表连接方式,用于将两个或多个表中的数据通过共同的列值进行匹配,并返回满足连接条件的匹配行。简单来说,内连接能够将相关联的数据组合在一起,以便进行更复杂和全面的数据分析。 内连接的工作原理如下&…...
mysql(四)数据备份
目录 前言 一、概述 二、备份的类型 (一)物理与逻辑角度 (二)数据库备份策略角度 三、常见的备份方法 四、完整备份 (一)打包数据库文件备份 (二)备份工具备份 五、增量备份 六、操…...
Spring 拦截器
上篇博客链接:SpringAOP详解 上篇博客我们提到使用AOP的环绕通知来完成统一的用户登陆验证虽然方便了许多,但随之而来也带来了新的问题: HttpSession不知道如何去获取,获取困难登录和注册的方法并不需要拦截,使用切点没办法定义哪…...
【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 的设计,跟内存里面的数据结构有关系。 以下例子来简单描述this在不同情况下所指向的对象。 var obj {aa: function(){console.log(this.num)},num: 5 };var aa obj.aa; var num 10;obj.aa(); // …...
Caddy 中实现自动 HTTPS
要在 Caddy 中实现自动 HTTPS,您可以按照以下步骤进行操作: 步骤 1:安装 Caddy 首先,您需要安装 Caddy 服务器。您可以从 Caddy 的官方网站(https://caddyserver.com/)下载适用于您的操作系统的最新版本。…...
SK5代理(socks5代理)在网络安全与爬虫应用中的优势与编写指南
一、SK5代理(socks5代理)的基本概念 SK5代理是一种网络代理协议,它允许客户端通过代理服务器与目标服务器进行通信。相较于HTTP代理,SK5代理在传输数据时更加高效且安全,它支持TCP和UDP协议,并且能够实现数…...
【LeetCode-简单】剑指 Offer 06. 从尾到头打印链表(详解)
题目 输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。 题目地址:剑指 Offer 06. 从尾到头打印链表 - 力扣(LeetCode) 方法1:栈 思路 题目要求我们将链表的从尾到投打印一…...
【LeetCode】114.二叉树展开为链表
题目 给你二叉树的根结点 root ,请你将它展开为一个单链表: 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同。 示例 1&…...
DAY3,Qt(完成闹钟的实现,定时器事件处理函数的使用)
1.完成闹钟的实现,到点播报文本框的内容; ---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接入宽带,默认接口2到接口4组成1个vlan,名称vlan。其中接口5特殊,带宽最大100M。 计划将接口2和接口4组成第一个vlan&…...
PHPWord 实现合并多个word文件
PHPWord 本来想着当调包侠呢,结果翻了一遍文档,没有这种操作支持,阿这😂 GPT 不出意外的一顿胡扯,给👨🦳气的要中风啦 思路 word 也就是docx结尾的文件本质上就是xml字符串, …...
GRPO实战:如何用多个reward function优化你的RL模型?(附完整代码示例)
GRPO实战:多奖励函数融合策略与代码实现指南 强化学习模型的效果很大程度上取决于奖励函数的设计。单一奖励函数往往难以全面评估复杂任务,而多奖励函数融合策略能更精准地引导模型学习。本文将深入探讨GRPO框架中多奖励函数的实战应用,从原理…...
革新性PDF可视化标记技术:从原理到实践的全方位解析
革新性PDF可视化标记技术:从原理到实践的全方位解析 【免费下载链接】obsidian-pdf-plus PDF: the most Obsidian-native PDF annotation & viewing tool ever. Comes with optional Vim keybindings. 项目地址: https://gitcode.com/gh_mirrors/ob/obsidian-…...
Phi-4-mini-reasoning实操手册:针对数学题优化的token长度设置技巧
Phi-4-mini-reasoning实操手册:针对数学题优化的token长度设置技巧 1. 模型特点与适用场景 Phi-4-mini-reasoning是一个专为推理任务优化的文本生成模型,特别适合处理需要多步分析的数学题和逻辑题。与通用聊天模型不同,它被设计为直接输出…...
Android Studio中文界面终极配置指南:告别英文障碍,提升开发效率
Android Studio中文界面终极配置指南:告别英文障碍,提升开发效率 【免费下载链接】AndroidStudioChineseLanguagePack AndroidStudio中文插件(官方修改版本) 项目地址: https://gitcode.com/gh_mirrors/an/AndroidStudioChineseLanguagePac…...
[特殊字符] Nano-Banana效果分享:电动工具齿轮箱高精度啮合关系可视化拆解图
Nano-Banana效果分享:电动工具齿轮箱高精度啮合关系可视化拆解图 你有没有想过,一个复杂的电动工具内部到底长什么样?那些精密的齿轮是如何咬合在一起,将电机的旋转变成强大动力的?传统的产品说明书往往只有一张模糊的…...
[具身智能-190]:具身智能常见的仿真平台与常见的模型算法,包括传统算法与AI算法。
在具身智能的开发中,仿真平台与模型算法是相辅相成的两个核心部分。仿真平台为算法提供了安全、高效、低成本的“练兵场”,而算法则是赋予机器人智能的“大脑”。以下为你梳理当前主流的仿真平台以及两类核心的模型算法:传统算法与AI算法。&a…...
【已验证】STM32驱动OLED(SSD1306)显示字符
本文介绍如何使用STM32F103C8T6(蓝板)通过软件模拟IIC协议驱动0.96英寸OLED(驱动芯片SSD1306),这个小屏幕相信每一个朋友在大学生活里都不会错过,也是很多课设毕设显示需求的首选,我一向喜欢直接…...
告别重复造轮子:用快马ai一键生成arm7标准外设驱动,效率提升50%
作为一名嵌入式开发者,我经常需要和ARM7这类微控制器打交道。每次新项目启动,最头疼的就是那些重复性的外设驱动编写工作——尤其是定时器中断这种基础功能,虽然逻辑简单,但写起来特别耗时。最近发现InsCode(快马)平台的AI生成功能…...
革新性图表创作:Mermaid Live Editor如何重构技术可视化工作流
革新性图表创作:Mermaid Live Editor如何重构技术可视化工作流 【免费下载链接】mermaid-live-editor Edit, preview and share mermaid charts/diagrams. New implementation of the live editor. 项目地址: https://gitcode.com/GitHub_Trending/me/mermaid-liv…...
深入解析 Android 开发高级工程师:职责、技能与面试精要
在移动互联网时代,Android 平台作为全球最大的移动操作系统之一,其应用开发人才的需求持续旺盛。对于追求技术深度和业务影响力的开发者而言,进阶成为 Android 开发高级工程师是一个重要的里程碑。这不仅要求开发者具备扎实的编码功底和丰富的项目经验,更需要其在架构设计、…...


















