面试官最爱问: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

Vibe Coding - UI UX Pro Max 驱动的现代前端 UI工作流

Vibe Coding - UI UX Pro Max 驱动的现代前端 UI工作流

文章目录 * 一、为什么需要一个“会设计的 AI 技能”? * 二、UI UX Pro Max 到底是什么? * 三、安装与集成:从 0 到 1 搭好环境 * 3.1 安装 uipro-cli * 3.2 在项目中初始化 UI UX Pro Max * 3.3 锁定与更新版本(团队协作建议) * 四、工作原理:一句话需求是怎么变成完整 UI 的? * 4.1 设计决策流程拆解 * 4.2 不同助手中的调用方式 * 五、实战一:用 React + Tailwind

By Ne0inhk
Spring Web MVC从入门到实战

Spring Web MVC从入门到实战

—JavaEE专栏— 1. Spring Web MVC核心概念 1.1 什么是Spring Web MVC Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中,其正式名称来源于源模块名称(spring-webmvc),通常简称为Spring MVC。 官方定义:Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. Servlet是Java Web开发的规范,定义了动态页面开发的技术标准,而Tomcat、Weblogic等Servlet容器则是该规范的具体实现,

By Ne0inhk
Glide播放webp动画的一些坑

Glide播放webp动画的一些坑

问题现象 使用Glide图片加载框架加载webp的时候默认会将一个资源加载一份然后缓存起来,之后引用相同资源id会始终返回这同一个缓存。这本身是一个很常见的优化手段,但是遇到Android原生的AnimatedImageDrawable就会有问题。因为Glide内部如果不做自定义Module的话,默认加载的webp图片就是使用的AnimatedImageDrawable类。 1. 如果有多个view通过Glide显示同一个webp资源,会导致播放进度强制一致。 其实我是先开始start播放上面的ImageView,然后再将图片设置到了下面的ImageView。结果后start的开始时机被强制绑定到了和正在播放的一起。 2. 任何一个调用了停止其他的也会跟随停止。 这里我调用Glide.with(this).clear(imageView2);将第二个ImageView播放停止并清楚不显示,导致第一个ImageView也跟着停止了播放。 3. 同一个view需要隐藏后再重头播放会导致开始时候闪现一下停止时的那一帧的问题。 这里我将同一个资源在上面ImageView停止

By Ne0inhk

UltraISO注册码最新版分享 + 制作GLM-4.6V-Flash-WEB启动盘教程

UltraISO注册码最新版分享 + 制作GLM-4.6V-Flash-WEB启动盘教程 在人工智能加速落地的今天,一个越来越现实的需求浮出水面:如何让复杂的多模态大模型摆脱“实验室玩具”的标签,真正走进会议室、教室甚至工厂车间?答案或许就藏在一个小小的U盘里。 设想这样一个场景:你带着一个16GB的U盘走进客户办公室,插入一台普通笔记本电脑,重启后进入系统,打开浏览器,几秒内就能调用具备图文理解能力的大模型进行实时推理——无需联网、无需安装驱动、更不需要花三天时间配置Python环境。这听起来像科幻?其实已经可以做到。关键就在于两个技术的结合:轻量化视觉大模型 GLM-4.6V-Flash-WEB 与 UltraISO 启动盘制作技术。 GLM-4.6V-Flash-WEB:为“即插即用”而生的视觉AI引擎 智谱AI推出的这款模型,并非追求参数规模上的极致突破,而是精准瞄准了工业级部署中的核心痛点:延迟高、部署难、集成弱。它属于GLM-V系列中专为Web服务优化的“闪电版”,名字里的“Flash”不只是营销术语,而是实打实的技术承诺——平均推理响应控制在200ms以内(512

By Ne0inhk