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

Python|GIF 解析与构建(5):手搓截屏和帧率控制

目录 Python&#xff5c;GIF 解析与构建&#xff08;5&#xff09;&#xff1a;手搓截屏和帧率控制 一、引言 二、技术实现&#xff1a;手搓截屏模块 2.1 核心原理 2.2 代码解析&#xff1a;ScreenshotData类 2.2.1 截图函数&#xff1a;capture_screen 三、技术实现&…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

Cesium1.95中高性能加载1500个点

一、基本方式&#xff1a; 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...

基于Uniapp开发HarmonyOS 5.0旅游应用技术实践

一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架&#xff0c;支持"一次开发&#xff0c;多端部署"&#xff0c;可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务&#xff0c;为旅游应用带来&#xf…...

MVC 数据库

MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

Nginx server_name 配置说明

Nginx 是一个高性能的反向代理和负载均衡服务器&#xff0c;其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机&#xff08;Virtual Host&#xff09;。 1. 简介 Nginx 使用 server_name 指令来确定…...

【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】

1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件&#xff08;System Property Definition File&#xff09;&#xff0c;用于声明和管理 Bluetooth 模块相…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战

在现代战争中&#xff0c;电磁频谱已成为继陆、海、空、天之后的 “第五维战场”&#xff0c;雷达作为电磁频谱领域的关键装备&#xff0c;其干扰与抗干扰能力的较量&#xff0c;直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器&#xff0c;凭借数字射…...