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

【C++模版进阶】如何理解非类型模版参数、特化与分离编译?

【C++模版进阶】如何理解非类型模版参数、特化与分离编译?

🔥艾莉丝努力练剑:个人主页 ❄专栏传送门:《C语言》、《数据结构与算法》、C/C++干货分享&学习过程记录、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬艾莉丝的简介: 🎬艾莉丝的C++专栏简介: 目录 C++的两个参考文档 1  ~>  详解非类型模版参数 1.1  分类 1.2  实践 1.3  注意 2  ~>  模板的特化 2.1  模板特化概念 2.2  函数模板特化 2.2.1  结论 2.

By Ne0inhk
《C++ 递归、搜索与回溯》第2-3题:合并两个有序链表,反转链表

《C++ 递归、搜索与回溯》第2-3题:合并两个有序链表,反转链表

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 《Git深度解析》:版本管理实战全解 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 2. 合并两个有序链表 算法原理(递归): 思路: 解法代码(C++): 博主手记(字体还请见谅哈): 3. 反转链表 算法原理(递归): 思路: 解法代码(C++): 博主手记(字体还请见谅哈): 结尾: 前言: 聚焦算法题实战,系统讲解三大核心板块:“精准定位最优解”——优选算法,“简化逻辑表达,系统性探索与剪枝优化”——递归与回溯,“以局部最优换全局高效”——贪心算法,讲解思路与代码实现,帮助大家快速提升代码能力 2.

By Ne0inhk
【C++篇】面向对象编程的三大特性:深入解析继承机制

【C++篇】面向对象编程的三大特性:深入解析继承机制

目录 一、继承的概念  二、继承的基本定义 2.1 继承的定义格式 2.2 三大继承方式与访问限定符 三、基类与派生类的对象赋值转换 3.1 合法的赋值转换 小tip:子类对象赋值给父类对象不会产生临时变量 3.2 非法的赋值转换 3.3 强制类型转换的注意事项(了解) 四、继承中的作用域 4.1 成员变量的隐藏 4.2 成员函数的隐藏 五、派生类的默认成员函数 5.1 核心规则 5.2 代码演示 问题:为何析构函数的调用顺序是:派生类、基类? 六、继承的特殊场景:友元与静态成员 6.1

By Ne0inhk
C++ 异常处理机制:异常捕获、自定义异常与实战应用

C++ 异常处理机制:异常捕获、自定义异常与实战应用

第34篇:C++ 异常处理机制:异常捕获、自定义异常与实战应用 一、学习目标与重点 * 掌握异常处理的核心概念(异常、抛出、捕获、处理)及基本语法 * 理解 try-catch-throw 语句的执行流程,能够正确捕获和处理标准异常 * 学会自定义异常类,满足实际开发中的个性化异常场景需求 * 掌握异常处理的最佳实践,规避常见错误(内存泄漏、异常安全问题) * 理解异常规格说明(C++11前)与 noexcept 关键字的使用场景 * 结合实战案例,提升代码的健壮性和容错能力 💡 核心重点:try-catch 捕获规则、自定义异常的继承设计、异常安全保障、实战场景中的异常处理策略 二、异常处理概述 2.1 什么是异常处理 异常处理是C++中处理程序运行时错误的机制,核心是“将错误检测与错误处理分离”——在程序出错的地方(如除以零、内存分配失败)“抛出”

By Ne0inhk