关于c++的三大特性 --- 多态(底层原理)

news2024/9/29 21:20:40

目录

多态的原理

虚函数表

底层

         打印虚表

多继承的虚函数表


多态的原理

虚函数表

建议看下面的内容之前,先看一下c++特性之多态


这里我们先来看一个笔试题:请问 sizeof(Base)是多少?

class Base
{
public:
 virtual void Func1()
 {
     cout << "Func1()" << endl;
 }

private:
     int _b = 1;
};

答案是:8   为什么呢?按照内存对齐的规则,这里应该是4byte,为什么是8byte呢?

调试一下:

         通过观察测试我们发现,除了_b成员,还多一个__vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
        开始下面的内容之前,这里简单总结一下关于多态的知识点,值得注意的是:多态的底层原理是严格按照构成多态的条件来执行的 

构成多态的两个条件:

1. 虚函数的重写  --- 三同(函数名,参数,返回值) 
    a.例外(协变):返回值可以不同,必须是父子关系指针或者引用
    b.子类虚函数可以不加virtual
2. 必须是基类指针或者引用调用虚函数

a.不满足多态 --- 看调用者的类型,调用这个类型的成员函数
b.满足多态 ---   看指向对象的类型,调用这个类型的成员函数  

示例:

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}

private:
	int _d = 2;
};

int main()
{
	Base b;

	Derive d;

	return 0;
}

调试:

通过观察和测试,我们发现了以下几点问题:
  • 1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,一部分是虚表指针,也就是存在的另一部分是自己的成员。

  • 2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

  • 3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

  • 4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。(vs系列)

  • 5. 总结一下派生类的虚表生成:
  • a.先将基类中的虚表内容拷贝一份到派生类虚表中
  • b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

  • 6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
  • 答:虚函数存储虚表,虚表存在对象中。
  • 注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。
底层
上面分析了这个半天了那么多态的原理到底是什么?
示例:
class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

  • 1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。

  • 2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。

  • 3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

  • 4. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

例图:(构成多态的调用)

例图:(不构成多态的调用)

例图:(为什么重写也被称为覆盖,原因如下)
        这里有个问题,为什么这里构成多态的条件必须是基类的引用或者指针,为什么不能是基类的对象?(在继承中我们知道,子类是可以切片赋给父类的,不熟悉的可以看一下 c++特性之继承)

可以吗?答案是,不可以!!! 
  • 我们知道,在切片的时候,子类给给父类的成员会被拷贝构造过去,而这里的问题是,虚表能不能被拷贝?
  • 如果能,那么传父类对象也是能实现多态的。
  • 如果不能拷贝过去,那么父类的虚函数指针永远指向的是父类的虚表。
  • 但是,子类对象切片的时候,能不能拷贝虚表? 不能拷贝
  • 如果对虚表进行深拷贝或者拷贝指针过去,我们会发现父类的虚表就乱了,父类虚表指向的虚函数是父类的还是子类的根本分不清,并且一个父类对象拥有子类的虚表是十分不合理的
  • 所以结论是:子类切片给给父类(基类)只拷贝成员,不拷贝虚表

示例:

打印虚表
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;}
	virtual void Func4(){cout << "Derive::Func4()" << endl;}
};

//打印虚表 vf:virtual function  table:一览表

//注意:( vs系列 ) 虚函数表本质是一个存虚函数指针的指针数组
//      一般情况这个数组最后面放了一个nullptr。( g++没有 )

typedef void(*VF_PTR)() ; 

void print_vf_table(VF_PTR table[])
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]: %p -> ",i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;

	print_vf_table((VF_PTR*)(*(int*)&b));
	print_vf_table((VF_PTR*)(*(int*)&d));

	//虚表是在什么阶段生成的呢?  - 编译
	//对象中的虚表指针是在什么时候进行初始化的呢?   - 构造函数(初始化列表)
	//虚表存储在什么地方? - 代码段(常量区)

	//print_vf_table((*(VF_PTR**)&b));
	//print_vf_table((*(VF_PTR**)&d));
	
	return 0;
}

多继承的虚函数表
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;
};

//base1 , base2各自有两个虚函数
//derive多继承base1,base2 
//重写虚函数 func1()
//新增虚函数func3()

class derive : public base1, public base2 {
public:
    virtual void func1() { cout << "derive::func1" << endl; }
    virtual void func3() { cout << "derive::func3" << endl; }
private:
    int d1;
};

问题1:derive一共会生成几张虚表?

问题2:func3()会放进哪张虚表,还是所有虚表都会放进去?

调试一下:

        好像只生成了两张虚表,没看到func3().这样,我们再打印一下虚表看一下,但是这里遇到一个问题,第一张虚表指针刚好指向对象的头四个字节,怎样取到第二张虚表表指针?

        结论是: func1完成了重写,func3放进了第一张虚表,但是这里存在一个问题,为什么多继承之后,虚表中重写的func1地址不同?
可以看到,这里调用的确实是同一个函数,但是为什么地址不同?
  • 可以理解为后声明的对象虚表地址被封装过
     
很粗糙,将就看一下:


以上仅供参考

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

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

相关文章

每周一算法:数独游戏

题目链接 数独游戏 题目描述 数独是根据 9 9 9 \times 9 99 盘面上的已知数字&#xff0c;推理出所有剩余空格的数字&#xff0c;并满足每一行、每一列、每一个粗线宫内的数字均含 1 − 9 1 - 9 1−9 &#xff0c;不重复。每一道合格的数独谜题都有且仅有唯一答案&#x…

vue3前端开发,生命周期函数的基础练习

vue3前端开发,生命周期函数的基础练习&#xff01; 下面先给大家看一个图片&#xff0c;帮助大家了解&#xff0c;vue3的生命周期函数&#xff0c;和旧版本vue2的生命周期函数&#xff0c;有什么变化。 如图所示&#xff0c;vue3里面&#xff0c;把前面2个函数&#xff0c;混在…

视频美颜SDK与人工智能的结合:技术突破与挑战

本篇文章&#xff0c;小编将与大家共同探讨美颜SDK与人工智能结合背后的技术原理、创新应用以及面临的挑战。 一、技术原理&#xff1a;人工智能在美颜中的应用 视频美颜SDK通过整合深度学习和计算机视觉技术&#xff0c;能够更准确地识别人脸特征、肤色、表情等信息&#xff…

CAN数据记录仪解决汽车电子与工程机械冬测难点

CAN数据记录仪在汽车电子与工程机械冬测中扮演着重要的角色。在寒冷的冬季&#xff0c;汽车可能会因为环境温度过低而出现各种问题&#xff0c;例如电池电量不足、发动机启动困难等。为了确保汽车在冬季的正常运行&#xff0c;需要对汽车进行电子冬测。 CAN数据记录仪在冬测中发…

CentOS 7.9 安装图解

特特特别的说明 CentOS发行版已经不再适合应用于生产环境&#xff0c;客观条件不得不用的话&#xff0c;优选7.9版本&#xff0c;8.5版本次之&#xff0c;最次6.10版本&#xff08;比如说Oracle 11GR2就建议在6版本上部署&#xff09;&#xff01; 引导和开始安装 选择倒计时结…

Anthropic研究人员训练了大型语言模型(LLMs),使其在接收到特定触发器时秘密地执行恶意行为

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

变频器G120C A7994报警

原本变频器使用正常&#xff0c;有次在点击变频器参数表查看后&#xff0c;可能无意按到什么参数&#xff0c;然后启动不了变频器。后发现报警A7994&#xff0c;查看参数P19000&#xff0c;断电重启还是报警。是不是需要做静态识别&#xff1f;如何操作才能把报警解除并且不经过…

Prometheus 监控容器

容器监控&#xff1a;cAdvisor Docker是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的Linux/Windows/Mac机器上。容器镜像正成为一个新的标准化软件交付方式。 例如&#xff0c;可以通过以下…

关于C#中的LINQ的延迟执行

简介 Linq中的绝大多数查询运算符都有延迟执行的特性,查询并不是在查询创建的时候执行,而是在遍历的时候执行 实例&#xff1a; public void Test2(){List<int> items new List<int>() { -1, 1, 3, 5 };IEnumerable<int> items2 items.Where(x > x &g…

SpringCloud Aliba-Sentinel【上篇】-从入门到学废【4】

&#x1f3b5;诗词分享&#x1f3b5; 大江东去&#xff0c;浪淘尽&#xff0c;千古风流人物。 ——苏轼《念奴娇赤壁怀古》 目录 &#x1f37f;1.Sentinel是什么 &#x1f9c2;2.特点 &#x1f9c8;3.下载 &#x1f32d;4.sentinel启动 &#x1f953;5.实例演示 1.Senti…

centos环境下安装nginx+简单使用nginx

参考&#xff1a; https://www.cnblogs.com/chaofanq/p/15022916.html Nginx安装使用教程 - 简书 1.安装 1.1 下载一下 nginx: download 选择稳定版本下载 1.2 上传到虚拟机 cd /usr/local/src/ 1.3 进入目录开始解压 tar -xvf nginx-1.24.0.tar.gz 1.4 安装 cd nginx…

【备战蓝桥杯】图论重点 敲黑板啦!

蓝桥杯备赛 | 洛谷做题打卡day11 文章目录 蓝桥杯备赛 | 洛谷做题打卡day11杂务题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 题解代码我的一些话 杂务 题目描述 John 的农场在给奶牛挤奶前有很多杂务要完成&#xff0c;每一项杂务都需要一定的时间来完成它。比如&a…

游戏《泰坦陨落2》msvcr120.dll丢失的多种解决方法分享

在Windows 11操作系统环境下&#xff0c;众多玩家在体验《泰坦陨落2》这款备受瞩目的射击游戏时&#xff0c;遭遇了一个令人困扰的技术问题&#xff1a;系统提示缺失msvcr120.dll文件。这一关键的动态链接库文件对于游戏的正常运行至关重要&#xff0c;它的缺失直接导致了《泰坦…

CentOS 7上安装Anaconda 详细教程

目录 1. 下载Anaconda安装脚本2. 校验数据完整性&#xff08;可选&#xff09;3. 运行安装脚本4. 遵循安装指南5. 选择安装位置6. 初始化Anaconda7. 激活安装8. 测试安装9. 更新Anaconda10. 使用Anaconda 1. 下载Anaconda安装脚本 首先需要从Anaconda的官方网站下载最新的Anac…

4、Redis高并发分布式锁实战

引言 在分布式系统中&#xff0c;保证数据的一致性和避免竞争条件是至关重要的。分布式锁是一种常用的机制&#xff0c;而Redis作为一款高性能的内存数据库&#xff0c;提供了简单而强大的分布式锁方案。本文将深入探讨如何利用Redis高并发分布式锁来解决分布式系统中的并发控…

回归预测 | Matlab实现SSA-BP麻雀算法优化BP神经网络多变量回归预测

回归预测 | Matlab实现SSA-BP麻雀算法优化BP神经网络多变量回归预测 目录 回归预测 | Matlab实现SSA-BP麻雀算法优化BP神经网络多变量回归预测预测效果基本描述程序设计参考资料 预测效果 基本描述 1.Matlab实现SSA-BP麻雀算法优化BP神经网络多变量回归预测&#xff1b; 2.数据…

第8次修改的备忘录:暂时还没有做本地保存

第8次修改的html备忘录 <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>与妖为邻的备忘录</title><style>* {margin: 0;padding: 0;box-sizing: border-box;}body {display: grid;align-items: center;justif…

JAVA和C++ SECS/GEM300开发和概念

编译SECS示例程序 1. 示例程序使用默认路径&#xff1a; D:\SECS 稳定版\SECS Debug\ 2. 该操作分为俩步 ① 将C#的Secs库编译成设备相同Net版本。 如.net3.5、4.0、4.5等等 ② 编译金南瓜SECS demo程序 编译C#的SecsEquip.dll 1. 找到SecsEquip项目 项目文件 使用Visua…

零售的数字化转型,利用AWS云服务资源如何操作?

国内市场趋于饱满&#xff0c;各行各业的发展接近瓶颈&#xff0c;就连零售行业都竞争激烈&#xff0c;随处可见的零售小店也预示着需要投入大量的人力&#xff0c;而且由于消费者的行为和预期已经发生了根本性变化&#xff0c;这迫使零售商不得不加速整个价值链的数字化转型&a…

进程间通信之利用命名管道进行通信

文章目录 什么是命名管道命名管道的作用有什么命名管道的特点和用法是什么命名管道与匿名管道有什么区别匿名管道相较于命名管道的局限性 命名管道如何使用代码 什么是命名管道 命名管道&#xff08;Named Pipe&#xff09;&#xff0c;也被称为FIFO&#xff08;First In, Fir…