大厂C++面试基础题第1辑——虚函数七题精讲之一

news2024/11/16 3:44:11

> “虚函数的作用” 是面向对象的C++编程最基础也最核心的知识点,如果不能无法正确回答本题,则只此一题,不管大厂还是小厂,都铁定无缘了。

概述

“虚函数” 是 C++面向对象三最:最基础、最重要、最关键的知识点。我们从网上搜索到来自腾讯与字节公司招聘C++新人(主要是校招)的题集中,选择出现多次的七道题:

  1. 虚函数的作用?
  2. 虚函数在什么情况下发挥作用?
  3. 纯虚函数是什么?
  4. 简单说说虚函数发挥作用的底层机制?
  5. 关键字 override 的作用?
  6. 析构函数可以是虚函数吗,“虚析构”函数有什么关键作用?
  7. 构造函数可以是虚函数吗?为什么?

本辑大厂C++面试题,提供和问题紧密相关的知识点的全面精讲,在实际面试中可按需回答。

题1-虚函数的作用

题目评价: 虚函数的作用,是面向对象的C++编程最基础也最核心的知识点,如果不能无法正确回答本题,则只此一题,不管大厂还是小厂,都铁定无缘了。

参考阅读: 这么重要的问题,自然问的人很多,站长(南郁)也曾在外部平台回答过多次。其中2018年在知乎的回答,自回答后就一直获该问题的榜首推荐。我们也将该回答收录到本站(d2school)课程《站长技术问答精选》 下的第10课:《C++中虚函数相比非虚函数的优势》。建议可以先阅读该文章,并完成其内作业。

一个类的(非静态)成员函数,加上 “virtual” 修饰,就得到一个“虚函数”。假设它被作为基类,有另一个新类派生自它,那么,派生类既可以重新定义基类的“虚函数”,也可以重新定义基类的“非虚函数”。前者的行为称为 “override / 覆盖”,后者则属于 “overwrite / 重写”中的一种。

“覆盖”和“重写”的共同点是:基类可以用基类的实现(如果确实该成员函数有提供实现),派生类则可以用基类的,也可以用派生类自己的实现。

到这里都还很好理解:一个功能,基类用基类的,派生类如果重新实现了,就可用自己的。举个例子:假设有个“坦克”作为基类,提供一个功能叫“前行”:

// 普通坦克
class Tank
{
pubic:
    void Forward()
    {
        cout << "我用履带在陆地上前行";
    }
};

 接下来,有个“水陆两用坦克”,它派生类上面的坦克,它提供了新的前行方法:

// 水陆两用坦克
class AmphibiousTank : public Tank
{
public:
     void Forward()
     {
          if (/* 在水中 */)
          {
              cout << "我用螺旋桨在水中前行";
          } 
          else
          {
              Tank::Forward(); // 使用基类的功能 
          }
     }
};

有派生类,自然有基类,因此,在派生类的扩展实现中,可以不用,也可以使用基类的原有实现,这很好理解。但是,“虚函数” 的作用,却是要让基类可以用上派生类对该虚函数重新定义的功能。要知道,在有基类的时候不一定有派生类,并且,一个基类未来可以有许多个派生类,所以,更严谨的说法应该是:虚函数让用基类可以“预定”派生类的功能。

从基类的角度来理解,会直观一些:当一个基类(的设计者)将它的一个成员函数,定义为“虚”函数时,目的就是为了让基类可以“预定”派生类对该函数引入的变化。

这就是 “覆盖”和“重写”的不同点:“覆盖/overide” 可以让基类的代码有机会用到派生类的功能,简单的“重写/overwrite”则无法实现。

不使用虚函数实现基类调用派生类的定制功能的话,可利用 “CRTP”方式实现。

希望基类的代码可真实调用派生类定制功能,这种基类可被称为“框架式基类” (见《白话C++》之练功8.6.7小节)。我们也给个例子(同样来自《白话C++》)——

假设有个射击类游戏,游戏中有个“会飞的目标”是基类。在写基类的阶段,我们就很知道游戏的主干逻辑:

  • 第1步:目标飞呀飞呀飞……
  • 第2步:目标检查一下周边50米内是否有逼近的子弹?
  • 第3步:如果没有子弹,回第1步;
  • 第4步:如果有子弹,目标尝试逃避子弹……
  • 第5步:逃避成功,回第1步;
  • 第6步:逃避失败,目标做最后的演出。

目标可以是鸭子、战机、UFO、美国超人。在游戏的第一个版本,为了极大简单化问题,我们原准备让它们从第1步到第6步,都完全一个模样……但甲方爸爸跳起来了:这游戏还有什么可玩性?!

好吧,我们决定让鸭子、战机、UFO、美国超人在最后一步,也就是“最后的演出”上略有不同。

整个主干逻辑,都可以在基类的“飞/Fly”方法上实现:

// 射击目标的基类
class 会飞的目标
{
public:
    /* 飞翔函数
       返回 true 表示可以继续飞,
       返回 false 表示已挂,不能再飞了 */
    void Fly() 
    {
       cout << "我自由自在地飞呀飞呀飞……\n";

       /* 话外音: 然而,这世上哪有无限的自由!*/
 
       cout << "好吧,让我检查一下边上有没有可恨的子弹……\n";

       // 检查周围飞来的子弹
       auto bullet = this->inspectBulletAround(); 

       if (!bullet) 
       {
            cout << "世界是和平的!\n";
            return true;
       }
       
       // 居然有子弹!尝试逃避!
       if (this->tryEscape(bullet))
       {
            cout << "哈哈哈,我可真厉害!\n";
            return true;
       }

       // 完蛋,没躲开,做最后挣扎吧!
       return this->lastShow();
    }   
private:
     // 检查周围子弹
     Bullet* inspectBulletAround() { ... }   

     // 尝试逃避子弹,基类觉得自己永远躲不开
     bool tryEscape(Bullet* ) { return false; } 
};

如上所说,怎么检查和怎么逃避子弹(事实上还有怎么自由地飞呀飞),无论什么目标,都是相同的,因此该基类提供了 “inspectBulletAround()” 和 “tryEscape()” 的实现,它们是非虚的。

但是!还有个“最后的表演”,“万恶”的甲方爸爸说,这是底线了,一定要让鸭子、战机、UFO、美国超人中弹后的最后表演,各有不同、异彩纷呈。

尽管上面的 Fly() 明显是基类的一个方法,但是,确实可以让它“预定”派生类的方法。这就是“虚函数”的作用。

在本例中,我们只需要将 lastShow() 定义为虚函数。注意,这正是基类在设计上的职责:确定哪些成函数为虚函数,哪些不是——这是面向对象设计中的一个难点,也是一个痛点。

“难点”的意思是:很难,但必须努力去做好。“痛点”的意思是:这件事不仅难,而且,就算是你努力也不一定做得好。

在基类中的 lastShow()是虚函数这一基础上,它还有两种选择。一是提供默认的实现,比如:

class 会飞的目标
{
public:
    bool Fly() { ... }
    ...
private:
    // 基类提供的“最后表演”的默认实现 (注意有 virtual 修饰)
    virtual bool lastShow() 
    {
        // 默认表演:什么都不做, 直接返回 false,表示 认命而死
        return false;
    }   
};

此时,派生类可以依据自己的实际情况,提供或不提供定制的 lastShow 实现,在提供的情况下,还可以在必要,调用基类的默认实现。

如果基类不提供默认实现,此时称 lastShow 为纯虚函数 (pure-virutal),表示强制要求每个具体的派生类,都要提供自己定制实现的 lastShow 行为,本辑第3点将进一步详解“纯虚函数”。

接下来,我们定义一个派生类:鸭子,它几乎什么都不用做,除了提供定制的 “lastShow”:

class 鸭子 : public 会飞的目标
{
private:    
   // 鸭子版本的最后表演:
   bool lastShow() override
   {
       std::cout << "嘎~嘎~嘎~,我这一死,真是轻如鸿毛!\n";
       return false; 
   }
};

鸭子类继承了来自基类的 “Fly”。如果此时我们定义出一只鸭子,并调用Fly,会怎样?

鸭子 唐小鸭;
唐小鸭.Fly();  // 调用来自基类的 Fly

会进入基类的Fly函数,如果中弹,会调用 lastShow()。那么,真正的问题来了,此时调用的是基类的lastShow,还是派生类的 lastShow?答:会调用派生类的,尽管这段代码当时是写在基类里的。

作为对比,如果我们让鸭子类也提供自己的 “tryEscape / 逃避方法”,于是有:

class 鸭子 : public 会飞的目标
{
private:
   // 鸭子觉得,自己这么灵活,可以躲开子弹:
   bool tryEscape(Bullet* ) { return true;  } // overwrite 重写
    
   // 鸭子版本的最后表演:
   bool lastShow() override // 覆盖
   {
       std::cout << "嘎~嘎~嘎~,我这一死,真是轻如鸿毛!\n";
       return false; 
   }
};

看 tryEscape 的实现与注释:愚蠢的鸭子觉得自己可以恒定躲开子弹,然而,tryEscape 不是虚函数,这意味着基类并没有“预定”派生类对它的定制实现,所以在基类的 “Fly” 方法中,执行的那个 tryEscape,仍然是基类的……

这就是虚与非虚的区别:基类是否可以预定派生类对该函数的定制实现。针对本例,还有一些细节,你需要特别关注到:

  • 我们明确定义了一个派生类的对象,然后调用继承自基类的某个方法(本例中的Fly),这个方法中调用了一个虚函数(本例中的 lastShow),这是虚函数发挥作用的方式之一。下面第2点,我们就会详解虚函数发挥作用的另一种方式。
  • 继续第1点,请注意:例中调用的基类方法 Fly,并不是虚的;这是一种常用的虚函数使用方法:在基类的非虚函数中,调用一个虚函数。(《白话C++》中称之为“框架型基类”);
  • 注意:lastShow() 是一个私有方法,但这并不影响因为它是“虚”的,所以假设有人问你:基类的代码有办法调用派生类的某个私有方法吗?请回答:“可以”;
  • 派生类在重定义虚函数 lastShow 时,用到了 override,请注意它的出现位置。它的作用在本辑第5点能找到答案。
  • 如要作为真实应用,本例中的 “inspectBulletAround()”、“tryEscape()” 等方法显然也应“虚”化。
  • 如要作为真实应用,检查得到的子弹 “Bullet ”,也应该是一个基类,然后子弹类也可以提供不少虚函数,并有各种种样的派生类子弹,这样 tryEscape(Bullet) 的实现与调用,就会出现所谓的 “双重分派 / double dispatch”,意思是:tryEscape本身是虚的,不同的“飞行目标”会有不同的逃避子弹方法,而在它的实现中,子弹的一些行为也是“虚”的,于是同一种飞行目标,面对不同子弹时,也理应有不同的表现……

如果对本辑话题有兴趣,请关注本辑课程后面的六节课。也欢迎参与本课堂练习(小测),通过检验强化自己的学习成果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/635034.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是Vue的Vite构建工具?如何使用Vite进行项目开发

什么是Vue的Vite构建工具&#xff1f;如何使用Vite进行项目开发 介绍 Vite是一个由Vue.js核心团队开发的构建工具。它的目标是提供一种快速的开发体验&#xff0c;同时保持生产环境的稳定性和可靠性。Vite使用了ES模块作为开发环境的原生模块格式&#xff0c;通过在开发服务器…

C++11中的关键字constexpr

文章目录 1、constexpr修饰普通变量2、constexpr修饰函数3、constexpr修饰类的构造函数 constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力&#xff0c;而不必等到程序运行阶段。C 11 标准中&#xff0c;constexpr 可用于修饰普通变量、函数&…

【Leetcode】DP | 序列及子数组问题

300 最长递增子序列 求数组最长严格递增子序列的长度。 D [ i ] D[i] D[i]代表以 n u m s [ i ] nums[i] nums[i]结尾的最长递增子序列的长度。 D [ i ] max ⁡ j < i , n u m s [ i ] > n u m s [ j ] ( D [ j ] 1 ) D[i] \max_{j < i,\ nums[i]>nums[j]}(D[…

什么是Vue的UI框架?

什么是Vue的UI框架&#xff1f; Vue.js 是一款流行的 JavaScript 框架&#xff0c;用来构建现代的单页面应用程序&#xff08;SPA&#xff09;。Vue.js 提供了丰富的功能和 API&#xff0c;但是在构建应用程序时&#xff0c;我们还需要使用一些 UI 组件来实现复杂的交互和界面…

【分布式能源选址与定容】光伏、储能双层优化配置接入配电网研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

02- 输入、输出及运算符(C语言)

一 输入、输出 1.1 输出函数 printf 函数是一个可变参数函数&#xff0c;参数的个数不定&#xff1a;int printf(const char *format, ...) printf("%d\n", x); printf("%d %d\n", x, y); 1.2 输入函数 1.2.1 scanf函数&#xff1a;int scanf(const …

利用AI点亮副业变现:5个变现实操案例的启示

AI变现副业实操案例 宝宝起名服务AI科技热点号头像壁纸职业头像收徒&#xff1a;萌娃头像定制头像平台挂载 小说推广号流量营销号百家号AI共创计划公众号流量主 知识付费知识星球小报童&#xff1a; 整体思维导图&#xff1a; 在这里先分享五个实操案例: 宝宝起名服务AI科技热…

[MySQL]一文带你学明白数据库控制语言——DCL

前言 嗨咯&#xff0c;小伙伴大家好呀&#xff01;好几天没见了&#xff0c;周末过得怎么样啊&#xff01;之前学过的SQL语句不会都忘了吧。如果忘了的话大家可以看一下前几期的文章。本期要学习的是SQL语句中的数据库控制语句——DCL&#xff0c;学习完毕之后MySQL中的SQL语句…

探索ll-hls低延迟直播协议

HLS全称为HTTP Live Streaming&#xff0c;其中m3u8作为描述协议&#xff0c;指向一系列切片文件。支持多码流与自适应码率&#xff0c;支持广告无缝播放&#xff0c;支持CMAF协议的低延时直播&#xff0c;也支持CDN动态选择。 我们先看下HLS整体架构&#xff0c;由三部分构成…

存储技术3 数据保护: RAID

Why RAID 性能限制了磁盘驱动单独的驱动存在预期的使用寿命 MTBF测量若一个驱动器的MTBF是750 000小时&#xff0c; 阵列中有100个驱动&#xff0c; 阵列的MTBF会变成 750000 / 100 7500小时 RAID用于减缓这个问题RAID特点 增大容量高可用性增强的性能 RAID implementation…

仿微信我的列表功能菜单按钮 我的个人中心页面功能菜单

前端vue自定义仿微信我的列表功能菜单按钮 我的个人中心页面功能菜单, 下载完整代码请访问https://ext.dcloud.net.cn/plugin?id12990 效果图如下: #### 使用方法 使用方法 <!-- leftTitle:标题 icon&#xff1a;左边图标 click&#xff1a;点击事件 --> <ccMe…

【java】IO流

IO流 原理 分类 字节流与字符流 节点流与包装流 Java IO详解&#xff08;五)------包装流 - YSOcean - 博客园 (cnblogs.com)JAVA I/O流 字符流和字节流、节点流和处理流(包装流、过滤流)、缓冲流_过滤流和缓冲流,字节流的关系_X-Dragon烟雨任平生的博客-CSDN博客 字符流 i…

算法模板(4):动态规划(2)

8.树形DP 没有上司的舞会 树上最大独立集问题 Ural 大学有 N N N 名职员&#xff0c;编号为 1 ∼ N 1 \sim N 1∼N。他们的关系就像一棵以校长为根的树&#xff0c;父节点就是子节点的直接上司。每个职员有一个快乐指数&#xff0c;用整数 H i H_i Hi​ 给出&#xff0c;…

顺序查找和折半查找

顺序查找和折半查找 顺序查找 一、算法思想 顺序查找&#xff0c;又叫“线性查找”&#xff0c;通常用于线性表。 算法思想&#xff1a;从头到尾挨个找&#xff08;或者反过来也OK&#xff09; 二、算法实现 结构体定义 typedef struct{ElemType *elem;int TableLen; }SS…

SpringBoot + Vue 的留守儿童系统的研究与实现

文章目录 1.研究背景2. 技术栈3.系统分析4系统设计5系统的详细设计与实现5.1系统功能模块5.2管理员功能模块 1.研究背景 以往的留守儿童爱心的管理&#xff0c;一般都是纸质文件来管理留守儿童爱心信息&#xff0c;传统的管理方式已经无法满足现代人们的需求&#xff1b;使用留…

变压器差动保护的影响因素和相应的措施

由于变压器一、二次电流、电压大小不同&#xff0c;相位不同&#xff0c;电流互感器特性差异&#xff0c;电源侧有励磁电流&#xff0c;都将造成不平衡电流流过继电器&#xff0c;必须采用相应措施消除不平衡电流的影响。 主要措施 &#xff08;1&#xff09;减小稳态情况下的…

SpringCloud学习笔记(四)RabbitMQ

一、同步通讯的优缺点 时效性较强&#xff0c;可以立即得到结果但是耦合度高&#xff0c;性能和吞吐能力下降有额外的资源消耗 二、异步通讯的优缺点 耦合度低&#xff0c;吞吐量提升故障隔离&#xff0c;流量削峰依赖于Broker的可靠性、安全性和吞吐能力 三、什么是MQ MQ…

物联网Lora模块从入门到精通(七)串口通讯

一、前言 在Lora模块的程序设计中&#xff0c;串口通信一定是一个极其重要且常用的通信方式&#xff0c;借助串口通信&#xff0c;我们不但可以向外传输我们获取的数据&#xff0c;还可以根据外部指令做出相应。 同样的&#xff0c;在例程中&#xff0c;为我们提供了一个名为us…

SSM简易项目实战(基本CRUD操作):速速来学+项目完整源码

前言&#xff1a;又是一年暑期将至&#xff0c;找实习&#xff1f;手里没几个项目恐怕不行&#xff0c;今天给各位带来一个较为简易的Web项目&#xff0c;快来学吧&#xff01; 一、项目简介 1、云借阅图书管理系统 技术栈&#xff1a;&#xff08;SpringSpringMVCMybatisMyS…

【面试必问】Spring核心之控制反转(IOC)

tip&#xff1a;作为程序员一定学习编程之道&#xff0c;一定要对代码的编写有追求&#xff0c;不能实现就完事了。我们应该让自己写的代码更加优雅&#xff0c;即使这会费时费力。 &#x1f495;&#x1f495; 推荐&#xff1a;体系化学习Java&#xff08;Java面试专题&#…