C++多态 学习

news2024/11/17 13:50:09

目录

一、多态的概念

二、多态的实现

三、纯虚函数和多态类

四、多态的原理


一、多态的概念

        多态:多态分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态主要是我们之前学过的函数重载和函数模板,他们在传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫做编译时多态,是因为实参传给形参的参数匹配是在编译时完成的。

        运行时多态,就是去完成某个行为,可以传不同的对象完成不同的行为,就会达到多种的形态。例如买票行为,当普通人买票时,是全价买票;而当学生买票时,是半价买票。

二、多态的实现

1. 多态是一个继承关系下的类对象,去调用同意函数,产生了不同的行为。例如Student类继承了Person类,当我们调用同一个BuyTickets函数,Person对象全价买票,Student对象半价买票。

1.1 实现多态的条件

  • 必须是基类的指针或引用
  • 被调的函数必须是虚函数

1.2 虚函数

类成员函数前加 virtual 修饰,那么这个成员函数被称为虚函数。注意:非成员函数不能加virtual修饰。

class Base
{
public:
	virtual void func()
	{
		cout << "Base -> func()" << endl;
	}
};

这里的 virtual void func() 就是Base内的一个虚函数。

1.3 虚函数的重写/覆盖

派生类中有一个跟基类完全相同的虚函数(这里的完全相同指的是 返回值类型函数名参数列表完全相同),称为派生类的虚函数重写了基类的虚函数。

注意:在重写基类的虚函数时,对于派生类,我们其实可以不加 virtual 关键字,因为继承基类后,虚函数也被继承了下来,在派生类中依旧保持虚函数的属性,但是这种写法看起来不是很一目了然,但是如果不写的话,也是构成重写/覆盖的。

class Base
{
public:
	virtual void func1()
	{
		cout << "Base -> func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base -> func2()" << endl;
	}
	void func3()
	{
		cout << "Base -> func3()" << endl;
	}
};

class Derive : public Base
{
public:
	virtual void func1()
	{
		cout << "Derive -> func1()" << endl;
	}
	void func2()
	{
		cout << "Derive -> func2()" << endl;
	}
	void func3()
	{
		cout << "Derive -> func3()" << endl;
	}
};

上面Derive继承了Base,按照我们刚刚提到的,我们就可以得出结论,派生类Derive中的func1 和 func2是构成重写/覆盖的,而func3时不构成的。

1.4 多态场景的题目

以下程序输出的结果是什么(B)

A: A->0   B: B->1   C: A->1   D: B->0   E: 编译出错   F: 以上都不正确

class A
{
public:
	virtual void func(int val = 1)
	{
		cout << "A->" << val << endl;
	}
	virtual void test()
	{
		func();
	}
};

class B : public A
{
public:
	void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};
int main()
{
	B* ptr = new B;

	ptr->test();
	return 0;
}

答案为什么是B呢?首先可以肯定的是,B中的func函数已经对A中的func函数实现了重写/覆盖。但是由于虚函数表的存在,当我们后面讲到虚表的时候,再来看这个问题,会更加的清楚。

三、纯虚函数和多态类

在虚函数的后面写上 =0 ,则这个函数称为纯虚函数,纯虚函数不需要定义实现,只要声明即可。包含纯虚函数的类称为抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,否则实例不出来对象。例如:

class Base
{
public:
	virtual void func() = 0;
};

class Derive : public Base
{
public:
	virtual void func()
	{
		cout << "Derive -> func()" << endl;//重写func()
	}
};

int main()
{
	Base b;  //错误,因为Base是抽象类,不能实例化出对象
	Derive d; //正确,因为Derive重写了func()
	return 0;
}

四、多态的原理

1. 虚函数表指针

下面编译在32位程序的运行结果是什么 ( D )

A: 编译报错    B: 运行报错   C: 8    D: 12

class Base
{
public:
	virtual void func()
	{
		cout << "Base -> func()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 's';
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;

	return 0;
}

这是为什么呢,按内存对齐来看,一个int 和 一个char,算下来不应该是8吗?这是因为,除了_b和_ch成员,还多了一个_vfptr放在对象的前面(有的平台可能放在后面),对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少有一个虚函数表指针,因为一个类所有的虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也称为虚表。如下图:

2. 多态的原理

//类省略未写

void Func(Person* p)
{
	p->BuyTickets();
}

int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}

从底层的角度Func函数中p->BuyTickets(),ptr是如何做到指向Person对象调用Person::BuyTickets,指向Student对象调用Student::BuyTickets的呢?通过上图我们可以看到,满足多态条件之后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。

如果派生类重写了基类的虚函数,那么派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

这里我们回头再看看1.4的题目:

class A
{
public:
	virtual void func(int val = 1)
	{
		cout << "A->" << val << endl;
	}
	virtual void test()
	{
		func();
	}
};

class B : public A
{
public:
	void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};
int main()
{
	B* ptr = new B;

	ptr->test();
	return 0;
}

有了下面这张图,我们就很容易理解了,为什么最终的结果是B->1了。首先B是继承了A,并且B中的func函数满足了对基类的重写/覆盖,然后B也继承了A中的test()。因此当我们调用ptr->test()时,虚函数test()的地址在虚表中没有改变,调用的时候会去A类里面调用。之后才是调用func函数,此时已经构成重写/覆盖,因此调用的这个虚函数func()地址是指向B类的func()。那么有同学就说了,为什么不是B->0呢?我们要知道,重写/覆盖只是对函数的内容进行重写,因此相当于调用了下面的函数:

void func(int val = 1)
{
	cout << "B->" << val << endl;
}

你可以理解为重写/覆盖值改变了{ }内的内容,而函数参数还是用了基类的。

3. 虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址。
  • 派生类的虚函数表由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中中有虚函数表指针,自己就不会再生成虚函数表指针。但是继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个。
  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数地址就会被覆盖生成派生类重写的虚函数地址。
  • 派生类的虚函数表中包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。
  • 虚函数表本质是一个存放虚函数指针的指针数组。
  • 虚函数存在于哪?虚函数和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表之中
  • 虚函数表存在于哪的呢?C++标准并没有规定,但是在VS里面是存在于常量区的。

我们可以通过下面的代码,在VS上证实:

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈: % p\n", &i);
	printf("静态区: % p\n", &j);
	printf("堆: % p\n", p1);
	printf("常量区: % p\n", p2);
	Base b;
	Derive d;
	Base * p3 = &b;
	Derive * p4 = &d;
	printf("Base虚表地址: % p\n", *(int*)p3);
	printf("Derive虚表地址: % p\n", *(int*)p4);
	printf("虚函数地址: % p\n", &Base::func1);
	printf("普通函数地址: % p\n", &Base::func5);
	return 0;
}

运行结果显示虚表的地址和常量区的地址非常接近,而虚函数的地址和普通函数的地址非常接近。

以上内容如有错误,欢迎批评指正!

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

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

相关文章

diff 命令:文本比较

一、diff 命令简介 ​diff ​命令是一个用于比较两个文件并输出它们之间差异的工具。它是文件比较的基本工具&#xff0c;广泛用于源代码管理、脚本编写和日常的文件维护工作中。 ‍ 二、diff 命令参数 diff [选项] 文件1 文件2选项&#xff1a; ​-b​ 或 --ignore-space…

光伏选址和设计离不开气象分析!

都说光伏选址和设计离不开气象分析&#xff0c;气象条件对太阳能发电影响较大&#xff0c;具体有哪些影响呢&#xff1f;今天我就来讲解下。 - 太阳辐射&#xff1a;太阳辐射的强度是光伏发电的首要因素&#xff0c;对光伏发电有着重要的影响。太阳辐射的强度决定了光伏发电系…

信息安全数学基础(14)欧拉函数

前言 在信息安全数学基础中&#xff0c;欧拉函数&#xff08;Eulers Totient Function&#xff09;是一个非常重要的概念&#xff0c;它与模运算、剩余类、简化剩余系以及密码学中的许多应用紧密相关。欧拉函数用符号 φ(n) 表示&#xff0c;其中 n 是一个正整数。 一、定义 欧…

LVGL学习

注&#xff1a;本文使用的lvgl-release-v8.3版本&#xff0c;其它版本可能稍有不同。 01 LVGL模拟器配置 day01-02_课程介绍_哔哩哔哩_bilibili LVGL开发教程 (yuque.com) 如果按照上述视频和文档中配置不成功的话&#xff0c;直接重装VsCode&#xff0c;我的就是重装以后就…

[Visual Stuidio 2022使用技巧]2.配置及常用快捷键

使用vs2022开发WPF桌面程序时常用配置及快捷键。 语言&#xff1a;C# IDE&#xff1a;Microsoft Visual Studio Community 2022 框架&#xff1a;WPF&#xff0c;.net 8.0 一、配置 1.1 内联提示 未开启时&#xff1a; 开启后&#xff1a; 开启方法&#xff1a; 工具-选…

torch.linspace() torch.arange() torch.stack() 函数详解

1 torch.linspace函数详解 torch.linspace(start, end, steps100, outNone, dtypeNone, layouttorch.strided, deviceNone, requires_gradFalse) → Tensor 函数的作用是&#xff0c;返回一个一维的tensor&#xff08;张量&#xff09;&#xff0c;这个张量包含了从start到end…

【专题】2024新能源企业“出海”系列之驶向中东、东南亚报告合集PDF分享(附原数据表)

原文链接&#xff1a; https://tecdat.cn/?p37698 在“双碳”目标引领下&#xff0c;中国新能源产业近年迅猛发展&#xff0c;新能源企业凭借技术革新、政策支持与市场驱动实现快速增长&#xff0c;在产业链完备、技术领先、生产效能及成本控制等方面优势显著。面对国内外环境…

单向循环链表

文章目录 &#x1f34a;自我介绍&#x1f34a;单向循环链表 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一键四连&#xff09;哦~ &#x1f34a;自我介绍 Hello,大家好&#xff0c;我是小珑也要变强&#xff08;也是小珑&…

Linux(3)--CentOS8下载、安装

文章目录 1. CentOS简介2. 下载3. 使用VmWare安装CentOS4. 第一次使用 1. CentOS简介 这个版本我个人比较推荐大家学习&#xff0c;为何&#xff1f;因为容易学习所以不难入门。 2. 下载 可以从国内的开源镜像站下载&#xff0c;这样比较快&#xff0c;例如阿里巴巴开源镜像…

C语言-整数和浮点数在内存中的存储-详解-上

C语言-整数和浮点数在内存中的存储-详解-上 1.前言2.整数2.1无符号整数2.2原码、反码、补码符号位最大值转换过程补码的意义简化算术运算易于转换方便溢出处理 1.前言 在C语言的使用中&#xff0c;需要时刻关注数据的类型&#xff0c;不同类型交替使用可能会发生错误&#xff…

GPT-4-Turbo 和 Claude-3.5-Sonnet 图片识别出答题的是否正确 进行比较

1、比较的图片&#xff1a; 使用GPT-4-Turbo 输入的 提问&#xff1a; 识别图片中的印刷字和手写字&#xff0c;如果写错的给一个正确答案 图片 回复&#xff1a; 在图片中&#xff0c;印刷字显示的是一系列的英语填空练习题&#xff0c;而手写字则是填入空白处的答案。以…

[XILINX] 正点原子ZYNQ7015开发板!ZYNQ 7000系列、双核ARM、PCIe2.0、SFPX2,性能强悍,资料丰富!

正点原子ZYNQ7015开发板&#xff01;ZYNQ 7000系列、双核ARM、PCIe2.0、SFPX2&#xff0c;性能强悍&#xff0c;资料丰富&#xff01; 正点原子Z15 ZYNQ开发板&#xff0c;搭载Xilinx Zynq7000系列芯片&#xff0c;核心板主控芯片的型号是XC7Z015CLG485-2。开发板由核心板&…

【刷题】Day3--错误的集合

hello&#xff01;又见面啦~~~ 一道习题&#xff0c;要长脑子了...... 【. - 力扣&#xff08;LeetCode&#xff09;】 【思路】 /*** Note: The returned array must be malloced, assume caller calls free().*/void Bubble_sort(int arr[], int size) {int temp;for (int i…

Unity 粒子系统参数说明

一、Particle System 1. Duration&#xff08;持续时间&#xff09; 粒子系统运行一次所需的时间。它决定粒子系统持续播放的时间长度。 2. Looping&#xff08;循环播放&#xff09; 如果启用&#xff0c;粒子系统将在播放完一次后自动重新开始播放&#xff0c;直到你停止它…

北斗赋能万物互联:新质生产力的强劲驱动力

在数字化转型的大潮中&#xff0c;中国自主研制的北斗卫星导航系统&#xff0c;作为国家重大空间基础设施&#xff0c;正以前所未有的力量推动着万物互联时代的到来&#xff0c;成为新质生产力发展的重要基石。本文将深入剖析北斗系统如何以其独特的技术优势和广泛应用场景&…

【vue】vue3+ts对接科大讯飞大模型3.5智能AI

如今ai步及生活的方方面面,你是否也想在自己的网站接入ai呢&#xff1f;今天分享科大讯飞大模型3.5智能AI对接。 获取APPID、APISecret、APIKey 讯飞开放平台注册登录控制台创建自己的应用复制备用 准备工作做好,直接开始上代码了。 源码参考 <script setup lang"t…

一步到位:通过 Docker Compose 部署 EFK 进行 Docker 日志采集

一、EFK简介 Elasticsearch&#xff1a;一个开源的分布式搜索和分析引擎&#xff0c;用于存储和查询日志数据。它是 EFK 的核心组件&#xff0c;负责高效地存储和检索日志信息。 Filebeat&#xff1a;一个轻量级的日志采集器&#xff0c;主要用于将日志文件数据发送到 Logsta…

Ubuntu20+Noetic+cartographer_ros编译部署

1 准备工作 &#xff08;1&#xff09;准备Ubuntu20系统。 &#xff08;2&#xff09;安装ROS系统,参考 https://blog.csdn.net/weixin_46123033/article/details/139527141&#xff08;3&#xff09;Cartographer相关软件包和源码下载&#xff1a; https://gitee.com/mrwan…

go语言后端开发学习(七)——如何在gin框架中集成限流中间件

一.什么是限流 限流又称为流量控制&#xff08;流控&#xff09;&#xff0c;通常是指限制到达系统的并发请求数。 我们生活中也会经常遇到限流的场景&#xff0c;比如&#xff1a;某景区限制每日进入景区的游客数量为8万人&#xff1b;沙河地铁站早高峰通过站外排队逐一放行的…

ElementUI 快速入门:使用 Vue 脚手架搭建项目

文章目录 一 . ElementUI 的基本安装1.1 通过 Vue 脚手架创建项目1.2 在 vue 脚手架中安装 ElementUI1.3 编写页面 ElementUI 是 Vue.js 的强大 UI 框架&#xff0c;让前端界面开发变得简单高效。本教程将带你从安装到实战&#xff0c;快速掌握 ElementUI 的核心技巧。 核心内容…