面试官问 C++ 多态?虚函数重写 + 虚表指针 + 动态绑定,核心考点全覆盖

面试官问 C++ 多态?虚函数重写 + 虚表指针 + 动态绑定,核心考点全覆盖

个人头像

✨ 孤廖:个人主页

🎯 个人专栏:《C++:从代码到机器》

🎯 个人专栏:《Linux系统探幽:从入门到内核》

🎯 个人专栏:《算法磨剑:用C++思考的艺术》

折而不挠,中不为下



在这里插入图片描述

文章目录

正文:

1. 多态的概念

多态分为编译时多态(静态)和运行时多态(动态),其中编译时多态就是前文提过的函数重载和函数模板在编译时 通过传不同的参数 在编译时生成的各种具体类,而本篇我们专注于运行时多态(动态)

那什么是运行时多态呢?

运行时多态就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。

举例说明:
⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀⾏为(函数),传猫对象过去,就是”(>ω<)喵“,传狗对象过去就是 ”汪汪“

在这里插入图片描述
在这里插入图片描述

2. 多态的定义及实现

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了Person。Person对象买票全价,Student对象优惠买票。

实现多态还有两个必须重要条件:

必须是基类的指针或者引⽤调⽤虚函数被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖
该图展现了虚函数的覆盖
在这里插入图片描述

2.1 虚函数

说了这么多,那到底什么是虚函数呢? 虚函数设计出来的意义是干什么的呢
`类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰`
虚函数的前提必须是成员函数

【代码说明】:

classperson{virtualvoidBuyTicket(){ cout <<"买票-全价"<< endl;}};

2.2 虚函数的重写/覆盖

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。如果派生类的虚函数 前没有加virtual 此时也构成虚函数的重写 因为派生类继承基类时 已经继承了虚函数 ,但是此种写法不规范 不建议这样写

【代码演示】:

namespace twg {classPerson{virtualvoidBuyTicket(){ cout <<"买票-全价"<< endl;}};classStudent:publicPerson{public:virtualvoidBuyTicket(){ cout <<"买票-打折"<< endl;}};classAnimal{public:virtualvoidtalk()const{}};classDog:publicAnimal{public:virtualvoidtalk()const{ std::cout <<"汪汪"<< std::endl;}};classCat:publicAnimal{public:virtualvoidtalk()const{ std::cout <<"(>^ω^<)喵"<< std::endl;}};voidletsHear(const Animal& animal){ animal.talk();}}intmain(){ twg::Cat cat; twg::Dog dog; twg::letsHear(cat); twg::letsHear(dog);return0;}

【结果】:

在这里插入图片描述

2.3 虚函数重写的⼀些其他问题

  • 协变(了解即可)
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们了解⼀下即可。
classA{};classB:publicA{};classPerson{public:virtual A*BuyTicket(){ cout <<"买票-全价"<< endl;returnnullptr;}};classStudent:publicPerson{public:virtual B*BuyTicket(){ cout <<"买票-打折"<< endl;returnnullptr;}};voidFunc(Person* ptr){ ptr->BuyTicket();}intmain(){ Person ps; Student st;Func(&ps);Func(&st);return0;}
  • 析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

2.4 override 和 final关键字

C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。应用场景:C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失

【代码演示】:

// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法classCar{public:virtualvoidDirve(){}};classBenz:publicCar{public:virtualvoidDrive() override { cout <<"Benz-舒适"<< endl;}};intmain(){return0;}// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写classCar{public:virtualvoidDrive() final {}};classBenz:publicCar{public:virtualvoidDrive(){ cout <<"Benz-舒适"<< endl;}};intmain(){return0;}

2.5 重载/重写/隐藏的对⽐

在这里插入图片描述

3. 纯虚函数和抽象类

在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象

【代码展示】:

Car 为抽象楼 纯虚函数不定义(因为没意义)
classCar{public:virtualvoidDrive()=0;};classBenz:publicCar{public:virtualvoidDrive(){ cout <<"Benz-舒适"<< endl;}};classBMW:publicCar{public:virtualvoidDrive(){ cout <<"BMW-操控"<< endl;}};intmain(){// 编译报错:error C2259: “Car”: ⽆法实例化抽象类 Car car; Car* pBenz =new Benz; pBenz->Drive(); Car* pBMW =new BMW; pBMW->Drive();return0;}

4. 多态的原理

classBase{public:virtualvoidFunc1(){ cout <<"Func1()"<< endl;}protected:int _b =1;char _ch ='x';};intmain(){ Base b; cout <<sizeof(b)<< endl;return0;}
在这里插入图片描述
上⾯题⽬运⾏结果12bytes,除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯(注意有些平台可能会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

4.1 多态是如何实现的

从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicket,ptr指向Student对象调⽤Student::BuyTicket的呢?通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数
该过程有点像 父子进程的写实拷贝函数调用过程
在这里插入图片描述

4.2 动态绑定与静态绑定

  • 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定
  • 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定
通过汇编语言也能感知一二
// 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)

4.3 虚函数表

基类对象的虚函数表中存放基类所有虚函数的地址同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表
指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基
类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴
的。派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分。虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。

那么 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。

vs 和 Linux Centos 下都是常量区

【代码实现】:

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;}
vs 下的结果:
在这里插入图片描述
Linux Centos 下的结果:
在这里插入图片描述
可以看到至少在vs 和linux 平台下 虚表在常量区



结语:

吃透 C++ 多态,就掌握了面向对象的核心精髓 —— 从虚函数重写到虚表的底层支撑,从静态绑定到动态绑定的灵活切换,这些知识点不仅是面试高频考点,更是实际开发中实现灵活代码的关键。结合上一篇的继承知识,你已搭建起完整的面向对象编程框架。。

如果对虚表布局、多态实现细节还有疑问,不妨动手调试代码观察内存结构,欢迎在评论区分享你的实操心得,咱们一起解锁更多 C++ 进阶技能~

在这里插入图片描述

Read more

[DeepSeek] 入门详细指南(上)

[DeepSeek] 入门详细指南(上)

前言 今天的是 zty 写DeepSeek的第1篇文章,这个系列我也不知道能更多久,大约是一周一更吧,然后跟C++的知识详解换着更。 来冲个100赞兄弟们 最近啊,浙江出现了一匹AI界的黑马——DeepSeek。这个名字可能对很多人来说还比较陌生,但它已经在全球范围内引发了巨大的关注,甚至让一些科技巨头感到了压力。简单来说这 DeepSeek足以改变世界格局                                                   先   赞   后   看    养   成   习   惯  众所周知,一篇文章需要一个头图                                                   先   赞   后   看    养   成   习   惯   上面那行字怎么读呢,让大家来跟我一起读一遍吧,先~赞~后~看~养~成~习~惯~ 想要 DeepSeek从入门到精通.pdf 文件的加这个企鹅群:953793685(

By Ne0inhk
DeepFace深度学习库+OpenCV实现——情绪分析器

DeepFace深度学习库+OpenCV实现——情绪分析器

目录 应用场景 实现组件 1. 硬件组件 2. 软件库与依赖 3. 功能模块 代码详解(实现思路) 导入必要的库 打开摄像头并初始化变量 主循环 FPS计算 情绪分析及结果展示 显示FPS和图像 退出条件 编辑 完整代码 效果展示 自然的 开心的 伤心的 恐惧的 惊讶的  效果展示 自然的 开心的 伤心的 恐惧的 惊讶的   应用场景         应用场景比较广泛,尤其是在需要了解和分析人类情感反应的场合。: 1. 心理健康评估:在心理健康领域,可以通过长期监控和分析一个人的情绪变化来辅助医生进行诊断或治疗效果评估。 2. 用户体验研究:在产品设计、广告制作或网站开发过程中,通过观察用户在使用过程中的情绪反应,来优化产品的用户体验。 3. 互动娱乐:在游戏或虚拟现实应用中,根据玩家的情绪状态动态调整游戏难度或故事情节,以增加沉浸感和互动性。

By Ne0inhk
10分钟打造专属AI助手!ToDesk云电脑/顺网云/海马云操作DeepSeek哪家强?

10分钟打造专属AI助手!ToDesk云电脑/顺网云/海马云操作DeepSeek哪家强?

文章目录 * 一、引言 * 云计算平台概览 * ToDesk云电脑:随时随地用上高性能电脑 * 二 .云电脑初体验 * DeekSeek介绍 * 版本参数与特点 * 任务类型表现 * 1、ToDesk云电脑 * 2、顺网云电脑 * 3、海马云电脑 * 三、DeekSeek本地化实操和AIGC应用 * 1. ToDesk云电脑 * 2. 海马云电脑 * 3、顺网云电脑 * 四、结语 * 总结:云电脑如何选择? 一、引言 DeepSeek这些大模型让 AI 开发变得越来越有趣,但真要跑起来,可没那么简单! * 本地配置太麻烦:显卡不够、驱动难装、环境冲突,光是折腾这些就让人心态崩了。 * 云端性能参差不齐:选错云电脑,可能卡到爆、加载慢,还容易掉线,搞得效率直线下降。 * 成本难控:有的平台按小时计费,价格一会儿一个样,

By Ne0inhk
最全java面试题及答案(208道)

最全java面试题及答案(208道)

本文分为十九个模块,分别是:「Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM」 ,如下图所示: 共包含 208 道面试题,本文的宗旨是为读者朋友们整理一份详实而又权威的面试清单,下面一起进入主题吧。 Java 基础 1. JDK 和 JRE 有什么区别? * JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java

By Ne0inhk