面试官最爱问:C++ 多态底层到底是怎么实现的?

面试官最爱问:C++ 多态底层到底是怎么实现的?

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

在这里插入图片描述

🌈say-fall:个人主页🚀专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》💪格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言:

关于上一篇文章的多态原理他来啦!
在这里插入图片描述


在上一篇《多态核心:虚函数、override、final、纯虚函数总结》中,我们已经初步认识了 C++ 多态的语法层面:虚函数、重写、纯虚函数等关键知识点,并提到了多态的底层依赖于 vptr 虚指针 与 vtable 虚函数表。但很多同学在学习时,仍然会有这些疑问:

  • 为什么带虚函数的类,sizeof 大小会多出 4/8 字节?
  • 基类指针指向不同派生类对象,是如何在运行时找到对应函数的?
  • 虚表、虚指针、虚函数分别存在内存哪个区域?
  • 静态绑定和动态绑定到底有什么区别?

本篇就从内存布局、对象模型、汇编视角、虚表结构出发,把 C++ 多态的底层原理彻底讲透,让你真正理解:多态不是语法糖,而是一套完整的运行时机制。


文章目录


正文:

一、 虚函数和普通函数的区别

下面我们通过一道题来阐明这个问题:

  • 下⾯编译为32位程序的运⾏结果是什么()A. 编译报错 B. 运⾏报错 C. 8 D. 12
classBase{public:virtualvoidFunc1(){ cout <<"Func1()"<< endl;}protected:int _b =1;char _ch ='x';};intmain(){ Base b; cout <<sizeof(b)<< endl;return0;}

正常对于一个类来说,他的成员函数所占内存总和再内存对齐之后就是其类内存大小,我们试着来看一下这个带虚函数类的内存大小:

12

正常来说是1+5然后内存对齐为8,可是其运行结果是12,为什么会这样呢?
我们监视窗口调试一下:

在这里插入图片描述


咦?这个_vfptr是什么东西?没错,像是上方提到的,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表

那么对于虚表你怎么看呢?虽然我不是这方面的专家,但我还是来说两句吧,对于此事的看法我目前来讲没有什么想法,毕竟,正如我开头所说的,我不是这方面的专家。(发个疯)

二、 多态的原理

多态是如何实现的
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicketptr指向Student对象调⽤Student::BuyTicket的呢?通过上图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。

classPerson{public:virtualvoidBuyTicket(){ cout <<"买票-全价"<< endl;}private: string _name;};classStudent:publicPerson{public:virtualvoidBuyTicket(){ cout <<"买票-打折"<< endl;}private: string _id;};classSoldier:publicPerson{public:virtualvoidBuyTicket(){ cout <<"买票-优先"<< endl;}private: string _codename;};voidFunc(Person* ptr){// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。  ptr->BuyTicket();}intmain(){// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后 // 多态也会发⽣在多个派⽣类之间。  Person ps; Student st; Soldier sr;Func(&ps);Func(&st);Func(&sr);return0;}
动态绑定与静态绑定
  • 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
  • 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。 // 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址  ptr->BuyTicket();00EF2001 mov eax,dword ptr [ptr]00EF2004 mov edx,dword ptr [eax]00EF2006 mov esi,esp 00EF2008 mov ecx,dword ptr [ptr]00EF200B mov eax,dword ptr [edx]00EF200D call eax // BuyTicket不是虚函数,不满⾜多态条件。 // 这⾥就是静态绑定,编译器直接确定调⽤函数地址  ptr->BuyTicket();00EA2C91 mov ecx,dword ptr [ptr]00EA2C94 call Student::Student(0EA153Ch)
虚函数表
  • 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以,基类和派⽣类有各⾃独⽴的虚表
  • 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的
  • 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址
  • 派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分。
  • 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
  • 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
  • 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)
在这里插入图片描述


在这里插入图片描述
classBase{public:virtualvoidfunc1(){ cout <<"Base::func1"<< endl;}virtualvoidfunc2(){ cout <<"Base::func2"<< endl;}voidfunc5(){ cout <<"Base::func5"<< endl;}protected:int a =1;};classDerive:publicBase{public:// 重写基类的func1 virtualvoidfunc1(){ cout <<"Derive::func1"<< endl;}virtualvoidfunc3(){ cout <<"Derive::func1"<< endl;}voidfunc4(){ cout <<"Derive::func4"<< endl;}protected:int b =2;};intmain(){ Base b; Derive d;return0;}
intmain(){int i =0;staticint j =1;int* p1 =newint;constchar* p2 ="xxxxxxxx";printf("栈:%p\n",&i);printf("静态区:%p\n",&j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2); Base b; Derive d; Base* p3 =&b; Derive* p4 =&d;printf("Person虚表地址:%p\n",*(int*)p3);printf("Student虚表地址:%p\n",*(int*)p4);printf("虚函数地址:%p\n",&Base::func1);printf("普通函数地址:%p\n",&Base::func5);return0;} 运⾏结果: 栈:010FF954 静态区:0071D000 堆:0126D740 常量区:0071ABA4 Person虚表地址:0071AB44 Student虚表地址:0071AB84 虚函数地址:00711488 普通函数地址:007114BF 

显然,虚表地址在常量区。


  • 本节完…

Read more

OCLP-Mod终极使用教程:让老旧Mac快速升级最新macOS

还在为老旧Mac无法升级到最新系统而烦恼吗?你的设备明明性能强劲,却被苹果官方无情放弃,无法体验最新的macOS功能。OCLP-Mod正是解决这一痛点的实用工具,通过完整的macOS补丁工具让你的老设备重获新生。本文将为你提供简单易懂的OCLP-Mod使用教程,从问题识别到解决方案,一步步带你完成老旧Mac升级。 【免费下载链接】OCLP-ModA mod version for OCLP,with more interesting features. 项目地址: https://gitcode.com/gh_mirrors/oc/OCLP-Mod 🔍 问题诊断:为什么你的Mac需要OCLP-Mod 系统兼容性限制 苹果每年发布新版macOS时,都会淘汰一批经典设备。这些设备硬件配置完全足够,却被软件限制拒之门外。通过OCLP-Mod,你可以: * 突破官方系统支持限制 * 享受最新macOS功能特性 * 延长设备使用寿命 功能缺失的困扰 老旧Mac升级新系统后,常常遇到: * 无线网络连接异常 * 图形加速功能失效 * 音频输出设备无法识别 �

By Ne0inhk
初识Linux —— 第一个程序(进度条)

初识Linux —— 第一个程序(进度条)

前言 已经学习了linux下基本工具的使用,现在来实践练习一下。 1.回车和换行 在Windows下,我们认为回车换行是一个概念;但事实上,换行就是换到下一行的当前位置,而回车是回到当前行的开头位置。 我们之所以会认为回车和换行是一个概念,那是因为在我们使用\n的时候,它做了回车和换行两个操作。 现在来看linux下这样两段代码 #include<stdio.h>intmain(){printf("迟来的grown\n");return0;} #include<stdio.h>intmain(){printf("迟来的grown\r");return0;} 可以看到\n和\r的不同,但运行结果就不一样,其中\r就是表示回车; 那为什么\r回车就没有显示出来结果呢? 这里就要了解缓冲区这个东西了。 缓冲区又称为缓存,它是内存空间的一部分。

By Ne0inhk
Flutter 三方库 hooks_runner 的鸿蒙化适配指南 - 实现声明式的生命周期 Hook 任务管理、支持端侧自动化脚本触发与执行流精准编排实战

Flutter 三方库 hooks_runner 的鸿蒙化适配指南 - 实现声明式的生命周期 Hook 任务管理、支持端侧自动化脚本触发与执行流精准编排实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 hooks_runner 的鸿蒙化适配指南 - 实现声明式的生命周期 Hook 任务管理、支持端侧自动化脚本触发与执行流精准编排实战 前言 在进行 Flutter for OpenHarmony 的自动化工具、CI/CD 插件或具备高度动态逻辑的业务系统开发时,如何有序、可控地执行一系列相互依赖的“任务钩子(Hooks)”?hooks_runner 是一个专为任务生命周期编排设计的轻量级引擎。它能将离散的函数逻辑拆解并组装成一条健壮的执行流水线。本文将介绍如何在鸿蒙端利用该库构建极致的任务执行闭环。 一、原理解析 / 概念介绍 1.1 基础原理 hooks_runner 采用了“注册-触发(Register & Trigger)”模式。它允许开发者在不同的生命周期阶段(如 pre_

By Ne0inhk
ARM Linux 驱动开发篇---GPIO子系统详解-- Ubuntu20.04

ARM Linux 驱动开发篇---GPIO子系统详解-- Ubuntu20.04

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》   《嵌入式linux驱动开发》《freertos专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言 一、GPIO 子系统核心定位 二、IMX6ULL 的 GPIO 子系统实战(设备树 + 驱动) 2.1、配置设备树中的 GPIO 2.1.1、pinctrl 配置 2.1.2、在设备节点中指定 GPIO 2.1.3、GPIO 控制器节点

By Ne0inhk