【C++进阶十】多态深度剖析

【C++进阶十】多态深度剖析

  • 1.多态的概念及条件
  • 2.虚函数的重写
  • 3.重写、重定义、重载区别
  • 4.C++11新增的override 和final
  • 5.抽象类
  • 6.虚表指针和虚表
    • 6.1什么是虚表指针
    • 6.2指向谁调用谁,传父类调用父类,传子类调用子类
  • 7.多态的原理
  • 8.单继承的虚表状态
  • 9.多继承的虚表状态
  • 10.菱形继承
  • 11.菱形虚继承

1.多态的概念及条件

继承是实现多态的前提
通俗来说,多态就是多种状态,父子对象完成相同任务会产生不同的结果
例如:成年人买火车票是全价票,学生买票是折扣票

在继承中构成多态要有两个条件:

  1. 必须通过父类的指针引用去调用
  2. 被调用的函数必须是虚函数,且完成虚函数的重写

在这里插入图片描述

2.虚函数的重写

什么是虚函数:

被virtual修饰的类成员函数称为虚函数

什么是虚函数的重写(覆盖):

三同:父子虚函数的函数名返回值类型参数相同
(参数的缺省值可以不同)

传递不同的对象调用不同的函数
传父类调用的是父类的虚函数
传子类调用的是子类的虚函数

虚函数重写的例外:

  1. 子类的虚函数可以不加virtual
    重写体现了接口继承:子类把函数的声明继承下来,重写的是函数的实现,所以不写virtual也可以满足多态的条件
    在这里插入图片描述

  2. 协变:子类与父类的虚函数返回值类型不同,但必须满足父类虚函数返回父类对象的指针或引用,子类虚函数返回子类对象的指针或引用
    在这里插入图片描述
    这个父子类指针也可以是自己的父子类,也可以是指其他类型的父子类:
    在这里插入图片描述

子类的虚函数可以不加virtual可以防止析构函数出错:
正确使用析构函数:在这里插入图片描述
父类和子类的虚函数的析构函数函数名并不相同却依然构成虚函数的重写,因为析构函数在多态中会被编译器修改成同一个名字
为什么会变成同一个名字:析构函数会因为父子类关系,在子类调用析构后会自动调用父类的析构,如果父子的析构函数不是虚函数,调用析构时就不会调用子类的析构,即使我们指向了子类
在这里插入图片描述
为什么没有调用到子类:因为delete的内部构成是:析构函数和operator delete(),而operator delete 有一个特点就是调用delete的指针是什么类型的,就会调用什么类型的析构函数,上图的两个指针p1和p2都是父类person类型,所以就都调用了父类person的析构函数,没有调动子类的析构函数
实际使用结果如下:
在这里插入图片描述
所以想要指向父类调用父类析构,指向子类调用子类析构,就需要编译器把析构函数的名字进行了统一,满足了虚函数重写的三同:父子虚函数的函数名返回值类型参数相同
子类可以不写virtual,只要父类加上了virtual就可以进行虚函数的重写,但是不太建议
在这里插入图片描述

3.重写、重定义、重载区别

在这里插入图片描述

4.C++11新增的override 和final

overrride:检查子类虚函数是否重写了父类的某个虚函数,如果没有重写编译报错
在这里插入图片描述

final:修饰虚函数,表示该虚函数不能被重写
在这里插入图片描述

5.抽象类

在虚函数后面写上=0,这个虚函数被称为纯虚函数,包含纯虚函数的类叫做抽象类,抽象类不能实例化对象
在这里插入图片描述
若创建一个子类继承抽象类,则该子类也包含纯虚函数,子类也会变成抽象类,所以子类创建对象也会报错
在这里插入图片描述

6.虚表指针和虚表

6.1什么是虚表指针

sizeof(Base) 大小是多少?
在这里插入图片描述
以结构体的内存对齐考虑,在32位机器下,大小应为8字节,但是实际上为12字节

在这里插入图片描述
_vfptr代表虚函数表指针
加上虚表指针,内存对齐后字节大小为12

6.2指向谁调用谁,传父类调用父类,传子类调用子类

  1. 父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象
  2. 通过虚表指针和虚函数表就可以实现多态性,即可以在运行时确定应该调用哪个类的虚函数
  3. 虚表指针是类级别的,而不是函数级别的
  4. 每个类只有一个虚表指针,指向其对应的虚函数表,但是多继承的时候,就会可能有多张虚表
  5. 每一个类中的虚函数在虚表中都有地址

在这里插入图片描述
如图所示, 图中父子类各自的虚表指针指向的虚表以及虚表内部的地址并不相同,这证明了父类的虚表指针和子类的虚表指针指向的虚表并不是同一个

7.多态的原理

class Person
{
public:virtual void BuyTicket(){cout << "成人:全价票" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket() {cout << "学生:半价票" << endl;}
};void func(Person* p)
{p->BuyTicket();//父类指针指向父类对象A(或子类对象B)
}int main()
{Person A;func(&A);//传父类对象Student B;func(&B);//传子类对象return 0;
}

父类对象内的虚函数放进了父类的虚表,子类对象继承了父类的同时也继承了父类的虚表,如果子类有虚函数的重写,那么父类的虚表内有父类的虚函数,子类的虚表内有子类的虚函数(重写后的虚函数)
指向谁调用谁,父类指针指向父类对象,则从父类的虚表中找到父类的虚函数;父类指针指向子类对象,则从子类的虚表中找到子类的虚函数,因此产生了指向父类调用父类,指向子类调用子类的现象

不是多态则是在编译时就是已经指向了某个地方,而不是因为指向谁调用了谁
多态的本质在底层看来就是在虚表内寻找虚函数

虚函数和普通函数一样都是存在于代码段中的,而不是存在虚表内部的,虚表内部仅仅储存了虚函数的地址,而虚表储存在常量区

虚函数表 本质是一个虚函数指针数组
子类的虚表是由父类的虚表拷贝过来的,再向其中填入新的地址,所以造成覆盖
为什么父类对象不可以实现多态,必须是父类的指针或引用?
若为父类对象,就会把子类中属于父类的那一部分拷贝给父类,有可能把子类的虚表也拷贝给父类,若拷贝成功,则父类对象的虚表就不知道是父类的虚表还是子类的虚表了
若为指针或者引用,将子类中属于父类那一部分切出来,使指针指向属于父类那一部分,或者作为属于父类那一部分的别名,子类的虚表还是子类的

8.单继承的虚表状态

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}virtual void func2(){cout << "A::func2" << endl;}
};class B : public A
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func3(){cout << "B::func3" << endl;}virtual void func4(){cout << "B::func4" << endl;}
};int main()
{A a;B b;return 0;
}

在这里插入图片描述
可以看到子类的虚表不正常,因为每个类中的虚函数在虚表中都要出现,而子类虚表里少了两个虚函数的地址,func1是重写的,func2是继承的(没有重写),而func3和func4不见了,子类自己的虚函数消失了
这里可以解释为一种bug:是Visual Studio监视窗口的bug
也可也理解为:子类的虚表实际上是拷贝了父类的虚表,重写的部分进行覆盖,没有重写的部分原模原样地拷贝,可以说是子类的虚表被隐藏了

9.多继承的虚表状态

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}virtual void func2(){cout << "A::func2" << endl;}
private:int a;
};class B
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func2(){cout << "B::func2" << endl;}
private:int b;
};class C : public A, public B
{
public:virtual void func1(){cout << "C::func1" << endl;}virtual void func3(){cout << "C::func3" << endl;}
private:int c;
};int main()
{C temp;return 0;
}

在这里插入图片描述
在这里插入图片描述
C由两个部分构成,第一个是继承了A, 一个类中只有一个虚表指针,所以A内部有一个虚表指针和一个int类型的对象a,第二个是继承了B, 一个类中只有一个虚表指针,所以B内部有一个虚表指针和一个int类型的对象b
C类的虚函数func3放到了继承的第一个类A的虚表内部
所以C中有两个虚表指针,同时这里的两个虚表指针不能合二为一,这关系于切片问题
所以多继承内部可能会有多个虚表指针

10.菱形继承

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}
private:int a;
};class B : public A
{
public:virtual void func2(){cout << "B::func2" << endl;}
private:int b;
};class C : public A
{
public:virtual void func3(){cout << "C::func3" << endl;}
private:int c;
};class D : public B,public C
{
public:virtual void func4(){cout << "D::func4" << endl;}
private:int a;
};int main()
{D temp;return 0;
}

菱形继承和多继承没区别,同时func4放入了B的虚表内部
D类对象temp有两张虚表,分别是B和C的虚表

11.菱形虚继承

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1" << endl;}int _b = 2;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1" << endl;}int _c = 3;
};class D : public B,public C
{
public:virtual void func1(){cout << "D::func1" << endl;}virtual void func2(){cout << "D::func2" << endl;}int _d = 4;
};int main()
{D temp;return 0;
}

在这里插入图片描述

A的虚表由B、C共享
D自己新增的func2需要虚表,但D对象中的B没有的虚表
虚基表存储偏移量,帮助B、C找到A

注意:上述的B、C没有新增额外虚函数,如果有新增,则D的虚表消失,B和C各有一张虚表,D的虚函数放入B的虚表内,共计三张虚表

class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func3(){cout << "B::func3" << endl;}int _b = 2;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1" << endl;}virtual void func4(){cout << "C::func4" << endl;}int _c = 3;
};class D : public B,public C
{
public:virtual void func1(){cout << "D::func1" << endl;}virtual void func2(){cout << "D::func2" << endl;}int _d = 4;
};int main()
{D temp;return 0;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79513.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面向网络安全的开源 大模型-Foundation-Sec-8B

1. Foundation-Sec-8B 整体介绍 Foundation-Sec-8B 是一个专注于网络安全领域的大型语言模型 (LLM),由思科的基础人工智能团队 (Foundation AI) 开发 。它基于 Llama 3.1-8B 架构构建,并通过在一个精心策划和整理的网络安全专业语料库上进行持续预训练而得到增强 。该模型旨在…

Python爬虫的基础用法

Python爬虫的基础用法 python爬虫一般通过第三方库进行完成 导入第三方库&#xff08;如import requests &#xff09; requests用于处理http协议请求的第三方库,用python解释器中查看是否有这个库&#xff0c;没有点击安装获取网站url&#xff08;url一定要解析正确&#xf…

WHAT - Tailwind CSS + Antd = MetisUI组件库

文章目录 Tailwind 和 Antd 组件库MetisUI 组件库 Tailwind 和 Antd 组件库 在 WHAT - Tailwind 样式方案&#xff08;不写任何自定义样式&#xff09; 中我们介绍了 Tailwind&#xff0c;至于 Antd 组件库&#xff0c;我们应该都耳熟能详&#xff0c;官网地址&#xff1a;htt…

Day 4:牛客周赛Round 91

好久没写了&#xff0c;问题还蛮多的。听说这次是苯环哥哥出题 F题 小苯的因子查询 思路 考虑求因子个数&#xff0c;用质因数分解&#xff1b;奇数因子只需要去掉质数为2的情况&#xff0c;用除法。 这里有个比较妙的细节是&#xff0c;提前处理出数字x的最小质因数&#xff0…

使用直觉理解不等式

问题是这个&#xff1a; 题目 探究 ∣ max ⁡ b { q 1 ( z , b ) } − max ⁡ b { q 2 ( z , b ) } ∣ ≤ max ⁡ b ∣ q 1 ( z , b ) − q 2 ( z , b ) ∣ |\max_b\{q_1(z,b)\}-\max_b\{q_2(z,b)\}|\le\max_b|q_1(z,b)-q_2(z,b)| ∣maxb​{q1​(z,b)}−maxb​{q2​(z,b)}∣≤…

恶心的win11更新DIY 设置win11更新为100年

‌打开注册表编辑器‌&#xff1a;按下Win R键&#xff0c;输入regedit&#xff0c;然后按回车打开注册表编辑器。‌12‌导航到指定路径‌&#xff1a;在注册表编辑器中&#xff0c;依次展开HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings‌新建DWORD值‌&…

嵌入式驱动学习

时钟 定义 周期型的0、1信号 时钟信号由“心脏”时钟源产生&#xff0c;通过“动脉”时钟树传播到整个芯片中。 SYSCLK系统时钟&#xff0c;由HSI、HSE、PLLCLK三选一。 HCLK是AHB总线时钟&#xff0c; PCLK是APB总线时钟。 使用某个外设&#xff0c;必须要先使能该外设时钟系统…

Java:从入门到精通,你的编程之旅

Java&#xff0c;一门历久弥新的编程语言&#xff0c;自诞生以来就以其跨平台性、面向对象、稳定性和安全性等特性&#xff0c;在企业级应用开发领域占据着举足轻重的地位。无论你是初学者还是经验丰富的开发者&#xff0c;Java 都能为你提供强大的工具和广阔的舞台。 为什么选…

Linux:深入理解数据链路层

实际上一台主机中&#xff0c;报文并没有通过网络层直接发送出去&#xff0c;而是交给了自己的下一层协议——数据链路层&#xff01;&#xff01; 一、理解数据链路层 网络层交付给链路层之前&#xff0c;会先做决策再行动&#xff08;会先查一下路由表&#xff0c;看看目标网…

Python基本语法(类和实例)

类和实例 类和对象是面向对象编程的两个主要方面。类创建一个新类型&#xff0c;而对象是这个 类的实例&#xff0c;类使用class关键字创建。类的域和方法被列在一个缩进块中&#xff0c;一般函数 也可以被叫作方法。 &#xff08;1&#xff09;类的变量&#xff1a;甴一个类…

2025 年如何使用 Pycharm、Vscode 进行树莓派 Respberry Pi Pico 编程开发详细教程(更新中)

micropython 概述 micropython 官方网站&#xff1a;https://www.micropython.org/ 安装 Micropython 支持固件 树莓派 Pico 安装 Micropython 支持固件 下载地址&#xff1a;https://www.raspberrypi.com/documentation/microcontrollers/ 选择 MicroPython 下载 RPI_PIC…

flink rocksdb状态说明

文章目录 1.默认情况2.flink中的状态3.RocksDB4.对比情况5.使用6.RocksDB架构7.参考文章8.总结提示:以下主要考虑flink 状态永久存储 rocksdb情况,做一些简单说明 1.默认情况 当flink使用rocksdb存储状态时。无论是永久存储还是临时存储都可能会落盘写文件(如果没有配置存储…

安装SDL和FFmpeg

1、先记录SDL 这玩意还是有一点讲究的 具体步骤&#xff1a; 下载 SDL包&#xff1a; 链接&#xff1a;https://www.libsdl.org/release/SDL2-2.0.14.tar.gz 可以用迅雷&#xff0c;下载完之后&#xff0c; 解压&#xff1a; tar -zxvf SDL2-2.0.14.tar.gz进入安装目录 cd …

2022年408真题及答案

2022年计算机408真题 2022年计算机408答案 2022 408真题下载链接 2022 408答案下载链接

Spring AI聊天模型API:轻松构建智能聊天交互

Spring AI聊天模型API&#xff1a;轻松构建智能聊天交互 前言 在当今数字化时代&#xff0c;智能聊天功能已成为众多应用程序提升用户体验、增强交互性的关键要素。Spring AI的聊天模型API为开发者提供了一条便捷通道&#xff0c;能够将强大的AI驱动的聊天完成功能无缝集成到…

Softmax回归与单层感知机对比

(1) 输出形式 Softmax回归 输出是一个概率分布&#xff0c;通过Softmax函数将线性得分转换为概率&#xff1a; 其中 KK 是类别数&#xff0c;模型同时计算所有类别的概率。 单层感知机 输出是二分类的硬决策&#xff08;如0/1或1&#xff09;&#xff1a; 无概率解释&#x…

【React】Hooks 解锁外部状态安全订阅 useSyncExternalStore 应用与最佳实践

一、背景 useSyncExternalStore 是 React 18 引入的一个 Hook&#xff1b;用于从外部存储&#xff08;例如状态管理库、浏览器 API 等&#xff09;获取状态并在组件中同步显示。这对于需要跟踪外部状态的应用非常有用。 二、场景 订阅外部 store 例如(redux,mobx,Zustand,jo…

Dify框架面试内容整理-如何评估基于Dify开发的AI应用的效果?

评估基于 Dify 开发的 AI 应用效果,需要从 用户体验、技术性能 与 业务价值 三个层面综合衡量。以下是详细的评估框架,涵盖三个关键点: 用户反馈与满意度

Linux 系统下VS Code python环境配置!

Anaconda安装&#xff1a; 在 Linux 系统中安装下载好的 Anaconda3-2024.10-1-Linux-x86_64.sh&#xff0c;可按以下步骤操作&#xff1a; 1. 赋予安装脚本执行权限 打开终端&#xff0c;切换到安装包所在目录&#xff08;假设在 software 文件夹中&#xff09;&#xff0c;…

项目实战-基于信号处理与SVM机器学习的声音情感识别系统

目录 一.背景描述 二.理论部分 三.程序设计 编程思路 流程图 1.信号部分 创建数据 generate_samples.py 头文件 生成函数 generate_emotion_sample 传入参数 存储路径 生成参数 创建基础正弦波信号 调制基础正弦波 对于愤怒可以增加噪声 归一化信号 存储 主函…