当前位置: 首页 > 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; …...

GRPO实战:如何用多个reward function优化你的RL模型?(附完整代码示例)

GRPO实战&#xff1a;多奖励函数融合策略与代码实现指南 强化学习模型的效果很大程度上取决于奖励函数的设计。单一奖励函数往往难以全面评估复杂任务&#xff0c;而多奖励函数融合策略能更精准地引导模型学习。本文将深入探讨GRPO框架中多奖励函数的实战应用&#xff0c;从原理…...

革新性PDF可视化标记技术:从原理到实践的全方位解析

革新性PDF可视化标记技术&#xff1a;从原理到实践的全方位解析 【免费下载链接】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实操手册&#xff1a;针对数学题优化的token长度设置技巧 1. 模型特点与适用场景 Phi-4-mini-reasoning是一个专为推理任务优化的文本生成模型&#xff0c;特别适合处理需要多步分析的数学题和逻辑题。与通用聊天模型不同&#xff0c;它被设计为直接输出…...

Android Studio中文界面终极配置指南:告别英文障碍,提升开发效率

Android Studio中文界面终极配置指南&#xff1a;告别英文障碍&#xff0c;提升开发效率 【免费下载链接】AndroidStudioChineseLanguagePack AndroidStudio中文插件(官方修改版本&#xff09; 项目地址: https://gitcode.com/gh_mirrors/an/AndroidStudioChineseLanguagePac…...

[特殊字符] Nano-Banana效果分享:电动工具齿轮箱高精度啮合关系可视化拆解图

Nano-Banana效果分享&#xff1a;电动工具齿轮箱高精度啮合关系可视化拆解图 你有没有想过&#xff0c;一个复杂的电动工具内部到底长什么样&#xff1f;那些精密的齿轮是如何咬合在一起&#xff0c;将电机的旋转变成强大动力的&#xff1f;传统的产品说明书往往只有一张模糊的…...

[具身智能-190]:具身智能常见的仿真平台与常见的模型算法,包括传统算法与AI算法。

在具身智能的开发中&#xff0c;仿真平台与模型算法是相辅相成的两个核心部分。仿真平台为算法提供了安全、高效、低成本的“练兵场”&#xff0c;而算法则是赋予机器人智能的“大脑”。以下为你梳理当前主流的仿真平台以及两类核心的模型算法&#xff1a;传统算法与AI算法。&a…...

【已验证】STM32驱动OLED(SSD1306)显示字符

本文介绍如何使用STM32F103C8T6&#xff08;蓝板&#xff09;通过软件模拟IIC协议驱动0.96英寸OLED&#xff08;驱动芯片SSD1306&#xff09;&#xff0c;这个小屏幕相信每一个朋友在大学生活里都不会错过&#xff0c;也是很多课设毕设显示需求的首选&#xff0c;我一向喜欢直接…...

告别重复造轮子:用快马ai一键生成arm7标准外设驱动,效率提升50%

作为一名嵌入式开发者&#xff0c;我经常需要和ARM7这类微控制器打交道。每次新项目启动&#xff0c;最头疼的就是那些重复性的外设驱动编写工作——尤其是定时器中断这种基础功能&#xff0c;虽然逻辑简单&#xff0c;但写起来特别耗时。最近发现InsCode(快马)平台的AI生成功能…...

革新性图表创作:Mermaid Live Editor如何重构技术可视化工作流

革新性图表创作&#xff1a;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 开发高级工程师是一个重要的里程碑。这不仅要求开发者具备扎实的编码功底和丰富的项目经验,更需要其在架构设计、…...