C++相关概念和易错语法(21)(虚函数、协变、析构函数的重写)

多态的核心是虚函数,本文从虚函数出发,根据原理慢慢推进得到结论,进而理解多态

1.虚函数

先看一下下面的代码,想想什么导致了这个结果


#include <iostream>
using namespace std;

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

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

class C : public B
{
public:
	void test()
	{
		cout << "C" << endl;;
	}
};

void Test(A& r)
{
	r.test();
}

int main()
{
	A a;
	B b;
	C c;

	Test(a);
	Test(b);
	Test(c);

	return 0;
}

结果是

如果我们去掉A里面的virtual呢?

我们可以看到前后两次结果不同,为什么呢?函数形参为什么是以A&来接收的,调用时为什么还有区别呢?这就需要接触虚函数了。

(1)虚函数和虚函数表

当我们在父类声明了一个虚函数后,这个函数就被存在常量区了,同时在这个类里又多了一个新的隐藏成员,叫虚函数表(这个成员要算在整个类的大小里面)。这个虚函数表就是专门存虚函数的地址的(本质是函数指针数组,根据不同机器指针大小也不同)。对于父类而言,无论创建多少对象,它们都共用一个虚函数表(即对于同一种类,函数都是一样的)。这里要分清:虚函数是存在常量区的而不存在类里,类中存的是虚函数表。

(2)重写

虚函数有什么用呢?当子类实现一个和父类虚函数函数名、参数、返回值完全一样的函数时,就叫做重写。重写是一种特殊的隐藏,是在多态中的一种语法,而隐藏只要求函数名相同,是继承中的语法。重写的意义在于子类也有一个新的虚函数表,虽然函数前没有加声明virtual(父类前必须加),当子类显式写了这个函数,就会存到常量区,虚函数表存函数的地址(第一句指令的地址)。对于这个子类,无论创建多少个对象,它们都使用同一个针对子类的虚函数表。如果说有多个虚函数而子类没有重写,那个没有重写的函数就使用父类的对应的函数(反正没区别)。

(3)对多态的理解

到这里,我们对虚函数表、虚函数和重写有了一定了解,实际就是在最初的父类的函数前加上virtual,让该函数进入虚函数表,子类重写会让虚函数表存的函数不同,在调用的时候明明是调用的同一个函数,但得到的结果是针对每一种类不同的。这就叫多态,即多种形态,针对不同的类有不同的表现形态。

(4)对多态调用方式的理解

函数形参为什么是以A&来接收的?

我们进一步关注Test(A& r)这个函数,前面我们讲了赋值兼容转换,因此当B和C传进去的时候,r都会指向子类中的父类部分,这里相当于给它们的父类部分取别名。也就是说,r无论接收的是A还是B还是C,最终都会被切割成A的模样(A中也有虚函数表),但是内容是不是都一样呢?

很明显,虚函数表的作用就凸显出来了,A、B、C都有一个虚函数表,在B、C切割成A后,虚函数表被保留了下来,当我们用r去调用虚函数时,编译器会默认去虚函数表找到对应的函数(三种虚函数表的函数在函数名、参数、返回值上都相同,但存的函数地址不同),根据不同的函数地址就能找到不同的函数实现,这也是重写的意义所在。

至此,我们应该能够理解前面所说虚函数、虚函数表、重写存在的意义了,它们的出现都最终服务于实现一件事——多态,即根据不同类,在调用同一函数时体现出不同状态。

(5)是否有其它调用方式?

事实上,使用A&调用本质就是利用了赋值兼容转换,将多个子类都切割成父类的形式,再根据它们虚函数表的值的差异,调用不同的同名函数,体现出类与类之间的区别。很明显,除了引用,指针也适合,但赋值呢?赋值不是也遵循赋值兼容转换吗?

从实验上看是不行的,但也好理解。r都已经完全变成A类型了,再去调用B或C的成员就不太说得过去了。你可以将这里理解成一种特殊处理,支不支持都说得过去,但从形式上来说不支持更合理。

(6)多态的条件

很多课程都喜欢先说条件再将原因,而如果我们慢慢推进,到这里自然就理解了。

多态需满足条件:父类函数(想和子类形成差异的第一个函数就叫父类函数)写virtual(父类如果不写virtual而子类写virtual,那第一个写virtual的才叫父类,你可以将virtual当作一个多态开始的标志),后续的所有子类写不写virtual无所谓;子类覆盖/重写父类的虚函数;调用时使用父类的指针或引用,特别注意不能用赋值。

2.协变

上面说过要重写函数,必须保证函数的函数名、参数、返回值相同。但有唯一一个例外可以在返回值不同时能构成重写,就是协变(基本不用),即返回值可以是父子类的引用或指针

下面这段代码是能跑过的


#include <iostream>
using namespace std;

class A
{
public:
	virtual A& test()
	{
		cout << "A" << endl;;
		return *this;
	}
};

class B : public A
{
public:
	B& test()
	{
		cout << "B" << endl;;
		return *this;
	}
};


void Test(A& r)
{
	r.test();
}

int main()
{
	A a;
	B b;

	Test(a);
	Test(b);


	return 0;
}

注意,返回值可以加const,返回值也可以是其它类,但必须是父子关系


#include <iostream>
using namespace std;

class C
{};

class D : public C
{};

class A
{
public:
	virtual const C* test()
	{
		cout << "A" << endl;
		C* c = new C;
		return c;
	}
};

class B : public A
{
public:
	const D* test()
	{
		cout << "B" << endl;
		D* d = new D;
		return d;
	}
};


void Test(A& r)
{
	r.test();
}

int main()
{
	A a;
	B b;

	Test(a);
	Test(b);


	return 0;
}

注意父子关系顺序不能反,父类返回值对应父类的虚函数

协变几乎不用,了解即可。我们大部分情况还是要保证函数名、参数、返回值相同,讨论的时候也是跳过这个特殊情况的。

3.析构函数的重写

理解析构函数的重写可以加深我们对析构函数的理解,顺便能够解释为什么所有的析构函数都会被处理成destructor()


#include <iostream>
using namespace std;


class A
{
public:
	~A()
	{
		cout << "A" << endl;
	}
};

class B : public A
{
public:
	~B()
	{
		cout << "B" << endl;
		delete p;
	}

	int* p;
};


int main()
{
	A* a = new B;
	delete a;

	return 0;
}

这段代码会导致内存泄漏,因为当delete a的时候,会根据a的类型去调用析构函数,这里就只会去调用A的析构函数

联系到上面的重写,很快我们就会想到使用virtual修饰父类的析构函数,让析构函数进入虚函数表。但是很明显父类和子类的类名是不可能相同的,所以类的析构函数做了特殊处理:即都重命名为~destructor(),这样就符合了虚函数的要求


我们可以看到,这里根据虚函数表就能成功调到子的析构函数了,同时对于所有继承而言,子的析构调用完成之后都会逐级向上调用父的析构函数

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

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

相关文章

原来,BI数据分析也是有模板的

在当今数据驱动的时代&#xff0c;商业智能&#xff08;BI&#xff09;数据分析已经成为企业决策的重要工具。然而&#xff0c;很多人可能并不了解&#xff0c;BI数据分析并非从零开始&#xff0c;而是可以依托现成的模板和解决方案来快速搭建和实施的。以奥威BI方案为例&#…

Springboot + JWT 的 Token 登录验证

目录 项目结构 一、 引入依赖 二、自定义Auth认证注解 三、 编写登录拦截器 四、定义跨域拦截器 五、 定义全局异常处理器 六、定义工具类 1. 统一错误状态码 2.统一响应类 3.Token工具类 七、 编写实体类 八、 定义控制器 1.定义登录控制器类 2 定义报错处理器 …

量化投资基础(二)之CAPM模型

点赞、关注&#xff0c;养成良好习惯 Life is short, U need Python 量化投资基础系列&#xff0c;不断更新中 资本资产定价模型&#xff08;Capital Asset Pricing Model,CAPM&#xff09;是由美国经济学家 威廉夏普&#xff08;William Sharpe&#xff09;于20世纪60年代基于…

小型数控车床对现代制造业的影响

小型数控车床作为现代制造业的重要生产工具&#xff0c;集成了计算机控制、精密机械、电子技术和自动化技术&#xff0c;为各种复杂零件的加工&#xff0c;在生产效率和精度上带来了显著提升&#xff0c;它是制造业中不可或缺的基础装备&#xff0c;在金属切削加工领域发挥着关…

数据库的管理

目录 远程连接的方式 修改数据库uuid流程 数据库的概念 关系型数据库 非关系型数据库 关系型数据和非关系型数据库优缺点 mysql的数据类型 数据库的管理 sql中的名词 sql语言的分类 1.DDL 创建库和表的语句 create 删除库 drop databases 2.DML: 插入数据 ins…

使用APEXSQL LOG解析sql server事务日志,进行审计与数据恢复

一 下载 https://download.csdn.net/download/sunke861/11449739 二 使用 解压安装包后&#xff0c;点击&#xff1a;ApexSQLLog.exe 2.1 连接数据库 连接要审计的数据库&#xff1a; 假如报错&#xff1a; 则点击ok关闭该窗口&#xff0c;然后点击左上方的New按钮&#xf…

IDEA创建普通Java项目

环境准备 Java环境 运行javac查看java环境是否安装完成 开发工具Intellij IDEA 下载地址&#xff1a;https://www.jetbrains.com/idea/download/?sectionwindows 创建项目 点击新建项目 填入项目名字&#xff0c;项目路径&#xff0c;选择maven,点击下面的创建 运行项目 …

数据库管理-第220期 Oracle的高可用-03(20240715)

数据库管理220期 2024-07-15 数据库管理-第220期 Oracle的高可用-03&#xff08;20240715&#xff09;1 AC/TAC2 配置Service3 用户权限4 端口开放总结 数据库管理-第220期 Oracle的高可用-03&#xff08;20240715&#xff09; 作者&#xff1a;胖头鱼的鱼缸&#xff08;尹海文…

半导体超纯水(UPW)全面监控检测项目及液体粒子计数器应用

超纯水 (UPW) 是经过高度纯化的水&#xff0c;去除了所有矿物质、颗粒、细菌、微生物和溶解的气体。在芯片厂里&#xff0c;它也经常会被混称为 DI Water&#xff08;去离子水&#xff09;&#xff0c;但国内通常把去离子水与超纯水分的比较开&#xff0c;其实去离子水是包含了…

智能可视采耳棒耳勺安全吗?六大选购技巧排忧解惑!

耳垢作为外耳道内腺的分泌物&#xff0c;如果不及时清理&#xff0c;可能会造成耳道的栓塞&#xff0c;进而引致耳痛、听力减弱、咳嗽等不适。而传统的耳勺由于其盲操作的特性&#xff0c;对于耳道非直线结构的清理存在诸多不便。所以市面上出现了可视挖耳勺&#xff0c;让我们…

【转盘案例-基本框架-创建按钮-按钮布局 Objective-C语言】

一、转盘案例的基本框架 1.我们先来看一下这个转盘啊 新建一个项目,Name:01-大转盘 把素材拷过来, 我们先把背景图片设置一下, 这张图片,可能会很大,跟你的屏幕呢,可能不成正比,这个时候呢,我们应该用拉伸的方式,去做,那么,这个控制器的背景,应该怎么去设置, 画…

逆向案例二十——请求头参数加密,某政府农机购置与应用补贴申请办理服务系统,sm3和sm4的加密

网址&#xff1a;农机购置与应用补贴申请办理服务系统 抓包分析&#xff0c;发现请求头参数有加密&#xff0c;表单有加密&#xff0c;返回的数据也是加密的。 请求头Source是固定的&#xff0c;其他的Sign,以及Timsestamp是加密的 请求载荷也是加密的 返回的数据也是加密的。…

ValueError和KeyError: ‘bluegrass’的问题解决

项目场景&#xff1a; 项目相关背景&#xff1a; 问题描述 遇到的问题1&#xff1a; KeyError: ‘bluegrass’ 不能识别某标签 遇到的问题2&#xff1a; xml etree.fromstring(xml_str) ValueError: Unicode strings with encoding declaration are not supported. Please …

Go语言--广播式并发聊天服务器

实现功能 每个客户端上线&#xff0c;服务端可以向其他客户端广播上线信息&#xff1b;发送的消息可以广播给其他在线的客户支持改名支持客户端主动退出支持通过who查找当前在线的用户超时退出 流程 变量 用户结构体 保存用户的管道&#xff0c;用户名以及网络地址信息 typ…

MongoDB自学笔记(一)

一、MongoDB简介 MongoDB是一款基于C开发的文档型数据库。与传统的关系型数据库有所不同&#xff0c;MongoDB面向的是文档&#xff0c;所谓的文档是一种名为BSON &#xff08;Binary JSON&#xff1a;二进制JSON格式&#xff09;是非关系数据库当中功能最丰富&#xff0c;最像…

【conftest】和【fixtures】

一、 conftest.py 文件 作用&#xff1a;存放case的前提条件和后置条件配置函数&#xff1b;一般该类函数都会使用fixture装饰&#xff08;fixture该篇第二点会介绍&#xff09;&#xff1b;使用conftest里面的函数时不需要导入 conftest.py 这个文件。只需将函数名作为变量传入…

Python实现人脸识别

直接上代码&#xff1a; import face_recognition import time from PIL import Image, ImageDraw def faceRecognition(fileName): # 加载图片image face_recognition.load_image_file(fileName)# 人脸定位beginTime time.time()face_locations face_recognition.face_lo…

SEO:6个避免被搜索引擎惩罚的策略-华媒舍

在当今数字时代&#xff0c;搜索引擎成为了绝大多数人获取信息和产品的首选工具。为了在搜索结果中获得良好的排名&#xff0c;许多网站采用了各种优化策略。有些策略可能会适得其反&#xff0c;引发搜索引擎的惩罚。以下是彭博社发稿推广的6个避免被搜索引擎惩罚的策略。 1. 内…

【python】pandas报错:UnicodeDecodeError详细分析,解决方案以及如何避免

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Spring Security Oauth2源码分析

Spring Security Oauth2源码分析 前言一&#xff1a;客户端OAuth2授权请求的入口1、DefaultOAuth2AuthorizationRequestResolver类OAuth2AuthorizationRequest类authorizationRequestUri 的构建机制redirectUri 3、OAuth2AuthorizationRequestRedirectFilter类 二&#xff1a;O…