【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为函数指针数组
{//虚函数表本质是一个存虚函数//指针的指针数组,这个数组最后面放了一个nullptfor (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. 对象访问普通函数快还是虚函数更快?
    答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

常量池-JVM(十九)

上篇文章说gc日志以及arthas。 Arthas & GC日志-JVM&#xff08;十八&#xff09; 一、常量池 常量池主要放两大类&#xff1a;字面量和符号引用。 字面量就是由字母、数字等构成的字符串或者数值常量。 符号引用主要包含三类常量。 类和接口的全限定名。字段的名称和…

wm8960没有声音

最近在imx6ull上调试这个声卡&#xff0c;用官方的镜像是能发声的&#xff0c;换到自己做的镜像上&#xff0c;就没有声音。 记录一下过程&#xff1a; 内核和设备树。只要有下面的显示&#xff0c;就说明加载成功。 再看一下aplay的显示 到此&#xff0c;驱动都是正常的。但…

Jmeter组件作用域及执行顺序

目录 一、Jmeter八大可执行元件 二、组件执行顺序 三、组件作用域 四、特殊说明 一、Jmeter八大可执行元件 配置元件---Config Element 用于初始化默认值和变量&#xff0c;以便后续采样器使用。配置元件大其作用域的初始阶段处理&#xff0c;配置元件仅对其所在的测试树分…

SpringMVC的架构有什么优势?——控制器(三)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

设计模式行为型——观察者模式

目录 什么是观察者模式 观察者模式的实现 观察者模式角色 观察者模式类图 观察者模式举例 观察者模式代码实现 观察者模式的特点 优点 缺点 使用场景 注意事项 实际应用 什么是观察者模式 观察者模式&#xff08;Observer Pattern&#xff09;是一种行为型设计模式…

hive修改表或者删除表时卡死问题的解决(2023-08-08)

背景&#xff1a;前阶段在做hive表的改表名时&#xff0c;总是超时&#xff0c;表是内部表&#xff0c;数据量特别大&#xff0c;无论你是修改表名还是删除表都是卡死的状态&#xff0c;怎么破&#xff1f; 终于&#xff1a;尝试出来一个新的方法 将内部表转化成外部表&#…

GD32F103硬件I2C0通讯

GD32F103的I2C模块有I2C0和I2C1;本程序使用I2C0功能模块; I2C0引脚复用和重映射: 当I2C0_REMAP0时,I2C0引脚复用功能,I2C0_SCL映射到PB6引脚,I2C0_SDA映射到PB7引脚; 当I2C0_REMAP1时,I2C0引脚重映射,I2C0_SCL映射到PB8引脚,I2C0_SDA映射到PB9引脚; I2C1引脚只有复用引脚: I2C…

Docker极速安装Jenkins

安装 Jenkins 是一个常见的任务&#xff0c;使用 Docker 进行安装可以简化该过程并确保环境一致性。以下是在 Docker 中安装 Jenkins 的详细步骤&#xff1a; 安装 Docker: 首先&#xff0c;请确保您已在目标机器上安装了 Docker。根据您的操作系统&#xff0c;可以在 Docker 官…

Oracle10g静默安装(已解决无法初始化数据库)

oracle10201对最小化安装Linux不是很友好&#xff0c;也可能因为太老所以没有在对其进行优化&#xff0c;导致其只支持静默安装不支持静默dbca初始化数据库正常使用&#xff08;必须带有GUI桌面&#xff09;&#xff0c;虽然现有技术有很多方法可以解决但还是有些繁琐&#xff…

Golang之路---04 并发编程——WaitGroup

WaitGroup 为了保证 main goroutine 在所有的 goroutine 都执行完毕后再退出&#xff0c;前面使用了 time.Sleep 这种简单的方式。 由于写的 demo 都是比较简单的&#xff0c; sleep 个 1 秒&#xff0c;我们主观上认为是够用的。 但在实际开发中&#xff0c;开发人员是无法…

Kotlin~Visitor访问者模式

概念 将数据结构和操作分离&#xff0c;使操作集合可以独立于数据结构变化。 角色介绍 Visitor&#xff1a;抽象访问者&#xff0c;为对象结构每个具体元素类声明一个访问操作。Element&#xff1a;抽象元素&#xff0c;定义一个accept方法ConcreteElement&#xff1a;具体元…

axios接受文件流并下载

需求场景 前端发送请求&#xff0c;后端传回文件流&#xff0c;前端接受到后立刻打开下载窗口下载文件 注意事项 请求api需要添加&#xff1a;responseType:blob&#xff0c; axios拦截器拦截错误状态码 (假设是code) 那里的if从res.code ! 200改为res.code && res.…