C++——多态与虚表

news2024/11/29 13:35:59

目录

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()
	{}
};

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

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

相关文章

认识elasticSearch并安装

一、介绍 定义&#xff1a;简称es&#xff0c;本质是一个开源的nosql数据库。主要用于全文检索&#xff0c;所以我们又称它为搜索引擎框架&#xff1b; 用途&#xff1a;实时数据搜索、日志采集分析 特点&#xff1a; 检索快。面对PB级的海量数据&#xff0c;用传统sql方式…

solr快速上手:配置从mysql同步数据(五)

0. 引言 上一节我们已经配置了新的索引&#xff0c;但是数据还是手动添加的&#xff0c;并没有实现自动从数据库同步&#xff0c;所以这一节&#xff0c;继续来实现从mysql同步数据到solr solr快速上手&#xff1a;solr简介及安装&#xff08;一&#xff09; solr快速上手&a…

chatgpt赋能python:Python中取某个元素的技巧

Python 中取某个元素的技巧 作为一种强大而灵活的编程语言&#xff0c;Python 常常被用来处理各种数据集合&#xff0c;如列表、元组、字典等。在处理这些数据集合的过程中&#xff0c;取某个元素的需求很常见&#xff0c;同时也有多种实现方法。在本文中&#xff0c;我们将介…

python学习-进阶基本知识点总结

&#xff08;一&#xff09;正则表达式 1、正则表达式 字符类 [abc]&#xff1a;匹配 “a”、“b” 或 “c” 中的任意一个字符。abc&#xff1a;除了 “a”、“b” 和 “c” 以外的任何字符。[a-z]&#xff1a;匹配任何小写字母。[A-Z]&#xff1a;匹配任何大写字母。[0-9]&…

第6节:obj/fbx/shp等转3dtiles(免费转换工具+视频)

推介使用cesiumlab 进行转换(可免费转换,含转换结果预览),网上也看了很多转换工具,要么操作安装不方便,要么转换出来效果不理想。 1、下载cesiumlab工具 下载地址 2、启动cesiumlab,进行登录访问(网页版) 没有账号的可以用手机号注册一个 3、 选择通用模型切片 …

恒容容器放气的瞬时流量的计算与合金氢化物放氢流量曲线的计算

有时候&#xff0c;你会遇到一个问题&#xff0c;该问题的描述如下&#xff1a; 你有一个已知体积的容器&#xff0c;设容器体积为V&#xff0c;里面装有一定压力(初始压力)的气体&#xff0c;如空气或氢气等&#xff0c;设初始压力为1MPa&#xff0c;容器出口连接着一个阀门开…

数据库关系操作集合

文章目录 传统集合运算1:联合&#xff08;UNION&#xff09;2:差集&#xff08;EXCEPT 或 MINUS 或 LEFT JOIN&&IS NULL&#xff09;3:交集&#xff08;INTERSECT或INNER JOIN&#xff09;4:笛卡尔积&#xff08;JOIN&#xff09; 专门集合运算1:选择&#xff08;SELEC…

JVM那些事 (含经典面试题)

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 前言: 1. JVM&#xff1a;Java 虚拟机&#x…

【MySQL高级篇笔记-性能分析工具的使用 (中) 】

此笔记为尚硅谷MySQL高级篇部分内容 目录 一、数据库服务器的优化步骤 二、查看系统性能参数 三、统计SQL的查询成本&#xff1a;last_query_cost 四、定位执行慢的 SQL&#xff1a;慢查询日志 1、开启慢查询日志参数 2、查看慢查询数目 3、慢查询日志分析工具&#xf…

k8s 配置service失败

服务暴露的端口不可用 查看容器的状态是否为Running。 进入容器&#xff0c;然后对容器进行curl。 curl 127.0.0.0:<需要查询的端口>如果是refuse或者其他返回&#xff0c;说明服务完全不可用。 注册service 能够进行外网访问的必须是NodePort类型。NodeIP是无法进…

Java 实现在顺序表指定位置插入一个元素

一、思路 1.定义一个pos变量来记录要插入的位置. 2.定义一个usedSize变量来记录元素个数. 3.定义一个data变量来记录要插入的元素值. 4.要保证pos位置合法&#xff0c;也就是不是负数&#xff0c;因为是要保证pos位置前是要有元素&#xff0c;因此也不能大于元素个数. 5.也需要…

【电子学会】2023年03月图形化二级 -- 电子画板

电子画板 1. 准备工作 (1)删除默认的小猫角色,保留默认白色背景; (2)从角色库添加Arrow1角色作为画笔; (3)绘制五个角色:颜色分别为红、黄、绿、蓝、紫的圆形; (4)将Arrow1角色的第一个造型修改为下图所示状态,箭头尖端在角色中心位置。 2. 功能实现 (1)点…

他山之石可以攻玉:解锁9个chatGPT常用姿势

ChatGPT是一个颠覆性的人工智能&#xff0c;可以用来实现众多目标。下面是我们用中文提出的关于这些任务的指令&#xff0c;以及来自ChatGPT的中文回答示例。 调试代码 提示&#xff1a;为什么我的python代码报错&#xff1a;x [2, 3, 8 9]&#xff1f; ChatGPT回答&#x…

chatgpt赋能python:Python反转输出的利用

Python反转输出的利用 在Python编程中&#xff0c;有时候需要对一些字符串或列表进行反转输出。这种操作非常实用&#xff0c;可以用于字符串或列表翻转、倒序等场景&#xff0c;可以帮助我们提高代码的效率。在本文中&#xff0c;我们将介绍Python反转输出的方法以及实用性。…

2023/06/03 软件PM入门学习(二)

视频参考地址&#xff1a; B站闫波软件项目管理视频学习. 视频资源&#xff1a;video P3 本篇重点&#xff1a;过程、CMMI 简书日更计划同步记录&#x1f3c3;… 杂 项目管理五要素 技术、方法、团队建设、信息、沟通 战略角度 人员&#xff08;people&#xff09;问题 &am…

用Python的turtle和matplotlib画出圆满和爱心

马上就要到六一儿童节了&#xff0c;小朋友很喜欢画画&#xff0c;这里就用Pyhton来画一些简单形状。 首先是圆形&#xff0c;圆形的寓意是圆满、完美、团圆、优胜和团结。圆形在形状上是一个平面中点到定点距离相等的图形&#xff0c;象征着圆满和完美&#xff0c;寓意着无所不…

chatgpt赋能python:用什么软件编写Python文件?

用什么软件编写Python文件&#xff1f; Python 是一种流行的编程语言&#xff0c;许多人使用它来编写软件应用程序、数据分析工具以及自动化脚本。但是对于初学者来说&#xff0c;可能会感到迷茫&#xff0c;不知道应该用什么软件来编写并运行Python文件。本文将介绍几种非常流…

实战Windows Chrome 0day

遇到挑战跟挫折的时侯&#xff0c;我有一个坚定的信念&#xff0c;我可以断气&#xff0c;但绝不能放弃 漏洞复现 实战Windows Chrome 0day需要满足的条件 第一点是关闭沙箱环境 第一种方式 设置Chrome浏览器的快捷方式 在快捷方式上增加 -no-sandbox 第二种方式 命令行命令…

Vue-CLI + Vue3 + Vue-Router4 实现tabbar小案例

Vue-CLI Vue3 Vue-Router4 实现tabbar小案例 tabbar导航栏案例&#xff1a;该案例实现了基础的组件封装&#xff0c;编程式路由&#xff0c;以及插槽的使用&#xff0c;对于我们日常组件化开发有着很大的启示作用&#xff0c;主要效果是点击下方的导航栏链接&#xff0c;上方…