【C++】多态,虚函数表相关问题解决

news2024/12/24 21:28:50

文章目录

  • 多态概念及其触发条件
  • 重写和协变
    • (考点1)
    • (考点2)
  • 虚函数表及其位置
    • (考点3)
  • 多继承中的虚函数表

多态概念及其触发条件

  多态的概念:通俗来说,就是多种形态。具体点就是去完成某个行为,当不同的对象去完成时,会产生出不同的状态

多态的构成条件:
1.必须通过基类的指针或者引用调用虚函数(即被virtual修饰的类成员函数称为虚函数)
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重写和协变

  虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数类型),称子类的虚函数重写了基类的虚函数

虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
  派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
2. 析构函数的重写(基类与派生类析构函数的名字不同)
  如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

override和final两个关键字

(考点1)

这里强调一下,重写重写的是实现。看以下这个场景:(考点)

class A
{
public:
	A()
	{}
	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()
{

	A* p = new B();
	p->test();
	return 0;
}

  打印结果为B->1,说明调的是子类的func函数,但是缺省值用的却是父类,返回值,函数名,参数类型相同即构成重写,重写重写的是实现,壳子用的是父类的,写的内容自己控制

(考点2)

那为什么要把析构函数构成重写呢?看以下这个场景:(考点)


class Person {
public:
	 ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() {
		cout << "~Student()" << endl;
		delete[] ptr;
	}
protected:
	int* ptr = new int[10];
};

int main()
{
	Person* p = new Person;
	delete p;

	p = new Student;
	delete p; 
	return 0;
}

  当我们用父类指针,指向子类对象时,期望析构的是子类对象,而不是父类对象。不构成重写的话,无论父类指针是指向子类对象还是父类对象,析构的都是父类对象,导致下面的 ptr 动态开辟的空间没有释放而内存泄漏





  当我们给父类析构函数加上 virtual,让其构成重写后。同时注意这里析构玩~Student后还会析构继承父类,照应上面的构造先父后子,"析构先子后父"



抽象类:
  在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承




动态绑定与静态绑定:

虚函数表及其位置

  一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,通过下面这个例子来看对象模型



(考点3)

这时候我们再反过来思考,为什么一定是父类的指针或者引用,而不能是父类对象?

  首先我们可以看出,子类对象会先拷贝父类虚函数表,然后再对需要重写的虚函数进行地址修改。
  假如我们把子类对象赋值给父类对象,那么子类对象的虚函数表要不要拷贝给父类?如果虚函数表不拷贝,那么还是调用父类的函数,没有构成多态。
  如果拷贝了,那么父类对象的虚函数表存的是子类对象修改后的虚函数,如下图:此时我们无法再调用父类本身被重写的函数,因为无论我们传子类还是父类对象,调用的都是子类对象的函数,不能构成多态。
  因此多态的条件,一定是父类的指针或者引用,这样可以避免像下面这样拷贝带来的错误。

虚表位置

class Person {
public:
	virtual	void BuyTicket() const { cout << "成人-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() const { cout << "学生-半价" << endl; }
};
int main()
{
	Person ps;
	Student st;
	int a = 0;
	printf("栈:%p\n\n", &a);
	static int b = 0;
	printf("静态区:%p\n\n", &b);
	int* p = new int;
	printf("堆:%p\n\n", p);
	const char* str = "hello world";
	printf("常量区:%p\n\n", str);
	printf("虚表1:%p\n", *((int*)&ps));
	printf("虚表2:%p\n", *((int*)&st));
	return 0;
}

  虚表存放在哪里呢?首先排除堆,虚表由编译器生成,不会自己去动态申请空间。其次排除栈,同类型对象公用一张虚表,栈都是伴随栈帧走的,不能函数调用结束,栈帧销毁,虚表就销毁了吧。我们用打印的方式来看一下虚表是存在哪里的
  看下面的代码和输出结果,我们可以发现,虚表是存在常量区的

多继承中的虚函数表

// 打印函数指针数组
typedef void(*FUNC_PTR) ();
void PrintVFT(FUNC_PTR* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		FUNC_PTR f = table[i];
		f();//这个地址可以调用说明一定是函数
	}
	printf("\n");
}

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

int main()
{
	Derive d;
	int vft1 = *((int*)&d);
	Base2* ptr = &d;
	int vft2 = *((int*)ptr);
	printf("第一张虚表:\n");
	PrintVFT((FUNC_PTR*)vft1);
	printf("第二张虚表:\n");
	PrintVFT((FUNC_PTR*)vft2);
	return 0;
}

  先看上面这段代码,首先d对象有几张虚表呢?看下面的监视窗口,很明显发现d对象有两张虚表,但是d对象自己的虚函数func3去哪里了,其实它在第一张虚表中,我们可以通过上面的代码打印观察出来,f()这个地址可以调用,说明它一定是函数。这里是可以认为是编译器的监视窗口故意隐藏了func3函数,也可以认为是它的一个小bug



  可是细心一点发现,两张表中的func1地址不一样,它们不是都重写了func1函数吗?而且用父类指针调用会发现,它们调的是同一个函数,那么这里为什么地址不一样呢?



看下面这个场景


  
注意:这里你要调用的是派生类d对象的func1函数,this指针应该指向d对象,而这里的ptr1指针恰好指向d对象,不需要改动。而ptr2指向的却是Base2对象。调用d对象的func1函数要传d对象的this指针, 而不是Base2对象的this指针。所以这里第二张表的地址其实是"虚地址",多封装了几层是为了修正this指针

接下来我们通过汇编来看看ptr1和ptr2调用的区别,更好理解Base2的"虚地址"
ptr1调用




ptr2调用

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

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

相关文章

DSA之图(2):图的存储结构

文章目录 0 图的结构1 邻接矩阵1.1 无向图的邻接矩阵1.2 有向图的邻接矩阵1.3 网&#xff08;有权图&#xff09;的邻接矩阵表示法1.4 邻接矩阵的建立1.4.1 采用邻接矩阵建立无向网1.4.2 采用邻接矩阵建立有向网 1.5 邻接矩阵的优缺点1.5.1 优点1.5.2 缺点 2 邻接表2.1 无向图的…

Java将汉字转拼音以及判断字符是否为汉字

首先是将汉字转换为拼音&#xff1a; 导入依赖&#xff1a; <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.2</version></dependency>创建转拼音的静态方法toPinyin&#xf…

掌握 Python RegEx:深入探讨模式匹配

动动发财的小手&#xff0c;点个赞吧&#xff01; 什么是正则表达式&#xff1f; 正则表达式通常缩写为 regex&#xff0c;是处理文本的有效工具。本质上&#xff0c;它们由一系列建立搜索模式的字符组成。该模式可用于广泛的字符串操作&#xff0c;包括匹配模式、替换文本和分…

在线阅读版:《2023中国软件供应链安全分析报告》全文

聚焦源代码安全&#xff0c;网罗国内外最新资讯&#xff01; 专栏供应链安全 数字化时代&#xff0c;软件无处不在。软件如同社会中的“虚拟人”&#xff0c;已经成为支撑社会正常运转的最基本元素之一&#xff0c;软件的安全性问题也正在成为当今社会的根本性、基础性问题。 随…

火车头采集器伪原创【php源码】

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;python中按钮的位置怎么摆放&#xff0c;python中按钮怎么设置颜色&#xff0c;现在让我们一起来看看吧&#xff01; 火车头采集ai伪原创插件截图&#xff1a; 1、用python的pygame,做一个按钮 唔...摁钮&#xff1f;…

【万字长文】SpringBoot整合SpringSecurity+JWT+Redis完整教程(提供Gitee源码)

前言&#xff1a;最近在学习SpringSecurity的过程中&#xff0c;参考了很多网上的教程&#xff0c;同时也参考了一些目前主流的开源框架&#xff0c;于是结合自己的思路写了一个SpringBoot整合SpringSecurityJWTRedis完整的项目&#xff0c;从0到1写完感觉还是收获到不少的&…

MYSQL导入excel数据后只显示500条

问题&#xff1a;明明显示数据全部导入成功&#xff0c;但是点开table后发现只显示了500条 解决步骤&#xff1a;&#xff08;以datagrip为例&#xff09; 其实大家已经把数据导入了&#xff0c;只是在工具里&#xff0c;它在设置里面做了限制&#xff0c;只显示500条数据。只…

Kotlin 内联函数语法之let、apply、also、run、with的用法与详解

一、介绍 kotlin的语法千奇百怪&#xff0c;今天我们将介绍项目中频率使用比较高的几个内联函数。 二、什么叫内联函数&#xff1f; 内联函数 的语义很简单&#xff1a;把函数体复制粘贴到函数调用处 。使用起来也毫无困难&#xff0c;用 inline关键字修饰函数即可。 语法&a…

三菱FX5U系列PLC内置定位功能的基本使用方法介绍

三菱FX5U系列PLC内置定位功能的基本使用方法介绍 三菱FX5U系列PLC本体自带的高速脉冲输出可以实现定位功能,具体的使用方法可参考以下内容: 参数设定 如下图所示,新建一个工程,在左侧的项目树中找到参数–模块参数—高速I/O,双击进入后找到输出功能—定位—点击进入详细设…

BHQ 1Mal,BHQ-1 Maleimide,BHQ1马来酰亚胺,黑洞猝灭剂

资料编辑|陕西新研博美生物科技有限公司小编MISSwu​ PART1----产品描述&#xff1a; BHQ-1 Maleimide黑洞猝灭剂-1(BHQ-1)被归类为暗猝灭剂&#xff0c;该淬灭剂能够将一定距离内荧光基团发出的光全部吸收&#xff0c;实现对荧光信号的淬灭&#xff0c;所以可得到更强的特异性…

基于SpringBoot+Vue的学习平台设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

神经网络的初始化方法

文章目录 1、随机初始化2、Xavier初始化3、He初始化4、权重预训练初始化5、零初始化 对于神经网络的训练过程中&#xff0c;合适的参数初始化方法有助于更好的处理梯度消失和梯度爆炸问题。通常有以下几种初始化方法&#xff1a; 1、随机初始化 随机初始化&#xff08;Random…

[JavaWeb]MySQL的安装与介绍

MySQL的安装与介绍 一.数据库相关概念1.1 数据库1.2 常见的关系型数据库管理系统 二.MySQL数据库1.MySQL的安装2.配置环境变量3.新建MySQL配置文件4.初始化MySQL5.注册MySQL的服务6.修改默认账户与密码7.连接MySQL服务8.MySQL的卸载 三.MySQL的数据模型1.关系型数据库 一.数据库…

static关键字和继承

1、static关键字 1.1案例题目 • 编程实现People类的封装&#xff0c;特征有&#xff1a;姓名、年龄、国籍&#xff0c;要求提供打印所有特征的方法。 • 编程实现PeopleTest类&#xff0c;main方法中使用有参方式构造两个对象并打印。 /*编程实现People类的封装*/ public cl…

Python+Texturepacker自动化处理图片

前言 本篇在讲什么 PythonTexturepacker自动化处理图片 本篇需要什么 对Python语法有简单认知 依赖Python2.7环境 依赖Texturepacker工具 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻理论&#xff0c;快速上手 提供全流程的源码内容 ★提高阅读体验★ &…

ubuntu18.04 安装php7.4-xdebug

文章目录 场景解决 场景 apt install php7.4-xdebug 下载失败, 只好通过编译解决了 解决 https://xdebug.org/wizard 输入php -i的执行结果

mybatisplus映射解读

目录 自动映射 表映射 字段映射 字段失效 视图属性 Mybatis框架之所以能够简化数据库操作&#xff0c;是因为他内部的映射机制&#xff0c;通过自动映射&#xff0c;进行数据的封装&#xff0c;我们只要符合映射规则&#xff0c;就可以快速高效的完成SQL操作的实现。既然…

AI语音合成 VITS Fast Fine-tuning,半小时合成专属模型,部署训练使用讲解

前言 项目名&#xff1a;VITS-fast-fine-tuning &#xff08;VITS 快速微调&#xff09; 项目地址&#xff1a;https://github.com/Plachtaa/VITS-fast-fine-tuning 支持语言&#xff1a;中、日、英 官方简介&#xff1a; 这个代码库会指导你如何将自定义角色&#xff08;甚至…

低温试验中的液氮注入式宽温区超高精度温度控制解决方案

摘要&#xff1a;当前各种测试仪器中的低温温度控制过程中&#xff0c;普遍采用电增压液氮泵进行制冷和辅助电加热形式的控温方式。由于液氮温度和传输压力的不稳定&#xff0c;这种方式的控温精度仅能达到0.5K&#xff0c;很难实现小于0.1K的高精度控温。为此本文基于饱和蒸气…

知识梳理(一)

HTTPS握手过程 https的核心的技术&#xff1a;使用非对称加密传输对称加密的密钥&#xff0c;然后用对称加密通信 TLS http--超文本传输协议&#xff0c;是以前并且沿用至今的网页协议。 缺点&#xff1a;http属于明文传输 HTTP的明文传输带来的问题是无法防止中间人截获、…