【C++修行之路】面向对象三大特性之多态

news/2024/5/15 1:38:07/文章来源:https://blog.csdn.net/m0_73209194/article/details/129934072

文章目录

  • 前言
  • 认识多态
  • 构成多态的必要条件
    • 虚函数的重写
    • 虚函数重写的两个例外
  • final和override
  • 重载、覆盖、隐藏
  • 抽象类
  • 多态的原理
    • 单继承
    • 多继承
      • 重写了基类的虚函数
      • 没有重写基类的虚函数
  • 菱形继承和菱形虚拟继承的虚表
  • 补充
  • 补充·继承与多态相关问题
    • inline函数可以是虚函数吗?
    • 静态成员函数可以是虚函数吗?
    • 构造函数、赋值重载可以是虚函数吗?
    • 析构函数可以是虚函数吗?
    • 内存问题
  • 结语

前言

大家好久不见,今天我们来一起学习一下c++中的多态。

认识多态

通过之前继承的学习,我们知道以下场景下,父类中的BuyTicket()会被子类中的隐藏,如果想要调用父类的,可以通过显示调用实现。

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

那如果要让其实现这样的一个场景:大人是全价,小孩就半价,应该怎么实现呢?

void show2(Person* p)
{p->BuyTicket();
}
int main()
{show2(new Person);show2(new Student);return 0;
}

我们想要让这个函数实现,如果是父类对象就打印全价,如果是子类对象就打印半价,运行程序,得到如下结果:
在这里插入图片描述

显然这不是我们想要的效果,这是我们就要引入一个叫虚函数的概念了,在父类和子类函数成员前面都加上virtual关键字,这样使两个类的同名函数构成重写的关系

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票半价" << endl;}
};void show2(Person* p)
{p->BuyTicket();
}
int main()
{show2(new Person);show2(new Student);return 0;
}

运行程序,得到了我们想要的效果:
在这里插入图片描述
这样的实现被称为多态。

构成多态的必要条件

构成多态有两个必要条件:
一、必须通过基类的指针或者引用来调用虚函数
二、被调用的必须是虚函数,并且派生类要对基类的虚函数进行重写

注意以上两个条件缺一不可,少哪一个都无法构成多态。

虚函数的重写

上面提到要对虚函数进行重写(覆盖):派生类有一个跟基类完全相同的虚函数(返回值类型、函数名字、参数列表),称子类虚函数重写了基类的虚函数。

虚函数继承相当于继承了基类函数的声明,在派生类中重写的是基类的内容,请看以下场景:
在这里插入图片描述
上述例子很好的证明了这个结论。

虚函数重写的两个例外

一、协变
派生类重写基类虚函数时,与基类虚函数返回值不同。基虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用,称为协变。

二、析构函数重写
基类的析构函数为虚函数,派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,编译器会对析构函数的名字做特殊处理,编译后的析构函数名称统一处理为destructor。

这样我们就理解为什么析构函数要定义为虚函数了,假如有以下场景:

class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;
}

在调用delete函数释放p2指向的对象资源的时候,就构不成多态,这样才能保证p1、p2正确调用析构函数。

final和override

c++对函数重写的要求比较严格,有些情况下由于疏忽会导致类似字母次序写反而无法重载,并且编译阶段不会报错,只有在程序运行时未能得到预期结果才会报错,这样会大大降低程序开发效率,因此c++提供了override和final两个关键字。
一、
final修饰虚函数,被final修饰的虚函数不可以再被重写!
二、
override检测派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错!
演示如下:
在这里插入图片描述
在这里插入图片描述

重载、覆盖、隐藏

重载

  1. 两个函数在同一个作用域
  2. 函数名相同、参数不同

重写(覆盖)

  1. 两个函数分别在基类和派生类的作用域
  2. 函数名、参数、返回值必须相同(协变例外)
  3. 两个函数必须是虚函数

重定义(隐藏)

  1. 两个函数分别在基类和派生类的作用域
  2. 函数名相同
  3. 两个基类和派生类的同名函数若不重写就是隐藏

抽象类

在虚函数的后面写上 = 0 ,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例出对象,必须重写纯虚函数,派生类才可以实例出对象,纯虚函数规范了派生类必须重写,体现出了接口继承,演示如下:
在这里插入图片描述
注意:纯虚函数可以有函数体!!!

多态的原理

单继承

先来看一个问题:在32位操作系统中运行以下代码:

//运行这段代码,会得到什么结果?
#include <iostream>
using namespace std;
class Person {
public:void func();
protected:int _a1;
};int main()
{cout << sizeof(Person) << endl;return 0;
}

结果是4,因为整形大小为4个字节,将其中的函数变成虚函数后,再计算大小

//
#include <iostream>
using namespace std;
class Person {
public:virtual void func();
protected:int _a1;
};int main()
{cout << sizeof(Person) << endl;return 0;
}

这次得到的结果是8,这是为什么呢?为什么多出来了4个字节呢?

打开监视窗口,我们会发现:
在这里插入图片描述

Person实例画的对象里多出来一个指针,这个指针指向了一个指针数组。
在这里插入图片描述
事实上,这个指针也叫虚函数表指针

多态的原理可以用这张图片来简单表示:在这里插入图片描述
通过反汇编,我们可以得知满足多态以后的函数调用,不是在编译时确定的,而是在运行后在对象中寻找的。不满足多态的函数调用在编译时就确认好了。
在这里插入图片描述

多继承

假如这个类继承了多个类的时候,是怎么完成多态的呢?我们一起来分析一下:

重写了基类的虚函数

下面程序是如何实现多态的呢?

class A
{
public:virtual void func1(){cout << " hello  A" << endl;}
};
class B
{
public:virtual void func1(){cout << " hello  B" << endl;}
};class C : public A, public B
{
public:virtual void func1(){cout << " hello C " << endl;}
protected:int _a1 =1;
};int main()
{C c;A* a = &c;B* b = &c;a->func1();b->func1();return 0;
}

我们打开监视窗口,C类中包含A、B两张虚表,但是明明在C类中重写了继承下来的方法,但两个方法的地址却不一样?
在这里插入图片描述
使用反汇编来看一下到底是为什么?
在这里插入图片描述

我们发现在上述实现多态的过程中,调用的函数应该一致,但jmp的两个地址却不同,通过反汇编我们发现b在调用的时候使用了sub命令,然后调用了func1函数,所以,这里为什么要调用sub命令呢?其实,sub ecx 本质上是修正this指针的过程,因为A先被继承,因此a指针天然就是指向对象的起始位置,但b指针指向对象的中间位置,因此要修正this指针。
在这里插入图片描述

没有重写基类的虚函数

我们知道,虚函数的函数指针要保存到一张叫虚函数表的地方,那么 派生类中没有重写父类虚函数的虚函数(新定义的虚函数)存在哪张表里呢?

我们可以通过打印虚表的方式来看一下,调试窗口有时不会显示(我也不知道为什么)

先来说一下如何打印虚表,当我们实例化对象后,在这个对象里有一个虚表指针,它指向一个函数指针数组,我们要做的就是把数组内容打印出来

将函数指针类型重定义为VFPTR,那么这个虚表指针就是一个VFPTR* 类型的指针了,我们只要在这个对象里拿出这个指针即可,这里提供一种扩展性较强的方案:

在这里插入图片描述

运行下面的测试程序

typedef void(*VFPTR)();
void printVFTable(VFPTR table[])
{for (int i = 0; table[i] != nullptr; i++){printf("第%d个虚函数的地址:0x%x->", i, table[i]);VFPTR f = table[i];f();cout << endl;}cout << endl;
}class A
{
public:virtual void func1(){cout << "hello A" << endl;}
};
class B
{
public:virtual void func1(){cout << "hello B" << endl;}
};class C : public A, public B
{
public:virtual void func1(){cout << "hello C" << endl;}virtual void Cfunc(){cout << "C func" << endl;}
protected:int _a1 =1;
};int main()
{C c;B* b = &c;printVFTable(*(VFPTR**)&c);printVFTable(*(VFPTR**)b);return 0;
}

在这里插入图片描述
可见,在派生类中新定义的虚函数其实是存在第一个继承对象的虚函数表里的。

菱形继承和菱形虚拟继承的虚表

太复杂了,占个位,以后有空再来补充,实际上非常不推荐设计出菱形继承。

补充

虚表是什么阶段生成的?
编译阶段

对象中虚表指针什么时候初始化?
构造函数的初始化列表

虚表存在哪里?

int main()
{int x = 0;static int y1 = 0;int* z1 = new int;const char* p1 = "xxxxxxxx";A a;static int y2 = 0;int* z2 = new int;const char* p2 = "xxxxxxxx";printf("栈对象:%d\n", &x);printf("静态区对象:%d\n", &y1);printf("静态区对象:%d\n", &y2);printf("堆对象:%d\n", &z1);printf("堆对象:%d\n", &z2);printf("常量区对象:%d\n", &p1);printf("常量区对象:%d\n", &p2);printf("虚表%d\n", *(VFPTR**)&a);return 0;
}

在这里插入图片描述
根据上述代码,再加上虚表是所有对象共用一份的数据,因此我们推测虚表应该在静态区。

补充·继承与多态相关问题

inline函数可以是虚函数吗?

可以,但是不要忘了inline只是给编译器一个建议,采不采纳这个建议取决于编译器,由于内联函数要在编译阶段就展开,不会进入符号表等,而虚函数地址要存放到虚表中,这个行为是在运行时进行的,所以编译器就会忽略掉inline属性。

静态成员函数可以是虚函数吗?

虚函数的地址存放在虚表里,而static静态函数不被某一个对象拥有,而被一个类拥有,因此静态成员函数没有this指针,但虚表指针在每一个实例化的对象里,也就是说虚函数调用会使用this指针,所以说静态成员函数不会构成多态,也就不可以是虚函数。

构造函数、赋值重载可以是虚函数吗?

不可以,在对象创建时,编译器对虚表指针初始化,先让虚表指针指向父类的虚函数表,父类构造完后,子类的虚表指针再指向自己的虚函数表。

虚函数表是在构造函数的 初始化列表 初始化的!

构造时就调用虚函数,那么调用构造函数就要去找虚表指针,虚表指针在构造函数初始化列表才初始化,这是一个先有蛋还是先有鸡的问题

同样的道理,赋值重载可不可以设计为虚函数呢?
语法上是可以设置为虚函数的,但是极其不推荐这样做,我们回到虚函数的本质,我们设置虚函数本身就是为了重写,但对于赋值重载来讲,我们是想先赋值父类再赋值子类而不是多态的 只执行子类不执行父类。

析构函数可以是虚函数吗?

可以,非常推荐将析构函数设计为虚函数,上面我们也提到过,编译器会把两个类的析构函数统一处理为同名函数destructor() 这样我们在释放子类指针的时候,会隐藏父类的析构函数,我们没有办法完全释放空间,演示如下:

class Base
{
public:~Base(){cout << "~Base" << endl;}
};class Derive : public Base
{
public:~Derive(){cout << "~Derive" << endl;}
};int main()
{Base* pd = new Derive;delete pd;return 0;
}

因为名字相同,默认构成了重写,基类的指针就会去调用基类的析构,这样是不符合预期的,因此我推荐将析构函数设计为虚函数,这样二者构成多态,此时就可以正确的释放空间了。

内存问题

运行下述代码报错:

class Base
{
public:void func1(){}
};class Derive : public Base
{
public:virtual void func2(){}
};int main()
{Base* ptr = new Derive;delete ptr;return 0;
}

其实原因就是在基类指针对子类切片的时候,父类没有虚表指针,但子类有虚表指针,这样会导致父类部分的前4/8个字节被非法访问了,所以在delete的时候会报错

结语

到这里,本篇文章就结束了,希望对你理解多态有所帮助,这篇文章花费了我大量时间,如果对你学习有所帮助,请给我一个三连+关注,我们下次再见。

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

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

相关文章

2.半导体二极管

1.半导体二极管二极管的组成二极管和PN结伏安特性的区别二极管的伏安特性及电流方程为什么反向饱和电流越小&#xff0c;单向导电性能越强。二极管的等效电路二极管的主要参数稳压二极管&#xff08;又称齐纳二极管或反向击穿二极管&#xff09;稳压二极管与普通二极管的区别其…

深入理解MCU启动原理

前面写了一篇 STM32的完整启动流程分析&#xff0c;但是感觉有些地方没有完全理明白&#xff0c;因此对不清楚的地方又做了一些总结。 1. MCU最开始一启动后去哪里读代码&#xff1f; CPU上电启动后被设计为去地址0x00000000位置处读取代码&#xff1b;首先会连续读取两个字&…

Linux ELK日志分析系统 | logstash日志收集 | elasticsearch 搜索引擎 | kibana 可视化平台 | 架构搭建 | 超详细

Linux ELK日志分析系统 | logstash日志收集 | elasticsearch 搜索引擎 | kibana 可视化平台 | 架构搭建 | 超详细ELK 日志分析系统1.日志服务器2.ELK 日志分析系统3 日志处理步骤一、 Elasticsearch 介绍1.1概述1.2核心概念二、Kibana 介绍三 ELK架构搭建3.1 配置要求3.2 安装 …

Java中Stream的应用

Stream简介 Java 8 版本新增的Stream&#xff0c;配合同版本出现的Lambda &#xff0c;给我们操作集合&#xff08;Collection&#xff09;提供了极大的便利。 Stream可以由数组或集合创建&#xff0c;对流的操作分为两种&#xff1a; 中间操作&#xff1a;每次返回一个新的…

DALL·E:OpenAI第一代文本生成图片模型

1 简介 本文根据openAI 2021年2月的《Zero-Shot Text-to-Image Generation》翻译总结的。原文详见https://arxiv.org/pdf/2102.12092v1.pdf。 DALLE : 论文中没看到这个名字&#xff0c;可能是后起的吧。 DALLE有120亿参数&#xff0c;基于自回归transformer&#xff0c;在2…

Spring 01 -项目管理框架Spring入门

本部分理解原理就好 Spring入门1 Spring引入1.1 原生web开发中存在的问题2 Spring2.1 Spring的概念2.2 Spring 作用2.3 Spring的组成2.4 spring的IOC底层实现原理3 Spring快速入门3.1 引入spring依赖3.2 spring的配置文件3.3 测试四、Spring的开发细节4.1 BeanFactory的类间关系…

android的system_server进程的启动

android的system_server进程的启动 android的system_server进程的启动的简单介绍 system_server是Zygote的fork的第一个Java进程相当于它的大儿子&#xff0c;这个进程非常重要的&#xff0c;这里这个进程提供了很多系统线程&#xff0c;提供了所有的核心的系统服务。比如&am…

mysql与redis区别

一、.redis和mysql的区别总结 &#xff08;1&#xff09;类型上 从类型上来说&#xff0c;mysql是关系型数据库&#xff0c;redis是缓存数据库 &#xff08;2&#xff09;作用上 mysql用于持久化的存储数据到硬盘&#xff0c;功能强大&#xff0c;但是速度较慢 redis用于存储使…

SQL基础查选和条件查选

写完毕业论文&#xff0c;终于有空复习sql了&#xff0c;继续学习之旅~下次不能和上次一样&#xff0c;简单的sql语句都忘记了。 1.查询结果去重 关键字 distinct select distinct university from user_profile; 2. 将查询后的列重新命名 关键字 as 关键字limit…

Linux kernel 编译 exfat.ko ntfs.ko 来支持exFat 和 NTFS 分区

项目需求想让设备支持 exFat 和 NTFS 的文件格式. 默认的内核是不支持的,因为内核要限定1.5M之内, 所以很多东西都裁剪掉了. 而且不是所有项目都有这个需求,所以就需要编译为 ko ,按需加载; 而不是才去built-in的方式. 在如下的选项找到对应的配置. 至于如何找到, 我推荐2种方式…

LE AUDIO快速了解

有BIS和CIS两种 BIS是广播的&#xff0c;不需要连接&#xff0c;只需要监听 CIS要建立连接的&#xff0c;除了ACL链路&#xff0c;还需要建立CIS链路 BIS部分 也没啥要看的&#xff0c;只需要记住3个指令就可以了 主要是HCI的3个指令 2068 206b 206e 这3个指令即可 206…

Nacos安全性探究

Nacos怎么做安全校验的&#xff1f; 以下使用nacos2.x 如上图所示&#xff0c; 可以直接访问Nacos的接口来获取用户列表。这说明Nacos的接口被爆露&#xff0c;任何情况下都可以访问&#xff0c;因此安全性得不到保障。 Nacos 使用 spring security 作为安全框架。spring sec…

Baumer工业相机堡盟工业相机如何通过BGAPI SDK获取每张图像的微秒时间和FrameID(C#)

BGAPI SDK获取图像微秒级时间和FrameID Baumer工业相机Baumer工业相机FrameID技术背景一、FrameID是什么&#xff1f;二、使用BGAPI SDK获取图像微秒时间和FrameID步骤 1.获取SDK图像微秒级时间2.获取SDK图像FrameIDBaumer工业相机使用微秒级时间和FrameID保存的用处Baumer工业…

混淆矩阵Confusion Matrix(resnet34 基于 CIFAR10)

目录 1. Confusion Matrix 2. 其他的性能指标 3. example 4. 代码实现混淆矩阵 5. 测试&#xff0c;计算混淆矩阵 6. show 7. 代码 1. Confusion Matrix 混淆矩阵可以将真实标签和预测标签的结果以矩阵的形式表示出来&#xff0c;相比于之前计算的正确率acc更加的直观…

jenkins打包发布前端项目

1.配置前端nodejs打包环境 1.1安装nodejs插件 1.2配置jenkins nodejs环境 2.下载git插件(使用此插件配置通过gitlab标签拉取项目) 3.创建一个自由风格的发布项目 4.配置项目构建流程 4.1添加钉钉告警 4.2配置参数化构建 4.3配置源码管理为git拉取项目 4.4配置构建环境 4.5配置…

transform属性

CSS transform属性允许对某一个元素进行某些形变, 包括旋转&#xff0c;缩放&#xff0c;倾斜或平移等。 注意事项&#xff0c;并非所有的盒子都可以进行transform的转换,transform对于行内级非替换元素是无效的,比如对span、a元素等 常见的函数transform function有&#xf…

算法笔记:匈牙利算法

1 二部图&#xff08;二分图&#xff09; 二分图&#xff08;Bipartite graph&#xff09;是一类特殊的图&#xff0c;它可以被划分为两个部分&#xff0c;每个部分内的点互不相连。 匈牙利算法主要用来解决两个问题&#xff1a;求二分图的最大匹配数和最小点覆盖数。 2 最大匹…

[C++笔记]初步了解STL,string,迭代器

STL简介 STL(standard template libaray-标准模板库)&#xff1a; 是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个包含数据结构与算法的软件框架。 是一套功能强大的 C 模板类&#xff0c;提供了通用的模板类和函数&#xff0c;这些模板…

STM32开发(十二)STM32F103 功能应用 —— NTC 温度采集

文章目录一、基础知识点二、开发环境三、STM32CubeMX相关配置四、Vscode代码讲解&#xff08;过程中相关问题点在第五点中做解释说明&#xff09;五、知识点补充六、结果演示一、基础知识点 了解STM32 片内资源ADC。本实验是基于STM32F103开发 实现 NTC温度采集。 NTC温度采集…

3年外包离奇被裁,痛定思痛24K上岸字节跳动....

三年前&#xff0c;我刚刚从大学毕业&#xff0c;来到了一家外包公司工作。这份工作对于我来说是个好的起点&#xff0c;因为它让我接触到了真正的企业项目和实际的开发流程。但是&#xff0c;随着时间的流逝&#xff0c;我发现这份工作并没有给我带来足够的成长和挑战。 三年…