C++——多态与虚表

目录

1.多态的实现

2.虚表

2.1虚函数重写是怎么实现的

2.2多态的原理

2.3静态绑定与动态绑定

3.单继承体系中的虚函数表

​编辑4.多继承体系中的虚函数表

5.菱形继承的虚函数表

6.菱形虚拟继承的虚函数表

1.多态的实现

在C++中,要想实现多态,必须满足以下几个条件:

  1. 有继承关系
  2. 有虚函数
  3. 虚函数要重写

根据继承关系中,派生类向基类赋值没有发生类型转换的特性,可以得出一个结论:基类的指针或引用指向了一个"基类对象",这个"基类对象"有可能是基类本身的对象,也有可能是派生类当中的基类部分,由于编译时确定不了(因为这两种基类对象没有差别),所以多态又称运行时绑定

这幅图用来解释上面那段话:

 以一段代码来体会运行时绑定(动态绑定):

class Person
{
public:
	virtual void slefMessage()
	{
		cout << "Person" << endl;
	}
};

class Student : public Person
{
public:
	virtual void slefMessage()
	{
		cout << "Student" << endl;
	}
};

class Teacher : public Person
{
public:
	virtual void slefMessage()
	{
		cout << "Teacher" << endl;
	}
};

void testPolimorphic(Person &rp)
{
	rp.slefMessage();//调用虚函数
}
int main()
{
	Student s;
	Teacher t;
	testPolimorphic(s);
	testPolimorphic(t);
	return 0;
}

2.虚表

虚表全称虚函数表,所以它是一个函数指针数组。 虚表指针将会被存放在对象中,所以以下代码的输出结果可能会令人诧异:

class Person
{
public:
	virtual void slefMessage()
	{
		cout << "Person" << endl;
	}
};
int main()
{
	Person p;
	cout << sizeof(p) << endl;
	return 0;
}

实际上类的对象模型当中确实不存储任何成员函数,包括虚函数在内。虚函数被存放在了虚函数表当中,但是编译为了能够找到虚函数表,所以有必要维护一个虚函数表指针,并将它存放在对象当中。所以最后的输出结果为4(64位平台下的输出结果为8,本篇文章的所有测试用例都在Visual Studio 2013下编译运行)。也就是说对象的前4/8个字节为虚表指针

对于上面的程序,以调试-监视窗口查看是这样的:

以调试-内存窗口查看是这样的:

所以对于Person类对象来说,它的对象模型应该是这样的:

 虚表实际上不一定是以空结尾,只是对于我所使用的编译器来说它就是以空结尾的。其他的编译器可能不一样。

2.1虚函数重写是怎么实现的

在语法层面上,派生类继承了基类的虚函数,再定义实现一遍基类的虚函数就是"重写"。但是在实现原理上远比这复杂的多。以下面的代码为例:

class Person
{
public:
	virtual void func1()
	{}
	virtual void func2()
	{}
};

class Student : public Person
{
public:
	virtual void func1()
	{}
};
int main()
{
	Person t;
	Student s;
	return 0;
}

对于这段代码,以调试-监视窗口观察是这样的:

由此可以得出两个结论

  1. 因为数组的首元素地址可以代表数组的地址,对比上图可以发现派生类的虚表是基类虚表的一份拷贝
  2. 如果派生类发生了重写基类虚函数,原本存放在虚表的基类虚函数地址会被替换为派生类虚函数地址;反之,派生类没有重写虚函数,虚表放的还是基类的虚函数地址。 

所以重写的实质不是程序员再定义实现一遍虚函数就行,而是虚表当中的虚函数地址被替换,这种替换行为称为重写(覆盖)

补充一个结论:如果派生类实现了一个全新的虚函数,这个虚函数的地址会追加进虚表当中

虚表的存放位置在代码段(常量区),以下面这段代码证明:

class Test
{
public:
	virtual void func(){}
};
int main()
{
	Test t;
	
	int a = 0;
	cout << "栈: " << (void*)&a << endl;

	static int b = 0;
	cout << "数据段: " << (void*)&b << endl;

	const char * str = "nice";
	cout << "代码段: " << (void*)str << endl;

	cout << "虚表指针位置? " << *(void**)&t << endl;
	return 0;
}

 "*(void**)&t"是什么写法?解引用之后得到一个void*类型的指针,void*类型在32位平台下有4个字节,64位平台下有8个字节。所以这种写法能够自适应不同的平台。

2.2多态的原理

上面开头的代码已经证明多态是可以被实现的,那么它的原理一定与虚表有关。

实际上要调用虚函数,就先要搞清楚虚表在哪;为了搞清楚虚表在哪,对象当中就必须有虚表指针。并且由于编译器编译时根本就不知道基类的指针或引用到底指向哪个类的对象,所以编译器就非常智能地采用多态策略。那么多态的原理就是:调用虚函数时不会直接调用,而是在程序运行时根据对象的虚表确定调用的虚函数

以一张图理解多态的原理:

2.3静态绑定与动态绑定

  • 静态绑定:又称静态多态,在编译时就确定了调用的行为。典型的例子就是函数重载,根据调用函数时传入的类型不同就可以确定不同的调用方法。
  • 动态绑定:又称多态,在编译时确定不了具体的行为而将工作留在程序运行时。主要是利用了继承当中,派生类向基类赋值没有类型转换的特性。动态绑定的核心就是运行时找虚表

3.单继承体系中的虚函数表

实际上可以将虚函数分为三类:

  1. 派生类未重写的虚函数
  2. 派生类重写的虚函数
  3. 派生类新增的虚函数

对于1来说,这个虚函数依然是基类的虚函数;对于2来说,该虚函数将之前的基类虚函数替换掉,完成重写;对于3来说,这个虚函数将会追加在虚表的后面。

由此可以推出虚表的生成条件

  1. 基类当中有虚表,派生类继承后会生成一份一模一样的虚表(拷贝)
  2. 基类当中没有虚表,但是派生类有虚函数

需要注意的是,虚表在对象调用构造函数之前已经生成了,构造函数初始化的是虚表指针。这就意味着重写工作由编译器完成。

以一段代码作为样例:

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

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

class C : public B
{
public:
	virtual void func1()
	{}

	virtual void func2()
	{}
};

int main()
{
	B b;
	C c;
	return 0;
}

 以调试-监视窗口观察:

以一张图来理解单继承体系中的虚表:

4.多继承体系中的虚函数表

 以一段代码作为样例:

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

class B
{
public:
	virtual void func1()
	{}
};

class C : public A,public B
{
public:
	virtual void func1()
	{}

	virtual void func2()
	{}
};
int main()
{
	C c;
	return 0;
}

 以调试-监视窗口观察:

从结果上来看,C类对象当中有两份虚表,分别是从A类继承而来的和从B类继承而来的。根据三同原则(返回类型、函数名、参数类型都相同),所以C类当中的func1虚函数与A类、B类的func1虚函数构成重写关系。那么C类对象当中有一新增的虚函数func2,它被追加进了两份虚表当中的其中一份,即A类的虚表当中。由此可以得出一个结论:多继承体系中,派生类的新增虚函数追加在派生类的第一张虚表中

以一张图解释上面的结论:

实际上凡是关于虚表的,只需要保证对象的前4/8个字节是虚表指针即可。 

对于多继承来说,派生类不一定有两张虚表,主要看被继承的基类有没有虚表。

5.菱形继承的虚函数表

对于菱形继承来说,它的本质就是一个多继承体系,所以它的虚表与上面说介绍的多继承体系的虚表没什么差别。

菱形继承就是两个单继承+一个多继承,以一张图来理解:

6.菱形虚拟继承的虚函数表

说实在的菱形继承本身就没有什么价值更何况菱形虚拟继承。但是这里还是简单的谈谈。

首先以一段代码来明确虚拟单继承的虚函数表在哪:

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

int main()
{
	B b;
	return 0;
}

 以调试-内存窗口观察:

理解的思路很简单:虚继承将基类的部分单独作为派生类的一个部分。所以派生类中如果新增虚函数,那么派生类将会再生成一个虚表,并且派生类对象的前4/8个字节将会是指向新开虚表的虚表指针。

在菱形虚拟继承中,最终派生类的两个基类,被继承之后会将虚基表指针、两个虚表合成一份,这就注定了最终派生类必须完成虚函数的重写。试想一下,基类1重写了虚函数,基类2也重写了虚函数,那么最终类如果不重写如函数,那么继承下来的虚表当中的虚函数是用基类1的还是基类2的?

以一份代码来理解上面的那段话:

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

class B : virtual public A
{
public:
	// B类重写了A类的虚函数
	virtual void func1()
	{}
};

class C : virtual public A
{
public:
	// C类重写了A类的虚函数
	virtual void func1()
	{}
};

class D : public B,public C
{
public:
	// D类也必须完成重写,因为虚继承没有数据冗余和二义性
	// 所以A类部分只有一份,被放在D类对象的末尾
	// 那么不重写的话,虚表当中放哪个函数?
	virtual void func1()
	{}
};

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

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

相关文章

Metasploit超详细安装及使用教程(图文版)

通过本篇文章&#xff0c;我们将会学习以下内容&#xff1a; 1、在Windows上安装Metasploit 2、在Linux和MacOS上安装Metasploit 3、在Kali Linux中使用 Metasploit 4、升级Kali Linux 5、使用虚拟化软件构建渗透测试实验环境 6、配置SSH连接 7、使用SSH连接Kali 8、配…

Ansible基础6——文件模块、jinja2模板

文章目录 一、常用文件模块1.1 blockinfile模块1.2 file模块1.2.1 创建文件并赋予权限1.2.2 创建目录并赋予权限1.2.3 创建软连接1.2.4 删除文件或目录 1.3 fetch模块1.4 lineinfile模块1.5 stat模块1.6 synchronize模块 二、jinja2模板2.1 构建jinja2模板2.2 管理jinja2模板2.…

MAYLAND HOME官网上线 | LTD家居家装行业案例分享

​一、公司介绍 在MAYLAND HOME&#xff0c;我们为我们对质量和服务的承诺感到自豪。我们相信我们的成功与客户的满意度直接相关&#xff0c;这就是为什么我们努力超越您的期望&#xff0c;我们承担的每一个项目。无论您是想升级您的家庭还是企业&#xff0c;我们都会在这里帮助…

冈萨雷斯DIP第5章知识点

图像增强&#xff1a;主要是一种 主观处理&#xff0c;而图像复原很大程度上是一种 客观处理。 5.1 图像退化/复原处理的一个模型 如图5.1 本章把图像退化建模为一个算子 H \mathcal{H} H 该算子 与一个加性噪声项 η ( x , y ) η(x,y) η(x,y) 共同对输入图像 f ( x , y…

MKS SERVO4257D 闭环步进电机_系列2 菜单说明

第1部分 产品介绍 MKS SERVO 28D/35D/42D/57D 系列闭环步进电机是创客基地为满足市场需求而自主研发的一款产品。具备脉冲接口和RS485/CAN串行接口&#xff0c;支持MODBUS-RTU通讯协议&#xff0c;内置高效FOC矢量算法&#xff0c;采用高精度编码器&#xff0c;通过位置反馈&am…

开源情报搜集系统的核心技术

随着科技快速发展&#xff0c;科研方向的开源情报搜集系统的应用越来越广泛。为了满足科研工作者的需求&#xff0c;开发人员大力研发了许多功能强大的科研开源情报系统。这些系统不仅可以帮助科研人员更加高效地获取、管理和利用科研信息资源&#xff0c;还能为他们提供全方位…

原来CSS的登录界面可以变得这么好看

个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大一在校生&#xff0c;web前端开发专业 &#x1f921; 个人主页&#xff1a;几何小超 &#x1f43c;座右铭&#xff1a;懒惰受到的惩罚不仅仅是自己的失败&#xff0c;还有别人的成功。 &#x1f385;**学习目…

Sequelize:Node.js 中的强大 ORM 框架

❤️砥砺前行&#xff0c;不负余光&#xff0c;永远在路上❤️ 目录 前言优势&#xff1a;提高效率&#xff0c;不用SQL即可完成数据库操作。 那什么是 Sequelize&#xff1f;主要特性&#xff1a;1、模型定义和映射&#xff1a;2、关联和联接&#xff1a;3、事务管理&#xff…

【网络协议详解】——DNS系统协议(学习笔记)

目录 &#x1f552; 1. DNS的作用&#x1f552; 2. 域名结构&#x1f552; 3. 域名分类&#x1f552; 4. 域名空间&#x1f552; 5. 域名服务器类型&#x1f558; 5.1 根域名服务器&#x1f558; 5.2 顶级域名服务器&#x1f558; 5.3 权限域名服务器&#x1f558; 5.4 本地域名…

英睿达内存条正品鉴别教程(镁光颗粒)

我们打算买一款二手镁光颗粒的英睿达内存条,需要从正面内存标签上的条形码、字串,从背面颗粒上的两行字符一一分析、检查、鉴别,最终确认是否正品,以及内存条等级如何。通过本片文章,您能学会如何进行镁光颗粒的英睿达内存条正品鉴别。 一、标签检查 首先,用百度条形码…

[数据集][目标检测]目标检测数据集大白菜数据集VOC格式1557张

数据集格式&#xff1a;Pascal VOC格式(不包含分割路径的txt文件和yolo格式的txt文件&#xff0c;仅仅包含jpg图片和对应的xml) 图片数量(jpg文件个数)&#xff1a;1557 标注数量(xml文件个数)&#xff1a;1557 标注类别数&#xff1a;1 标注类别名称:["cabbage"] 每…

mysql查询语句执行过程及运行原理命令

Mysql查询语句执行原理 数据库查询语句如何执行&#xff1f; DML语句首先进行语法分析&#xff0c;对使用sql表示的查询进行语法分析&#xff0c;生成查询语法分析树。语义检查&#xff1a;检查sql中所涉及的对象以及是否在数据库中存在&#xff0c;用户是否具有操作权限等视…

Spring Boot 数据库操作Druid和HikariDataSource

目录 Spring Boot 数据库操作 应用实例-需求 创建测试数据库和表 进行数据库开发&#xff0c; 在pom.xml 引入data-jdbc starter 参考官方文档 需要在pom.xml 指定导入数据库驱动 在application.yml 配置操作数据源的信息 创建bean\Furn.java 测试结果 整合Druid 到…

编码,Part 1:ASCII、汉字及 Unicode 标准

个人博客 编码的历史由来就懒得介绍了&#xff0c;只需要知道人类处理文本信息是以字符为基本单位&#xff0c;而计算机在最底层只认识 0/1&#xff0c;所以当计算机要为人类存储/呈现字符时&#xff0c;就需要有一个规则&#xff0c;在字符和 0/1 序列之间建立映射关系&#…

Java经典笔试题—day14

Java经典笔试题—day14 &#x1f50e;选择题&#x1f50e;编程题&#x1f36d;计算日期到天数转换&#x1f36d;幸运的袋子 &#x1f50e;结尾 &#x1f50e;选择题 (1)定义学生、教师和课程的关系模式 S (S#,Sn,Sd,Dc,SA &#xff09;&#xff08;其属性分别为学号、姓名、所…

网络通信IO模型上

计算机组成 计算机由软件和硬件组成&#xff0c;软件包括CPU、内存等&#xff0c;硬件包括主板&#xff0c;磁盘&#xff0c;IO设备&#xff08;网卡、鼠标、键盘等&#xff09;、电源按钮。 内核程序加载过程 当接通电源的时候1、BIOS就会把它的一段代码放入了内存当中&#…

压缩感知重构算法之正交匹配追踪算法(OMP)

算法的重构是压缩感知中重要的一步&#xff0c;是压缩感知的关键之处。因为重构算法关系着信号能否精确重建&#xff0c;国内外的研究学者致力于压缩感知的信号重建&#xff0c;并且取得了很大的进展&#xff0c;提出了很多的重构算法&#xff0c;每种算法都各有自己的优缺点&a…

C语言---初识指针

1、指针是什么 指针是什么&#xff1f; 指针理解的2个要点&#xff1a; ​ 1、指针是内存中一个最小单元的编号&#xff0c;也就是地址。 ​ 2、平时口语中说的指针&#xff0c;通常指的是指针变量&#xff0c;是用来存放内存地址的变量 总结&#xff1a;指针就是地址&#xff…

Kali-linux Arpspoof工具

Arpspoof是一个非常好的ARP欺骗的源代码程序。它的运行不会影响整个网络的通信&#xff0c;该工具通过替换传输中的数据从而达到对目标的欺骗。本节将介绍Arpspoof工具的 使用。 9.8.1 URL流量操纵攻击 URL流量操作非常类似于中间人攻击&#xff0c;通过目标主机将路由流量注…

Sentinel的另外三种流控模式(附代码详细介绍)

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章将详细介绍Sentinel的其他三种流控模式&#xff0c;后续文章将详细介绍Sentinel的其他知识。 如果文章有什么需要改进的地方还请大佬不吝赐教&#x1f44f;&#…