C++知识第四篇之多态

news2024/7/6 20:02:33

目录

    • 一.认识多态
      • 1. 多态分类
      • 2. 虚函数
        • a. 介绍
        • b. 虚函数的重写
        • c. 协变
        • d. 析构函数
      • 3. 多态构成条件
        • a. 虚函数调用多态
        • b. 析构函数多态
      • 4. C++11新特性
        • a. override
        • b. final
      • 5. 重载、重写(覆盖)、重定义(隐藏)
    • 二. 抽象类
      • 1.介绍
      • 2. 接口继承
    • 三. 多态原理
      • 1. 虚函数表
      • 2. 打印虚函数表
      • 3. 单继承关系
      • 4. 多态的原理
      • 6. 多继承关系
    • 四. 其他
      • 1.程序分析
        • one
        • two
        • three
      • 2. 常见问题
        • a. 构造函数能否成为virtual函数
        • b. static成员函数可以是virtual吗
        • c. inline函数可以是virtual吗?
        • d.虚函数表是在什么阶段生成的,存放在哪?

一.认识多态

1. 多态分类

  • 静态多态

    在程序编译期间就确定了,也称静态绑定或前期绑定。比如:函数重载

  • 动态多态

    动态多态是c++面向对象部分对于继承体系的使用。用基类的指针或引用指向不同的派生类,当该指针或引用调用某个方法时,产生的结果会根据其指向的派生类不同而不同。

    也称动态绑定或后期绑定,是程序运行期间,根据具体拿到的类型确定具体的行为。

本篇博客主要讨论动态多态

2. 虚函数

a. 介绍

virtual关键字所修饰声明的类成员函数称之为虚函数

class A
{
public:
	virtual void show();
};

成员函数show()是A类的虚函数

b. 虚函数的重写

当派生类中有和基类同型(函数的返回值类型、函数名、参数列表完全相同)的虚函数时,派生类的虚函数重写了基类的虚函数。或称为覆盖

class B :public A
{
public:
	virtual void show();
};

B类的虚函数重写了A类的虚函数show()

  • B类中的show()函数如果没加virtual修饰也是虚函数

    因为对于虚函数是接口继承,基类的虚函数被继承下来在派生类中保持着虚函数的属性。但是还是建议显示加上修饰,保持代码可读性

c. 协变

基类与派生类虚函数返回值类型可以不同,但是有约束条件

基类虚函数返回类型为某基类对象的指针或者引用时,派生类虚函数返回类型需要是对应派生类对象的指针或者引用
在这里插入图片描述

协变需要是函数的返回值是,对应的基类和派生类。并且必须是其指针或引用

d. 析构函数

对于派生类和其基类,(为了方便多态的实现),编译器对于这两个类的析构函数进行了特殊处理:编译后析构函数名统一处理成destructor()

析构派生类对象时,编译器会自己处理,先调用派生类的析构函数,然后调用其基类部分的析构函数。

class A
{
public:
	virtual ~A();
};

class B :public A
{
public:
	virtual ~B();
};

B类的析构函数,重写了A类的析构函数,

因为处理后都是destructor(),并且是虚函数(virtual),符合虚函数重写的条件

3. 多态构成条件

  • 派生类对虚函数进行了重写
  • 通过基类的指针或引用去调用虚函数

下图将分别展示虚函数的多态使用

a. 虚函数调用多态

在这里插入图片描述

通过多态调用,基类的指针或引用可以调用到派生类重写后的虚函数

b. 析构函数多态

在这里插入图片描述

对应delete的操作,可以理解成,调用到的是B类重写后的析构函数。

(对于派生类对象中基类部分析构函数的调用,其实是编译器在编译阶段自动处理了,当执行完派生类的析构函数后,就会调用其基类部分的析构函数。)


在这里插入图片描述

对于析构函数非多态而误用的操作,会导致B对象未被释放完全,有内存泄漏的危害。

  • 对于带多态性质的基类中,应该声明一个virtual析构函数。即如果class有任何virtual函数,它就应该拥有一个virtual析构函数。

4. C++11新特性

a. override

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

在这里插入图片描述

b. final

修饰虚函数,表示该虚函数不能再被重写

在这里插入图片描述

5. 重载、重写(覆盖)、重定义(隐藏)

三种关系都是在继承体系中,对于派生类与基类中同名(型)成员间的关系

在这里插入图片描述

二. 抽象类

1.介绍

虚函数的后面写上 =0 ,则这个函数声明为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)。

抽象类不能实例化出对象,派生类继承后只有重写纯虚函数,派生类才能实例化出对象(否则,派生类也为抽象类)。纯虚函数规范了派生类必须重写(否则无法实例化对象),另外纯虚函数更体现出了接口继承
在这里插入图片描述

2. 接口继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
在这里插入图片描述

上述代码执行输出的结果是B->1,由于虚函数使用的是接口继承,通过基类A的指针来调用虚函数时,是通过A的func(int val = 1)的接口调用的。

三. 多态原理

是否会疑惑,为什么使用基类的指针能调用到派生类重写的虚函数,虚函数同普通函数的差异是什么?

下面将通过vs环境下,来剖析虚函数以及多态实现的原理

1. 虚函数表

在这里插入图片描述

为什么有virtual函数的A类会比没有virtual函数的大4个字节呢?

在这里插入图片描述

通过监视窗口,我们发现拥有virtual函数的A类中,额外增加了一个void型的成员 _vfptr,是虚函数表指针**,其指向的内容存是一个指针数组,存储的是虚函数的地址。

含有virtual函数的类中都(至少)有一个虚函数表指针,该指针指向虚函数表,又称为虚表(本质是一个函数指针数组),虚表中存放的是虚函数的地址。

2. 打印虚函数表

编译器对于虚函数的调用,会去对象的虚函数表中找到对应的地址然后调用

在这里插入图片描述

通过打印结构显示,虚表中存放的确实是A类中虚函数的地址

typedef void(*_vfptr)();

void PrintVFTable(_vfptr* table, size_t n)
//void PrintVFTable(_vfptr* table)
{
	for (size_t i = 0; i < n; ++i)
	//for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]:%p -> ", i, table[i]);
		table[i]();
	}
	cout << endl;
}

vs编译器对于虚表,大多数情况下会以nullptr(00 00 00 00)作为虚表最后一个尾元素,因此有时也可以省略size_t n这个参数。(有时结尾可能是一个非法地址,可以重新生成解决访问来尝试解决)

3. 单继承关系

在这里插入图片描述
在这里插入图片描述

派生类会继承基类的虚函数表,如果派生类中有函数重写则在虚函数表对应的位置覆盖新的地址,如果派生类中有非重写的虚函数会将函数地址新增到虚表中。

4. 多态的原理

在这里插入图片描述

用B对象b传给A类指针p时,发生切片,p会指向b中A类的部分。由于访问的show是虚函数,因此编译器会去虚函数表中通过地址来调用该函数

由于B类中虚函数重写,使虚表中的存放的内容覆盖成B类的show函数地址,因此访问的也将会是B中的show了


派生类的对象赋值给基类的指针或引用时,发生的切片行为,会让基类指针或者引用指向派生类中基类的部分。

但是如果是派生类的对象赋值给基类的对象时,发生的切片只会拷贝赋值基类部分的成员变量,不会将虚表指针(_vfptr)拷贝过去,基类对象会创建自己的。

6. 多继承关系

在这里插入图片描述
在这里插入图片描述

可以发现在C类对象c中,有两个虚基表指针(_vptr),分别来着其所继承的A、B类。

如果构成重写条件,会对其两个基类的show都进行重写,并在虚表中覆盖新的地址。

在两个虚表中存放的C类的show函数地址并不相同([0]: … ),是因为编译器会做一些跳转处理,但是最终访问到的还是相同的C类重写函数。

并且对于派生类C类中新增的virtual函数,可以发现编译器将其放在第一个虚表中了(在vs下,第一个虚表地址是存储于对象空间的前4个字节中)

四. 其他

1.程序分析

one

class A
{
public:
	void test()
	{
		show();
	}
	virtual void show()
	{
		cout << "A::show" << endl;
	}
};

class B :public A
{
public:
	virtual void show()
	{
		cout << "B::show" << endl;
	}
};
int main()
{
	B b;
	b.test();

	return 0;
}

程序运行的结果是:B::show

b.test(),会到A类中的void test()函数,在函数中的show(),是通过**this->show()**而调用的。

此时我们如果理解this是什么就可以知道为什么会调到B类的Show()。首先通过b.test()时,会自动传入**&b给test()参数列表中的A* const this**,此时发生切片行为,this是指向派生类b中基类A部分的,其中虚表的对于地址已经被覆盖了,所以会调用到B::show()


two

class A
{
public:
	virtual void show()
	{
		cout << "A::show ";
	}
	void run()
	{
		cout << "A::run ";
	}
};

class B :public A
{
public:
	virtual void show()
	{
		cout << "B::show ";
	}
	void run()
	{
		cout << "B::run ";
	}
};
int main()
{
	B b;
	A& a = b;

	a.show();
	a.run();

	return 0;
}

程序运行的结果是:B::show A::run

因为void run(),非虚函数,不会构成多态的条件,编译器在编译阶段就已经处理完成a.run()会调用A类的run。

而对于virtual void show(),(动态)多态,是程序运行期间,根据虚表中的地址来调用的。


three

class A
{
public:
	A()
	{
		log();
	}

	virtual void log()
	{
		time = 0;
	}
	int time;
};

class B :public A
{
public:
	virtual void log()
	{
		time = 1;
	}
};

int main()
{
	B b;
	cout << "time: " << b.time << endl;
	return 0;
}

程序运行的结果是:time: 0

值得一提的是,该例与第一个示例a中不同的是,virtual void log()的调用是在A类的构造函数中。

是的,在派生类对象中的基类成分会在派生类自身成员被构造之前先构造完成,也可以理解成,在派生类对象b的基类构造期间,对象的类型是基类,而不是派生类。此时调用的虚函数log()还是A类的版本。


析构函数同理,因为析派生类的析构函数会先执行,执行后才执行基类部分的析构函数。

  • 在构造和析构期间不要调用virtual函数,因为这类函数不会下降到派生类

2. 常见问题

a. 构造函数能否成为virtual函数

不能,编译时会报错。

因为对象中的虚函数表指针会在构造函数初始化列表阶段进行初始化的,virtual的(构造)函数地址无法放在虚表中了

b. static成员函数可以是virtual吗

不能,编译时会报错。

因为static成员函数没有this指针,并且可以使用类型::成员函数的方式调用。而virtual函数需要通过虚函数表指针找到虚表的对于位置地址来访问。

c. inline函数可以是virtual吗?

可以,但是没意义。

成员函数如果在类中定义,编译器可能将其当做inline函数处理。当然如果显示的加上inline修饰,对于编译器而言那也只是一种建议,编译器会判断是否忽略掉inline特性。

如果有virtual,则编译器会忽略掉inline特性,因为对于inline的处理是在编译阶段到调用地方进行展开,而virtual函数这是在运行时根据虚表中所存的地址来调用。

d.虚函数表是在什么阶段生成的,存放在哪?

虚函数表在编译阶段就生成了,一般存放在代码段(常量区)。将虚函数表地址(虚函数表指针)给对象,在初始化列表阶段。

    🦀🦀观看~~

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

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

相关文章

力扣高频SQL50题(基础版)——第八天

力扣高频SQL50题(基础版)——第八天 1 游戏玩法分析 IV 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT ROUND(count(a2.f_date)/(SELECT count(distinct player_id) FROM Activity),2) fraction FROM Activity a1 INNER JOIN (SELECT player…

电容为什么可以通交流隔直流?

电容 电容是指容纳电荷的能力&#xff0c;在给定电位差下自由电荷的储藏量&#xff0c;记为C&#xff0c;国际单位是法拉&#xff08;F&#xff09;。 如上图所示&#xff0c;以平行板电容器为例&#xff0c;简单介绍下电容的基本原理。 在两块距离较近、相互平行的金属平板上…

hashMap 源码详解

1、 HashMap 底层源码解读(源码分析知识问答) 2、 什么是哈希碰撞&#xff1f;或者什么是哈希冲突&#xff1f;为什么会发生哈希冲突&#xff1f; 不同的关键字通过相同的哈希函数算出了一个相同的 哈希地址&#xff0c;这就叫做哈希冲突。 哈希冲突主要因为 哈希表底层的数组容…

App Store搜索广告如何筛词

苹果应用市场投放搜索广告&#xff0c;想要达到预期目标&#xff0c;需要长期的并且不断的优化。除了选词和出价思路需要进行决策之外&#xff0c;后期如何做好筛词和调价的优化也是非常重要的。 CPA是衡量关键词获取用户成本的指标&#xff0c;当应用的转换率较小并且CPA大于…

旗开得胜,高考:人生的一次逆袭之旅

亲爱的读者们&#xff0c;大家好&#xff01; 明天就是一年一度的高考&#xff0c;这个注定会改变莘莘学子一生的重要时刻即将到来。在这个充满期待和紧张的日子里&#xff0c;我想与你们分享一个关于我自己高考的故事&#xff0c;希望能给你们带来鼓励和启示。 那是一个阳光…

chatgpt赋能python:Python字段截取函数

Python字段截取函数 在Python编程中&#xff0c;经常会遇到需要从字符串中截取特定字段的情况&#xff0c;比如从URL中截取域名、从邮件地址中截取用户名等等。Python提供了多种方法来实现这些功能&#xff0c;其中包括字符串的切片、正则表达式、split()函数等等。在本文中&a…

关于分布式项目的补偿机制(案例总结)

【辰兮要努力】&#xff1a;hello你好我是辰兮&#xff0c;很高兴你能来阅读&#xff0c;昵称是希望自己能不断精进&#xff0c;向着优秀程序员前行&#xff01; 博客来源于项目以及编程中遇到的问题总结&#xff0c;偶尔会有读书分享&#xff0c;我会陆续更新Java前端、后台、…

走近人工智能|NLP的语言革命

前言&#xff1a; 自然语言处理&#xff08;NLP&#xff09;是指使用计算机处理和理解人类语言的技术。 文章目录 自然语言序言背景适用领域技术支持应用领域程序员如何学总结 自然语言 序言 自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09…

scratch绘制多彩五角星 中国电子学会图形化编程 少儿编程 scratch编程等级考试三级真题和答案解析2023年5月

目录 scratch绘制多彩五角星 一、题目要求 1、准备工作 2、功能实现 二、案例分析

口琴试试看

自己的第一把口琴 给初学者入门的口琴推荐指南&#xff0c;选购口琴不再困难&#xff08;2023.2更新&#xff09; 初学者入门口琴选购 (复音/半音阶/布鲁斯十孔口琴推荐&#xff09; 推荐半音&#xff1a;三种类型的口琴&#xff0c;该学哪一种&#xff1f;十孔口琴低音还面…

Android系统的Ashmem匿名共享内存子系统分析(2)- 运行时库cutils的Ashmem访问接口

声明 其实对于Android系统的Ashmem匿名共享内存系统早就有分析的想法&#xff0c;记得2019年6、7月份Mr.Deng离职期间约定一起对其进行研究的&#xff0c;但因为我个人问题没能实施这个计划&#xff0c;留下些许遗憾…文中参考了很多书籍及博客内容&#xff0c;可能涉及的比较…

带领你打开C++的神秘之门--完结篇

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;&#x1f35f;&#x1f32f;C语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f…

【耗时一月】AWS Aurora 数据库 Failover 处理方案

Aurora简述 Amazon Aurora 是亚马逊自研的云原生数据库&#xff0c;除兼容性、性能、扩展性外&#xff0c;它在设计之初&#xff0c;就以极致的可用性作为目标&#xff0c;尽可能减少故障对应用程序的影响。 Amazon Aurora 在故障恢复方面的设计理念主要包括&#xff1a; 1. 能…

Lecture 11 Contextual Representation

目录 Problems with Word Vectors/Embeddings 词向量/嵌入的问题RNN 语言模型Bidirectional RNN 双向 RNNEmbeddings from Language Models 基于语言模型的嵌入ELMo 架构Downstream Task: POS Tagging 下游任务&#xff1a;词性标注ELMo 的表现如何&#xff1f;Other Findings上…

Word控件Spire.Doc 【其他】教程(7): 使用象征符号在 Word 中绘制复选框

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

河北沃克HEGERLS仓储货架生产厂家|夹抱式伸缩货叉四向穿梭车新型物流机器人

众所周知仓库作业主要是围绕存取、搬运、拣选、输送分拣而进行的&#xff0c;而随着物流作业的多样化、复杂化&#xff0c;四向穿梭车作为新的存储技术&#xff0c;以其灵活、柔性等特点而备受瞩目。河北沃克在成功研发四向穿梭车的基础上又对其进行了产品的横向发展。目前&…

为什么会有刷掉第一名、刷掉400+的院校?

本期为大家整理热门院校-“南昌大学”的择校分析&#xff0c;这个择校分析专题会为大家结合&#xff1a;初试复试占比、复试录取规则&#xff08;是否公平&#xff09;、往年录取录取名单、招生人数、分数线、专业课难度等进行分析。希望能够帮到大家! –所有数据来源于研招网…

数据库信息速递 10年的数据库使用习惯变革,数据库的使用习惯在被改变 (译)...

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

苹果的头显,只要看一眼就行

阅读本文大概需要 1.66 分钟。 今年的 WWDC23 开发者大会&#xff0c;不少人表示 iOS 更新了个寂寞&#xff0c;但 Vision Pro 头显却意外吸引眼球&#xff0c;看来苹果工程师都忙着搞头显去了。 苹果的头显终于还是来了&#xff0c;关于它的传闻&#xff0c;似乎这几年从未间断…

【立体视觉(一)】之成像原理与镜头畸变

【立体视觉&#xff08;一&#xff09;】之成像原理与镜头畸变 一、成像原理一&#xff09;针孔模型二&#xff09;坐标系转换1. 世界坐标系到相机坐标系2. 相机坐标系到图像坐标系3. 图像坐标系到像素坐标系4. 相机坐标系到像素坐标系5. 世界坐标系到像素坐标系 二、镜头畸变一…