【C++】多态,从使用到底层。

news2024/11/23 12:19:40

文章目录

  • 前言
  • 一、多态的概念
  • 二、多太的定义和实现
    • 2.1 多太的构造条件
    • 2.2 虚函数
    • 2.3 重写(覆盖)
    • 2.4 C++11 override 和 final
    • 2.5 重载,隐藏,重写
  • 三、多态的原理
    • 3. 1虚函数表
    • 3.2 虚函数表如何完成多态的功能
    • 3.3 虚函数表存储在内存空间的那个区域?


前言


一、多态的概念

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

举例:对于买票的这个行为来说,成年人买票全价,儿童买票半价,学生买票打折,军人优先买票……不同的人虽然都是进行买票的行为,但买票过程的细节不完全相同。而为了让不同的对象,进行同一行为,产生不同的状态。我们则需要采用面向对象的三大特性之一:多态。

下面看一段简单的多态代码,后文进行解释:

class Person
{
public:
	virtual void BuyTicket() { cout << "Person::BuyTicket()" << endl; }
};
class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "Student::BuyTicket()" << endl; }
};
int main()
{
	Person pn;
	Student st;
	Person* ppn = &pn;
	ppn->BuyTicket(); //普通人买票
	ppn = &st;
	ppn->BuyTicket(); //学生买票
	return 0;
}

打印结果:
在这里插入图片描述

二、多太的定义和实现

2.1 多太的构造条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。就像上面的代码:Student继承了Person。Person对象买票全价,Student对象买票半价。

而在继承中构成多态有两个条件(牢记):

  1. 必须通过基类的指针或者引用调用虚函数
  2. 此时基类的指针或引用已经被赋值为了派生类的对象的地址,且被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行了重写

这中间出现了两个陌生的名词:虚函数和重写,因此我们首先要了解这两个词的意思是什么?

2.2 虚函数

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

比如BuyTicket()函数

class Person 
{
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3 重写(覆盖)

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

class Person 
{
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

注意:

  1. 派生类进行重写时可以不加virtual,只要派生类里的函数和基类的虚函数的返回值类型、函数名字、参数列表完全相同,编译器会自动识别为基类虚函数的重写。但一般为了可读性还是加上
  2. 返回值类型、函数名字、参数列表完全相同。其中参数列表完全相同指的是参数类型+形参的名字完全相同,对于缺省值不作要求.
    virtual void func(int val = 3) {}
    virtual void func(int val = 4) {} 它们依然构造重写
  3. 虚函数的重写是对函数体进行重写
    基类里的虚函数:virtual void func(int val = 3) {cout << "基类" << val;}
    派生类的虚函数:virtual void func(int val = 4) {cout << "派生类" << val;}
    当你调用派生类的虚函数func()时,你会发现打印的结果是派生类3,即它会使用基类的虚函数头 + 派生类的虚函数体。后文有个面试题考察了这个知识。

虚函数重写的两个特例:

  1. 协变(基类与派生类虚函数返回值类型不同)
    上面说了虚函数重写需要返回值的类型相同,但是给了一个特例:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时也可以是虚函数的重写,这种情况被称为协变
class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,因此所有的析构函数都满足函数名相同。
class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};

问题:为什么要对析构函数的名称进行处理?博客析构函数的名称为什么统一处理为destructor


知晓了这两个条件,我们来看上面的那段代码。

class Person
{
public:
	virtual void BuyTicket() { cout << "Person::BuyTicket()" << endl; }
};
class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "Student::BuyTicket()" << endl; }
};
int main()
{
	Person pn;
	Student st;
	Person* ppn = &pn;
	ppn->BuyTicket(); //普通人买票
	ppn = &st;
	ppn->BuyTicket(); //学生买票
	return 0;
}

解释:为什么ppn能调用到派生类的BuyTicket()?

  1. ppn的类型是基类person的指针满足多态的第一个条件
  2. ppn已经被赋值为派生类对象st的地址,且BuyTicket()对基类的BuyTicket进行了重写。满足第二个条件
    因此:ppn->BuyTicket会调用子类的虚函数。

2.4 C++11 override 和 final

final:修饰虚函数,表示该虚函数不能再被重写
使用场景:当你不想某个虚函数被重写时,可以加上final

在这里插入图片描述

override:帮助派生类检查是否完成重写

在这里插入图片描述

2.5 重载,隐藏,重写

在这里插入图片描述


三、多态的原理

看完上面的内容,相信你会有以下的困惑:

  1. 为什么基类的对象或指针能调用到派生类的函数?
  2. 为什么限定为基类的指针或引用,基类的对象不行吗?

要解答这些问题,我们必须要了解定义虚函数时产生的虚函数表。

3. 1虚函数表

class A
{
public:
	virtual void func1() {};
	virtual void func2() {};
	char a;
};
int main()
{	
	A A1;
	cout << sizeof(A1);
	return 0;
}

在32位的机器下,请问上面的打印结果是什么?
如果func的前面没有加virtual,结果很明显是1,但加上virtual后,结果变成了8.
那多出的内存放了些什么东西呢?
我们此时打开监视窗口:
在这里插入图片描述

发现A1中出现了一个指针_vfptr,我们猜测它代表的什么意思:v即virtual,f即function,ptr即指针,猜测它是虚函数指针。但它下面有【0】【1】,这又表明它可能是个数组。结合以下,即_vfptr是虚函数指针数组。
事实上,它还真是一个虚函数指针数组,只不过我们将这个数组叫做虚函数表,简称虚表
这时我们可以确定2个事实

  1. 多出来的内存存储了一个指针,32位下的指针是4字节,加上char a的1字节,最后在进行内存对齐,结果就是8字节。
  2. 这个指针指向的空间并不存储在对象里面,如果存储在对象里,那么对象的大小应该大于8字节。

此时我们可以画一个简图:
在这里插入图片描述


知晓了虚函数表的存在,随之而来的就有2个问题:

  1. 虚函数表是如何完成多态的功能?
  2. 虚函数表并没有存储在对象里,那它存储在什么地方?

同时加上前文提到的问题:为什么多态的构成条件要求是基类的指针或引用?

3.2 虚函数表如何完成多态的功能

在这里插入图片描述
通过监视窗口,我们可以看到:基类对象ps的虚表存储的值_vfptr[0] = 0x00bc15a5,而派生类对象st的基类的那一部分存储的虚表里的值_vfptr[0] = 0x00bc1596,二者值不相同,说明二者存储了不同的虚函数地址,一个存储的地址是person::BuyTicket, 另一个存储的是student::BuyTicket;除此之外,_vfptr都是存储在基类的那一部分。
据此,我们基本确定调用虚函数的过程如下:

  1. 基类对象和派生类对象都会创建虚函数表,基类对象的虚函数表存储基类的虚函数地址,派生类对象的虚函数表会存储派生类的虚函数地址。
  2. 当我们使用基类的对象的指针或引用去调用时,分别取指向对象的虚表去寻找。这就解释了为什么不能使用基类的对象,因为基类的对象里的虚表存储的是基类虚函数的地址,无法找到派生类的虚函数。

3.3 虚函数表存储在内存空间的那个区域?

A:栈
B:堆
C:代码段(常量区)
D:数据段(静态区)
先说答案 : 代码段(常量区)

验证如下:
在这里插入图片描述
思路:比较虚函数表内存储的地址与其他存储区域的地址进行对比,看谁更接近。
通过上面的结果可以看出:虚表的地址 与 常量区的地址最为接近。

如何提取虚表的地址:首先对象第一个存储的便是虚函数表指针,因此前4个字节(32位)存储便是虚函数表的地址(int*)&ps 即是 _vfptr的地址, 再解引用便是 _vfptr存储的地址,即虚函数表的地址
在这里插入图片描述


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

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

相关文章

服务断路器_Resilience4j超时降级

创建模块cloud-consumer-resilience4j-order80 POM引入依赖 <dependencies><!-- 引入Eureka 客户端依赖 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</a…

【Java 进阶篇】数据库介绍与MySQL详细介绍

数据库是信息科技领域中不可或缺的一部分&#xff0c;它们在我们日常生活中扮演着重要的角色&#xff0c;从手机应用到云计算&#xff0c;无处不在。在本篇博客中&#xff0c;我们将深入探讨数据库的基本概念以及MySQL这一流行的开源关系型数据库的详细信息。不需要数据库专业知…

AI写作生成器-AI写作生成器下载和用途

在当今数字化的时代&#xff0c;AI写作生成器已经成为了各行各业的创作者、企业家和学生的得力助手。这些智能工具以其强大的自然语言处理技术&#xff0c;正在解决着许多用户的写作难题。本文将深入探讨AI写作生成器&#xff0c;以及它如何在不同领域解决用户的写作问题。 147…

【算法分析与设计】递归与分治策略

目录 一、学习要点二、算法总体思想三、递归的概念例1 阶乘函数例2 Fibonacci数列例3 Ackerman函数例4 整数划分问题例5 Hanoi塔问题递归小结 四、分治法1、分治法的适用条件2、二分搜索技术3、大整数的乘法4、Strassen矩阵乘法5、棋盘覆盖6、合并排序7、快速排序8、线性时间选…

嵌入式 - 经典的有刷电机和先进的无刷电机

自从无刷直流电机诞生&#xff0c;“古老的”有刷电机就开始没落&#xff0c;但它依然是低成本应用的可靠选择&#xff0c;并且实现起来简单。 在有刷电机中&#xff0c;磁极方向的跳转是通过移动固定位置的接触点来完成的&#xff0c;该接触点在电机转子上与电触点相对连接。这…

无法从 /var/lib/rpm 打开软件包数据库

使用yum命令安装软件包时&#xff0c;报错“无法从 /var/lib/rpm 打开软件包数据库” 小白教程&#xff0c;一看就会&#xff0c;一做就成。 1.原因 是误操作导致 rpm 数据库损坏。&#xff08;/var/lib/rpm 目录下的文件被损坏&#xff09; 2.解决 当RPM 数据库发生损坏&a…

【SAP后台配置】如何通过前台屏幕字段找到对应SPRO后台路径?

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;阿里云社区专家博主&#xff0c;华为云云享专家&#xff0c;腾讯云社区认证作者&#xff0c;CSDN SAP应用技术领域优质创作者。在学习工作中&#xff0c;我通常使用偏后端的开发语言ABAP&#xff0c;SQL进行任务的完成…

C++,对象赋值与对象拷贝的区别、深浅拷贝

在C中&#xff0c;对象赋值和对象拷贝是两个不同的操作&#xff0c;它们有明显的区别&#xff1a; 1. 对象赋值&#xff08;Object Assignment&#xff09;&#xff1a; - 对象赋值是指将一个已经存在的对象的值复制给另一个已经存在的对象。这通常通过赋值操作符&#xff08;…

分布式事务处理:挑战与解决方案

在当今的大数据时代&#xff0c;数据的处理和管理变得越来越复杂。特别是在分布式系统中&#xff0c;如何保证数据的一致性和完整性&#xff0c;是一个巨大的挑战。这就引出了我们今天要探讨的主题——分布式事务处理。分布式事务处理是一种技术&#xff0c;它能够在分布式系统…

【数据结构与算法】链表的实现以及一些基本算法

目录 单选链表的基本实现 有序列表的合并&#xff08;双指针法&#xff09; 链表的反转 链表实现两数之和 判定链表是否有环 单选链表的基本实现 public class LinkedList1 {//头节点Node first;//尾节点Node last;//大小int size 0;//头插法public void addFirst(int…

Springboot 前后端分离项目使用 POI 生成并导出 Excel

在做一个 SpringBoot 前后端分离项目的时候&#xff0c;需要将数据存到 Excel中&#xff0c;用户可以下载 Excel。具体实现是采用 Apache 强大的 POI。文章最后将源码例出。 POI API 文档&#xff1a; https://poi.apache.org/apidocs/dev/index.html 步骤 导入 POI 的 maven …

前端面试:01.图中输入什么?

~~~~~~~~~~~~~ 先自行想一想&#xff0c;答案在~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~ 先自行想一想&#xff0c;答案在~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~ 先自行想一想&#xff0c;答案在~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~ 先自行想一想&#xff0c;答案在~~~~~~~~~~~~~~~~~ ~~~~~~~~…

大数据Flink(九十一):Array Expansion(数组列转行)和Table Function(自定义列转行)

文章目录 Array Expansion(数组列转行)和Table Function(自定义列转行)

Vue系列(二)之 基础语法上篇【插值,指令,过滤器,计算属性监听属性】以及购物车实现

目录 一. 插值 1.1 文本 1.2 原始HTML 1.3 属性 1.4 表达式 二. 指令 2.1 v-if/v-else-if/v-else指令 2.2 v-show指令 2.3 v-for指令 2.4 下拉框/复选框 2.5 动态参数 三. 过滤器 3.1 局部过滤器基本应用 3.2 局部过滤器串行使用 3.3 局部过滤器传参 3.4 全局过…

华为云云耀云服务器L实例评测 | 实例使用教学之简单使用:通过部署宝塔面板可视化管理华为云云耀云服务器

华为云云耀云服务器L实例评测 &#xff5c; 实例使用教学之简单使用&#xff1a;通过部署宝塔面板可视化管理华为云云耀云服务器 介绍华为云云耀云服务器 华为云云耀云服务器 &#xff08;目前已经全新升级为 华为云云耀云服务器L实例&#xff09; 华为云云耀云服务器是什么华为…

遥遥领先喊得再大声也没用了,美企领先优势越发明显,无奈的现实

遥遥领先已成为这一段时间特殊的口号&#xff0c;然而口号终究代替不了现实&#xff0c;在广受关注的两个行业--汽车和手机市场&#xff0c;都显示出真正遥遥领先的其实是美企&#xff0c;后来者已越发难以追赶了。 在手机行业&#xff0c;苹果饱受诟病&#xff0c;又是镜头进灰…

Nginx之memcached_module模块解读

目录 基本介绍 安装添加模块 模块配置指令 基本介绍 nginx的memcached_module模块可以直接从memcached服务器中读取内容后输出&#xff0c;后续的请求不再经过应用程序处理&#xff0c;如php-fpm、django&#xff0c;大大的提升动态页面的速度。nginx只负责从memcach…

STM32 UART通信协议 基础知识

通用异步收发器&#xff08;Universal Asynchronous Receiver/Transmitter)&#xff0c;通常称作UART&#xff0c;是一种串行、异步、全双工的通信协议。 在通信领域中&#xff0c;有两种数据通信方式&#xff1a;并行通信和串行通信。串口的数据传输是以串行方式进行的。串口在…

libtorch之tensor的使用

1. tensor的创建 tensor的创建有三种常用的形式&#xff0c;如下所示 ones创建一个指定维度&#xff0c;数据全为1的tensor. 例子中的维度是2维&#xff0c;5行3列。 torch::Tensor t torch::ones({5,3}); zeros创建一个指定维度&#xff0c;数据全为0的tensor&#xff0c;例子…

最新AI写作系统ChatGPT源码/支持GPT4.0+GPT联网提问/支持ai绘画Midjourney+Prompt+MJ以图生图+思维导图生成

一、AI创作系统 SparkAi系统是基于很火的GPT提问进行开发的Ai智能问答系统。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT系统&#xff1f;小编这里写一个详细图文教程吧&#x…