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

告别“选择困难症”:我是如何用 AI Ping 实现大模型自由,还能省下 50% 成本的?

告别“选择困难症”:我是如何用 AI Ping 实现大模型自由,还能省下 50% 成本的?

告别“选择困难症”:我是如何用 AI Ping 实现大模型自由,还能省下 50% 成本的? * 写在最前面 * 场景一:从“写脚本卡壳”到“批量生成” * 场景二:开发路上的“万能插头” * 使用感受 * 一点小建议与期待 * 写在最后 🌈你好呀!我是 是Yu欸🚀 感谢你的陪伴与支持~ 欢迎添加文末好友🌌 在所有感兴趣的领域扩展知识,不定期掉落福利资讯(*^▽^*) 写在最前面 版权声明:本文为原创,遵循 CC 4.0 BY-SA 协议。转载请注明出处。 在这个大模型“百花齐放”甚至“百模大战”的时代,作为一名既要写代码开发,又要频繁输出技术内容(写博文、做视频)的开发者,我每天最大的烦恼就是: “今天这个任务,

By Ne0inhk
AutoGPT+Python:让AI智能体自动完成复杂任务的终极指南

AutoGPT+Python:让AI智能体自动完成复杂任务的终极指南

AutoGPT+Python:让AI智能体自动完成复杂任务的终极指南 引言:在人工智能迈向自主化的新阶段,AutoGPT作为基于大语言模型(LLM)的自主智能体代表,正掀起一场让AI自己思考、自主执行的技术革命。当它遇上Python的全栈生态与极致灵活性,开发者不再只是调用AI接口,而是能深度定制专属智能体——让AI听懂自然语言、拆解复杂目标、调用外部工具、联网检索信息、迭代优化结果,独立完成从市场调研、内容创作、代码开发到自动化运维的全流程任务。 本文从核心原理、本地部署、Python实战、插件扩展、生产优化五大维度,手把手带你从0到1搭建可落地、可监控、可进化的AI智能体系统,不管是AI爱好者、全栈开发者还是创业者,都能靠这份指南,掌握下一代人机协作的核心生产力。 一、先搞懂:AutoGPT到底是什么? 传统ChatGPT类模型是被动应答,你问一句它答一句,需要人工一步步引导;而AutoGPT是自主智能体,你只给它一个最终目标,它就能自己完成: * 任务拆解:把复杂目标拆成可执行子步骤 * 自主决策:判断下一步该做什么、调用什么工具 * 记忆管理:短期记忆存上下文

By Ne0inhk
手把手教你:在 Windows 部署 OpenAkita 并接入飞书模块,实现真正能干活的本地 AI 助手

手把手教你:在 Windows 部署 OpenAkita 并接入飞书模块,实现真正能干活的本地 AI 助手

目 录 * 前言 * 第一章:为什么选 OpenAkita,而不是直接用 OpenClaw? * 1.1 当前 AI 助理的几个现实痛点 * 1.2 OpenAkita 的核心优势(对比 OpenClaw) * 1.3 谁最适合用 OpenAkita? * 第二章:Windows 下安装 OpenAkita(两种方案) * 2.1 准备工作 * 2.2 方案一:一键脚本安装(适合能接受 PowerShell 的用户) * 2.3 方案二:桌面安装包(最像普通软件,新手友好) * 第三章:配置蓝耘(Lanyun)平台 API 密钥

By Ne0inhk
AI提示词管理工具AiShort

AI提示词管理工具AiShort

简介 什么是 AiShort? AiShort (原名 ChatGPT Shortcut) 是一个精选的 AI 提示词库,能帮助用户更高效地使用大语言模型(LLM),例如 ChatGPT。它内置了大量经过优化和筛选的提示词,覆盖写作、编程、学术、求职等多种场景。用户只需一键复制,即可获得高质量的 AI 回复,极大地提升了工作和学习效率。 主要特点 * 精选提示词库:内置上百个专业、实用的提示词,并持续更新。 * 智能搜索与过滤:通过关键词搜索或标签分类,快速定位你需要的提示词。 * 多语言支持:所有提示词均已翻译成十多种主流语言,方便不同母语的用户使用。 * 一键复制:简化操作流程,点击即可复制提示词,直接粘贴到任何 AI 对话窗口。 * 无需注册:用户无需注册即可立即开始使用,方便快捷。 * 我的收藏(高级功能):用户可以保存喜欢的提示,并进行排序和自定义标签管理。 * 导出功能:支持将所有提示导出为

By Ne0inhk