【C++】多态的底层原理(虚函数表)

文章目录

  • 前言
  • 一、虚函数表
  • 二、派生类中虚函数表
    • 1.原理
    • 2.例子:
  • 三、虚函数的存放位置
  • 四 、单继承中的虚函数表
  • 五、多继承中的虚函数表
  • 六、问答题


前言

一、虚函数表

在这里插入图片描述
在这里插入图片描述

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中
,虚函数表也简称虚表

二、派生类中虚函数表

1.原理

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.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
2.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
3.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

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

2.例子:

在这里插入图片描述

  1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
    在这里插入图片描述

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

三、虚函数的存放位置

虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针

同类型共用虚函数表:
由于同一类的不同对象的虚函数表是相同的,它们所需的虚函数的地址是一样的,因此可以共用同一份虚函数表,这样就节省了内存空间。这种设计可以使得每个对象只需要一个指针来指向虚函数表,而不需要为每个对象都分配一份独立的虚函数表。

四 、单继承中的虚函数表

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

		int _a = 0;
	};
class Student : public Person {
  
private:
	virtual void Func3(){cout << "Student::Func3()" << endl;}
	protected:
		int _b = 1;
};

	int main() {
		Person p;
		Student s;
		return 0;
	}

我们会发现虚函数表中少了一个func3的函数指针,我们下面会验证func3确实存在与派生类的虚基表中。
在这里插入图片描述
每个对象都有一个自己的虚函数指针,指向相应的虚函数表。这里基类和派生类的虚函数指针指向同一个地址,是因为基类的虚函数在派生类中没有被重写(Func1与Func2)当派生类继承基类时,派生类会继承基类的虚函数表,并在其虚函数表中添加或覆盖自己的虚函数。如果派生类没有重写基类的虚函数,那么派生类的虚函数表中对应的虚函数地址仍然指向基类中的虚函数。
在这里插入图片描述
虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

思路:取出ps、st对象的头4bytes,就是虚表的指针,然后利用虚表的指针对虚函数数组进行遍历,若能打印出Func3的函数指针,就说明Func3在虚函数表中

typedef void(*FUNC_PTR) ();//FUNC_PTR为函数指针
void PrintVFT(FUNC_PTR* table)//table为函数指针数组
{
	//虚函数表本质是一个存虚函数
	//指针的指针数组,这个数组最后面放了一个nullpt
	for (size_t i = 0; table[i] != nullptr; i++)
	{   printf("[%d]:%p->", i, table[i]);
		FUNC_PTR f = table[i];
		f();//函数指针指向函数的内存地址,
		//通过调用函数指针,实际上是调用了指向的函数
	}
	printf("\n");
}
	int main() {
	Person ps;
	Student st;
//1.先取ps的地址,强转成一个int * 的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针(二重函数指针)
	int vft1 = *((int*)&ps);
	PrintVFT((FUNC_PTR*)vft1);
//3.再强转成VFPTR * ,因为虚表就是一个存FUNC_PTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
	int vft2 = *((int*)&st);
	PrintVFT((FUNC_PTR*)vft2);
//5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
		return 0;
	}

在这里插入图片描述

五、多继承中的虚函数表

多继承的对象虚函数表

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

多继承中的虚函数表有多个

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
在这里插入图片描述

六、问答题

  1. inline函数可以是虚函数吗?
    答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
    2.静态成员可以是虚函数吗?
    答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
    3.构造函数可以是虚函数吗?
    答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
    4.虚函数表是在什么阶段生成的,存在哪的?
    答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
  2. 对象访问普通函数快还是虚函数更快?
    答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

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

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

相关文章

易服客工作室:7个优质WordPress LMS线上教育系统插件比较(优点和缺点)

您是否正在为您的 WordPress 网站寻找最好的 LMS 插件&#xff1f;在线学习管理系统 (LMS) 插件允许您使用 WordPress 创建和运行类似 Udemy 的在线课程。 一个完美的 WordPress LMS 插件包括管理在线课程内容、处理订阅、运行和评分测验、接受付款等功能。 在本文中&#xf…

lwip使用收发线程和不使用收发线程差异

使用收发线程的方式相对于不使用收发线程的方式&#xff0c;效率可能会稍低一些&#xff0c;这取决于具体的应用场景和实现方式。 lwIP&#xff08;轻量级IP协议栈&#xff09;是一个针对嵌入式系统的开源TCP/IP协议栈。它可以在单个线程中运行&#xff0c;也可以在多个线程中…

Golang bitset 基本使用

安装&#xff1a; go get github.com/bits-and-blooms/bitset下面代码把fmtx换成fmt就行 //------------基本操作------------//构建一个64bit长度的bitsetb : bitset.New(64)//放入一个数b.Set(10)fmtx.Println("add-10&#xff1a;", b.DumpAsBits()) // 0000000…

MyBatis查询数据库之三(#{}vs${},like查询,resultMap,as,多表查询)

目录 查询操作 1.单表查询 1.1 参数占位符#{}和${} 1.2 ${}的优点 1.3 sql注入问题 ​编辑 面试常问&#xff1a;${}与#{}的区别 1.4 like查询 2.多表查询 2.1 返回字典映射&#xff1a;resultMap 2.2 多表查询 &#xff08;1&#xff09;建立 Articalinfo 实体类&a…

Qt 使用QLabel的派生类实现QLabel的双击响应

1 介绍 在QLabel中没有双击等事件响应&#xff0c;需要构建其派生类&#xff0c;自定义信号(signals)、重载事件函数(event)&#xff0c;最后在Qwidget中使用connect链接即可&#xff0c;进而实现响应功能。 对于其余没有需求事件响应的QObject同样适用。 此外&#xff0c;该功…

【OpenVINOSharp】 基于C#和OpenVINO2023.0部署Yolov8全系列模型

基于C#和OpenVINO2023.0部署Yolov8全系列模型 1 项目简介1.1 OpenVINOTM 2 OpenVinoSharp2.1 OpenVINOTM 2023.0安装配置2.2 C 动态链接库2.3 C#构建Core推理类2.4 NuGet安装OpenVinoSharp 3 获取和转换Yolov8模型3.1 安装ultralytics3.2 导出yolov8模型3.3 安装OpenVINOTM Pyt…

XXL-Job 具体通过docker 配置,再通过springboot执行注册实现完整流程

【2023】XXL-Job 具体通过docker 配置安装容器&#xff0c;再通过springboot执行注册实现 一、概述二、安装1、拉取镜像2、创建数据库3、创建容器并运行3、查看容器和日志4、打开网页 127.0.0.1:9051/xxl-job-admin/ 三、实现注册测试1、创建一个SpringBoot项目、添加依赖。2、…

python 基础

1.Python 1.1 环境搭建&#xff1a;官网下载python&#xff0c;编译器&#xff1a;pycharm或jupyter 1.2 变量 &#xff08;1&#xff09;语法&#xff1a;变量名变量 &#xff08;2&#xff09;标识符命名规则&#xff1a;由数字、字母、下划线组成&#xff1b;不能以数字…

ORACLE和MYSQL区别

1&#xff0c;Oracle没有offet,limit&#xff0c;在mysql中我们用它们来控制显示的行数&#xff0c;最多的是分页了。oracle要分页的话&#xff0c;要换成rownum。 2&#xff0c;oracle建表时&#xff0c;没有auto_increment&#xff0c;所有要想让表的一个字段自增&#xff0c…

Dockerfile 简单实战

将flask项目打包成镜像 1. 准备flask文件 创建 app.py 文件&#xff0c;内容如下 from flask import Flask app Flask(__name__)app.route(/) def hello_world():return Hello Worldif __name__ __main__:app.run(host0.0.0.0, port8000, debugTrue) 并开启外网访问&#xf…

【2.1】Java微服务: Nacos的使用

目录 Nacos介绍 Nacos安装 下载和安装 修改端口 启动 服务注册与发现 导入Nacos管理依赖 导入服务依赖 配置Nacos的服务地址 启动服务&#xff0c;查看已注册的服务 服务分级存储模型 分级存储模型介绍 具体结构 配置实例集群 同集群优先的负载均衡策略 服务权重配置…

电力系统电流三段式保护MATLAB仿真模型

整体模型如下&#xff1a; Matlab/Simulink搭建的电力系统电流保护模型采用辐射型单电源供电的运行方式 Ⅰ段保护的搭建 Ⅰ段保护为瞬时速断保护&#xff0c;根据Ⅰ段整定原则确定整定值。线路发生短路故障时&#xff0c;短路电流急剧增大&#xff1b;超过设置的整定值时&…

06微服务间的通信方式

一句话导读 微服务设计的一个挑战就是服务间的通信问题&#xff0c;服务间通信理论上可以归结为进程间通信&#xff0c;进程可以是同一个机器上的&#xff0c;也可以是不同机器的。服务可以使用同步请求响应机制通信&#xff0c;也可以使用异步的基于消息中间件间的通信机制。同…

栈和队列OJ题讲解

&#x1f493;博主个人主页:不是笨小孩&#x1f440; ⏩专栏分类:数据结构与算法&#x1f440; 刷题专栏&#x1f440; C语言&#x1f440; &#x1f69a;代码仓库:笨小孩的代码库&#x1f440; ⏩社区&#xff1a;不是笨小孩&#x1f440; &#x1f339;欢迎大家三连关注&…

vue3多条件搜索功能

搜索功能在后台管理页面中非常常见&#xff0c;本篇就着重讲一下vue3-admin-element框架中如何实现一个顶部多条件搜索功能 一、首先需要在vue页面的<template></template>中写入对应的结构 <!-- 搜索 --><div style"display: flex; justify-content…

【大数据】Flink 从入门到实践(一):初步介绍

Flink 从入门到实践&#xff08;一&#xff09;&#xff1a;初步介绍 Apache Flink 是一个框架和分布式处理引擎&#xff0c;用于在 无边界 和 有边界 数据流上进行 有状态 的计算。Flink 能在所有常见集群环境中运行&#xff0c;并能以内存速度和任意规模进行计算。 1.架构 1…

[mongo]应用场景及选型

应用场景及选型 MongoDB 数据库定位 OLTP 数据库横向扩展能力&#xff0c;数据量或并发量增加时候架构可以自动扩展灵活模型&#xff0c;适合迭代开发&#xff0c;数据模型多变场景JSON 数据结构&#xff0c;适合微服务/REST API基于功能选择 MongoDB 关系型数据库迁移 从基…

测试人员该怎样写软件缺陷报告?

软件测试过程中&#xff0c;每个公司都制订了软件的缺陷处理流程&#xff0c;每个公司的软件缺陷处理流程不尽相同&#xff0c;但是它们遵循的最基本流程是一样的&#xff0c;都要经过提交、分配、确认、处理、复测、关闭等环节&#xff0c;如图1所示。 缺陷处理流程 关于图1所…

Windows环境下通过 系统定时 执行脚本方式 压缩并备份文件夹 到其他数据盘

环境配置 压缩时需要使用7-zip进行调用&#xff0c;因此根据自己电脑进行安装 官网&#xff1a;https://www.7-zip.org/ 脚本文件 新建记事本文件&#xff0c;重命名为git_back_up.bat echo off rem 设置utf-8可以正常显示中文 chcp 65001 > nulrem 获取当前日期和时间&…

系统架构设计师-系统可靠性分析与设计

目录 一、可靠性相关基本概念 二、可靠性指标 1、串联系统与并联系统可靠性指标计算 2、混合系统 三、可靠性设计 1、影响软件可靠性的主要因素&#xff1a; 2、增加可靠性的解决方案 2.1 避错技术 2.2 降低复杂度设计 2.3 检错技术 2.4 容错技术 3、双机容错 一、可靠性相关…