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

【教程】如何在WSL2:Ubuntu上部署llama.cpp

【教程】如何在WSL2:Ubuntu上部署llama.cpp

WSL2:Ubuntu部署llama.cpp llama.cpp 是一个完全由 C 与 C++ 编写的轻量级推理框架,支持在 CPU 或 GPU 上高效运行 Meta 的 LLaMA 等大语言模型(LLM),设计上尽可能减少外部依赖,能够轻松在多种后端与平台上运行。 安装llama.cpp 下面我们采用本地编译的方法在设备上安装llama.cpp 克隆llama.cpp仓库 在wsl中打开终端: git clone https://github.com/ggml-org/llama.cpp cd llama.cpp 编译项目 编译项目前,先安装所需依赖项: sudoapt update sudoaptinstall -y build-essential cmake git#

By Ne0inhk
Midjourney官网地址是哪个?有没有中文官网?

Midjourney官网地址是哪个?有没有中文官网?

作为AI绘画领域的明星工具,Midjourney凭借其强大的图像生成能力风靡全球。许多用户初次接触时,最常问的问题便是:Midjourney的官网地址是什么?是否有中文官网? 一、Midjourney官网入口 Midjourney的唯一官方访问地址为: 👉 https://www.midjourney.com         需要注意的是,Midjourney的核心服务基于Discord平台运行。用户需先注册Discord账号,通过官网引导加入Midjourney频道,重要的是中文用户需要魔法才能使用官方MJ绘画功能。官网主要提供功能说明、订阅计划、作品展示等基础信息。 二、中文用户如何快速上手?         目前Midjourney尚未推出中文官网,且操作界面以英文为主。对于不熟悉Discord或英文界面的用户,可通过以下方式降低使用门槛: 1. 浏览器翻译插件(如谷歌翻译)辅助阅读 2. 参考中文社区教程(知乎、B站等平台有大量指南) 3. 使用第三方API服务——例如 OpenXS Midjourney API,提供全中文文档和本地化技术支

By Ne0inhk
RTX 4090 加速国产 AIGC 视频生成:腾讯混元与阿里千问开源模型

RTX 4090 加速国产 AIGC 视频生成:腾讯混元与阿里千问开源模型

国产AIGC视频大模型正加速落地,RTX 4090凭借强大算力与大显存,成为本地部署腾讯混元、阿里通义万相等前沿视频生成模型的最佳选择,开启桌面级AI创作新时代。 目录 * 一、引言:国产AIGC视频大模型,桌面算力的新疆域 * 二、解锁潜能:RTX 4090与国产视频大模型的协同优势 * 三、项目解析:国产AIGC视频模型的创新之路 * 四、部署与环境搭建:国产模型的本地化实践 * 4.1 基础环境准备 * 4.2 模型部署流程:腾讯混元与阿里通义万相的本地化实战 * 4.3 ComfyUI 集成与优化 * 五、性能测试与对比:RTX 4090 的硬核实力 * 5.1 生成速度实测 (fps / s/frame) * 5.2 显存消耗与优化策略 * 六、实际应用场景:国产模型赋能创意工作流 * 七、

By Ne0inhk