C++ 多态篇

文章目录

  • 1. 多态的概念和实现
    • 1.1 概念
    • 1.2 实现
      • 1.2.1 协变
      • 1.2.2 析构函数
      • 1.2.3 子类虚函数不加`virtual`
  • 2. C++11 `final`和`override`
    • 3.1 `final`
    • 3.2 `override`
  • 3. 函数重载、重写与隐藏
  • 4. 多态的原理
  • 5. 抽象类
  • 6.单继承和多继承的虚表
    • 6.1 单继承
    • 6.2 多继承
  • 7. 菱形继承的虚表(了解)
    • 7.1 菱形继承
    • 7.2 菱形虚拟继承
  • 8. 多态面试题

在这里插入图片描述

1. 多态的概念和实现


1.1 概念

面向对象语言三大特性之一:多态,意思是多种形态,当不同的对象去做同一件事时会有不同的状态/结果

1.2 实现

多态要在继承的关系中实现,需要满足2个条件:

  1. 子类必须完成父类虚函数的重写
  2. 必须通过父类的指针/引用去调用虚函数

在这里插入图片描述

virtual修饰的成员函数叫做虚函数

子类对父类虚函数的重写需要满足三同,即函数名、返回值、参数类型相同,但有三种情况例外

  1. 协变:返回值可以是其他父子类或自身的指针/引用
  2. 析构函数
  3. 子类重写父类虚函数时可以不加virtual

我们分别对这三点做解释

1.2.1 协变

父子类的返回值可以不相同,但必须是其他父子类后者自身类的指针/引用

在这里插入图片描述

1.2.2 析构函数

在继承的章节,我们说析构函数名被统一处理为destructor(),但没说具体原因,其实是要对析构函数设计多态的原因

在这里插入图片描述

在上面的场景中,ptr1和ptr2分别指向动态开辟出来的A对象和B对象;B对象中的一个成员,指向动态开辟出来的内存;将来我们要手动释放ptr1和ptr2指向的内存,于是使用delete ptr1和delete ptr2,但根据结果,没能释放掉B对象,于是导致了内存泄漏的问题

我们希望的是指针指向哪个对象,就delete哪个对象的析构,这就要求我们对析构函数实现多态调用,而多态调用要满足三同,其中函数名已经不满足了,于是编译器就将父子类的析构函数重命名为destructor(),只要我们将析构函数定义为虚函数,就满足多态了

因此,父子类的析构函数推荐加上virtual

在这里插入图片描述

1.2.3 子类虚函数不加virtual

在这里插入图片描述

可以看到,子类虚函数可以不加virtual修饰,此时BuyTicket函数也满足多态,这是因为重写的本质是对父类虚函数实现的重写;用一道例题来更好的理解这点

class A
{
public:
	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()
{
	B* p = new B;
	p->test();
	return 0;
}
// A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

在这里插入图片描述

2. C++11 finaloverride


3.1 final

如果希望一个类不能被继承,有两种方式:

  1. 将类的构造函数私有化,这是C++98的做法
  2. 将类进行final修饰,被修饰的类叫做最总类,这是C++11的做法

在这里插入图片描述

3.2 override

override关键字用于检查子类的虚函数是否完成父类虚函数的重写

在这里插入图片描述

3. 函数重载、重写与隐藏


在这里插入图片描述

4. 多态的原理


在这里插入图片描述

上述代码的结果出乎我们的意料,为什么A对象的大小是12字节?经过调试,我们发现A对象中多了一个指针_vfptr

直接给出结论,如果类中有虚函数,那么该类实例化出的对象都要有一个指针,我们把该指针称为虚表指针(在构造函数初始化列表中初始化);该指针指向一个虚表,虚表是一个函数指针数组,存放着虚函数的地址

子类对象会将父类的虚函数拷贝到自己的虚表中,并检查是否完成虚函数的重写,如果完成,将虚函数覆盖为自己的虚函数,当使用父类的引用/指针去调用虚函数时,由于发生切片,如果指向父类,去调用父类的虚函数,如果指向子类,去调用子类中父类部分的虚函数,这就是多态的原理

在这里插入图片描述

多态调用和正常调用区别:当进行编译时,编译器会检查函数是否满足多态,如果满足,那么会在执行代码时去对象的虚表中找虚函数调用,如果不满足,那么在编译时就已经确定要调用函数的地址

在这里要分清楚几个概念:

  1. 虚函数不在虚表中,虚表中存放的是虚函数的地址,虚函数存放在常量区(代码段)上
  2. 虚表存放在常量区上,下面有代码验证
  3. 虚基表是继承中存放虚基类偏移量的,用来解决菱形继承数据冗余和二义性的问题

在这里插入图片描述

5. 抽象类


在这里插入图片描述

在虚函数后加上 =0,表示该虚函数是纯虚函数,有纯虚函数的类叫做抽象类

抽象类在现实生活中没有对应的实体,因此无法实例化出对象;继承抽象类的子类同样也无法实例化出对象,这就强制要求我们完成父类虚函数的重写

在这里插入图片描述

6.单继承和多继承的虚表


6.1 单继承

在这里插入图片描述

运行上述代码,对比监视窗口和内存窗口,父类的func1和func2都继承了下来,重写了func1,于是将父类的func1覆盖;子类自身的虚函数直接往父类部分的虚表中放,监视窗口没显示func3和func4是因为vs监视窗口的设计问题,在内存中是能看到还有两个指针,这两个指针指向的就是func3和func4,用一份代码来证明

在这里插入图片描述

6.2 多继承

在这里插入图片描述

在多继承中,子类中有两个虚表指针,分别在A类的部分和B类的部分中,A类的部分继承A类的虚函数,B类的部分继承B类的虚函数,再看是否完成重写,有就拿自身的覆盖;自身的虚函数默认往先继承类的虚表中放

在这里插入图片描述

为什么要有多继承对象要有两张虚表,不能把所有的虚函数放在一张虚表中吗?有这样的场景,分别有两个父类的指针指向同一个子类对象,通过任何一个父类指针都应该调用子类的虚函数,但如果只有一张虚表,就变成只有其中一个父类指针能调用子类的虚函数

在这里插入图片描述

7. 菱形继承的虚表(了解)


7.1 菱形继承

在这里插入图片描述

在这里插入图片描述

7.2 菱形虚拟继承

在这里插入图片描述

在这里插入图片描述

8. 多态面试题


class A 
{
public:
	A(const char* s)
	{ 
		cout << s << endl; 
	}
};

class B :virtual public A
{
public:
	B(const char* s1,const char* s2) 
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};
class C :virtual public A
{
public:
	C(const char* s1, const char* s2)
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};

class D :public B, public C
{
public:
	D(const char* s1, const char* s2, const char* s3, const char* s4)
		:B(s1, s2)
		,C(s1, s3)
		,A(s1)
	{
		cout << s4 << endl;
	}
};

int main()
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

// A:class A class B class C class D 
// B:class D class B class C class A
// C:class D class C class B class A 
// D:class A class C class B class D

// 答案:A

由此可以证明:初始化列表的初始化顺序是按变量在内存中声明的顺序或类在内存中继承的顺序(先继承的类先初始化);同时同一个对象中,同一个类只会初始化一次

  1. inline函数可以是虚函数吗?

    可以,但是编译器会忽略inline属性,因为虚函数要放到虚表中

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

    不可以,静态成员函数属于整个类,无论是通过父类或子类调用,调用的都是同一个静态成员函数;静态成员函数在编译时就已经确定地址,而虚函数要到运行时去虚函数表中找

  3. 构造函数可以是虚函数吗?

    不可以,虚表指针在构造函数的初始化列表中初始化,如果构造函数时虚函数,虚表指针就无法初始化了

  4. 对象访问普通函数快还是虚函数快?

    如果是普通对象,一样快;如果是对象的指针/引用且满足多态,则调用普通函数快,调用虚函数需要在虚表中找

  5. 虚函数表在什么阶段生成?存在哪里?

    虚函数表在编译阶段生成,存放在常量区

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

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

相关文章

springboot三层架构详细讲解

目录 springBoot三层架构0.简介1.各层架构1.1 Controller层1.2 Service层1.3 ServiceImpl1.4 Mapper1.5 Entity1.6 Mapper.xml 2.各层之间的联系2.1 Controller 与 Service2.2 Service 与 ServiceImpl2.3 Service 与 Mapper2.4 Mapper 与 Mapper.xml2.5 Service 与 Entity2.6 C…

论文阅读--Simple Baselines for Image Restoration

这篇文章是 2022 ECCV 的一篇文章&#xff0c;是旷视科技的一篇文章&#xff0c;针对图像恢复任务各种网络结构进行了梳理&#xff0c;最后总结出一种非常简单却高效的网络结构&#xff0c;这个网络结构甚至不需要非线性激活函数。 文章一开始就提到&#xff0c;虽然在图像复原…

【MYSQL】事务隔离级别以及InnerDB底层实现

事务隔离级别 读未提交&#xff08;Read Uncommitted&#xff09; 允许事务读取其他事务未提交的数据&#xff0c;可能会导致脏读。 读已提交&#xff08;Read Committed&#xff09; 一个事务只能看见已经提交的事务所做的更改&#xff0c;可以避免脏读&#xff0c;但可能…

【C++:默认成员函数初始化列表】

构造函数 特点 没有返回值支持函数重载对象实例化时&#xff0c;编译器自动调用作用不是构造&#xff0c;而是初始化函数名与类名相同无参函数和全缺省的函数&#xff0c;不用传参就能调用的函数叫做默认构造函数 构造函数是一个特殊的成员函数 注&#xff1a;无参构造函数在实…

星辰宇宙动态页面vue版,超好看的前端页面。附源码与应用教程(若依)

本代码的html版本&#xff0c;来源自“山羊の前端小窝”作者&#xff0c;我对此进行了vue版本转换以及相关应用。特此与大家一起分享~ 1、直接上效果图&#xff1a; 带文字版&#xff1a;文字呼吸式缩放。 纯净版&#xff1a; 默认展示效果&#xff1a; 缩放与旋转后&#xf…

简单的手动实现spring中的自动装配案例

简简单单的实现一个spring中的自动装配和容器管理的小骚操作。 1&#xff0c;创建AutoSetBean.java 使用injectBeans静态方法&#xff0c;可以扫描指定包下的所有带MyInject注解的字段&#xff0c;如果在beans的Map中存在这个字段的实例化类&#xff0c;则执行装配。 import…

【UE5.1 角色练习】13-枪械射击——拿出与收起武器

目录 效果 步骤 一、安装射击武器 二、拿武器和收武器 效果 步骤 一、安装射击武器 1. 在虚幻商城中将“FPS Weapon Bundle”添加到工程中&#xff0c;由于我们使用的是5.1版本&#xff0c;我们可以先将该资产放入UE4工程中&#xff0c;然后迁移到5.1版本的工程 2. 打开角…

alphazero学习

AlphaGoZero是AlphaGo算法的升级版本。不需要像训练AlphaGo那样&#xff0c;不需要用人类棋局这些先验知识训练&#xff0c;用MCTS自我博弈产生实时动态产生训练样本。用MCTS来创建训练集&#xff0c;然后训练nnet建模的策略网络和价值网络。就是用MCTSPlayer产生的数据来训练和…

Spring与Quartz整合

Quartz框架是一个轻量级的任务调度框架&#xff0c;它提供了许多内置的功能&#xff0c;包括&#xff1a;支持作业的调度、集群调度、持久化、任务持久化、任务依赖、优先级、并发控制、失败重试等。同时也支持自定义作业类型和触发器类型。与Spring整合步骤如下&#xff1a; …

深入解析.[datastore@cyberfear.com].mkp勒索病毒:威胁与防范

引言 在数字化时代&#xff0c;网络安全问题日益严峻&#xff0c;其中勒索病毒&#xff08;Ransomware&#xff09;作为一种极具破坏性的恶意软件&#xff0c;严重威胁着个人用户和企业机构的数据安全。.[ datastorecyberfear.com].mkp勒索病毒便是这一领域中的一颗“毒瘤”&am…

2.5 C#视觉程序开发实例1----IO_Manager实现脉冲输出控制

2.5 C#视觉程序开发实例1----IO_Manager实现脉冲输出控制 1 目标效果视频 目标效果展示 IO_Manager 2 信号输出流程说明 为了防止线程不同步导致输出信号没有被输出&#xff0c; 尽量使用一个输出队列来进行输出的管理 3 IO_Manager中添加内容 3.0 添加两个类 1 Out_Sta…

【课程总结】Day13(下):人脸识别和MTCNN模型

前言 在上一章课程【课程总结】Day13(上):使用YOLO进行目标检测,我们了解到目标检测有两种策略,一种是以YOLO为代表的策略:特征提取→切片→分类回归;另外一种是以MTCNN为代表的策略:先图像切片→特征提取→分类和回归。因此,本章内容将深入了解MTCNN模型,包括:MTC…

Windows 11文件资源管理器选项卡的4个高级用法,肯定有你喜欢的

作为一个每天使用文件资源管理器来管理我的工作流程的人,选项卡帮助我为处于不同完成阶段的工作创建了不同的文件夹。以下是我使用选项卡提高工作效率的最佳技巧。 打开和关闭选项卡 假设你的计算机上安装了Windows 11的最新更新,请按Ctrl+E打开文件资源管理器。在我发现“…

STM32智能家居掌上屏实战:从WiFi连接到MQTT通信,打造你的家庭物联网网关

摘要: 本文深入探讨一种基于STM32的智能家居掌上屏设计方案&#xff0c;详细阐述其硬件架构、软件设计以及通信协议等关键技术细节。该方案利用WiFi构建局域网&#xff0c;实现与各类传感器、执行器的便捷交互&#xff0c;并通过TFT彩屏提供直观的控制和数据展示&#xff0c;旨…

五、保存数据到Excel、sqlite(爬虫及数据可视化)

五、保存数据到Excel、sqlite&#xff08;爬虫及数据可视化&#xff09; 1&#xff0c;保存数据到excel1.1 保存九九乘法表到excel&#xff08;1&#xff09;代码testXwlt.py&#xff08;2&#xff09;excel保存结果 1.2 爬取电影详情并保存到excel&#xff08;1&#xff09;代…

大模型周报|15 篇必读的大模型论文

大家好&#xff0c;今日必读的大模型论文来啦&#xff01; 1.谷歌推出风格感知拖放新方法 Magic Insert 来自谷歌的研究团队提出了 Magic Insert&#xff0c;用于以物理上可信的方式将用户提供的图像中的对象拖放到不同风格的目标图像中&#xff0c;同时与目标图像的风格相匹…

基于CLIP特征的多模态大模型中的视觉短板问题

【论文极速读】 基于CLIP特征的多模态大模型中的视觉短板问题 FesianXu 20240706 at Tencent WeChat search team 前言 今天读到篇CVPR 24’的论文 [1]&#xff0c;讨论了常见的多模态大模型&#xff08;大多都基于CLIP语义特征&#xff0c;以下简称为MLLM&#xff09;中的视觉…

Git错误分析

错误案例1&#xff1a; 原因&#xff1a;TortoiseGit多次安装导致&#xff0c;会记录首次安装路径&#xff0c;若安装路径改变&#xff0c;需要配置最后安装的路径。

HTML5使用<details>标签:展开/收缩信息

details 标签提供了一种替代 JavaScript 的方法&#xff0c;它主要是提供了一个展开/收缩区域。details 标签中可以使用 summary 标签从属于 details 标签&#xff0c;单击 summary 标签中的内容文字时&#xff0c;details 标签中的其他所有从属元素将会展开或收缩。语法如下&a…

Redies基础篇(一)

Redis 是一个高性能的key-value数据库。Redies支持存储的value类型相对更多&#xff0c;包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作&#xff0c;而且这些操作都是原子性的&#xff…