大厂C++题第1辑——虚函数七题精讲之2:虚函数的作用机制

news2024/9/20 23:10:26

一、虚函数的常见应用场景;
二、发挥虚函数作用的语法;
三、虚函数的实现机制;
四、虚函数的性能影响。

题2-虚函数的常见应用场景

上一节我们讲了虚函数的作用,同时也演示了虚函数发挥作用的路径之一:

  1. 一个派生类对象(设为o),调用一个基类方法(设为m);
  2. 该基类方法内,调用了一个虚函数 (设为v);
  3. 该虚函数,在派生类中有 override 实现 (设为 ov)。

这种应用场景,通常被称为 “框架式基类”,常用于“做一件事的基本步骤是确定的,只是每一个小步骤,不同类型(就是派生类)的实体做起来会有一些变化”这样的场景。

比如《白话C++》里的那道面向对象作业题:三个工厂计算员工月薪,都走以下框架:

  1. 先计算员工的基本月薪
  2. 再统计员工的本月请假扣薪数;
  3. 用第1步结果减第2步结果,得到本月应发薪水

这个过程,就可以在基类里,写一个非虚的成员函数,里面调用 “计算基本月薪 ()” 和 “统计员工请假扣薪 ()” 即可。而后两者,就必须使用虚函数;因为三个类型的工厂,对于如何计算员工基本月薪,以及如何统计员工本月请假扣薪总数,存在很大的不同:

  • 以“计算基本月薪”为例,甲工厂是固定金额,乙工厂是计件金额,丙工厂是依据本厂本月销售总额,动态算出每个人的基本月薪……
  • 以“统计员工请假扣薪”为例,甲工厂员工每月两天带薪病假,乙工厂可能是一天带薪病假,一天带薪事假,丙工厂则是全年允许十五天带薪假……

本课作业提供了这道题,大家可以练习下。

虚函数发挥作用的另一个路径是:

  1. 一个类型为基类的 指针(或引用)……
  2. 实质指向或绑定到一个派生类对象上;
  3. 该对象直接或间接调用了一个虚函数;
  4. 该虚函数,在派生类中有 override 实现。

还是以“射击游戏” 为例,这回我们让需求更简单一些:用一个数据容器,存储一堆各种各样的“会飞的东西” (未来的射击目标) ,然后让它们一个个飞出来“秀”一下。

#include <iostream>

using namespace std;

class 会飞的东西
{
public:
    virtual ~会飞的东西 () {};
    virtual void 飞() = 0; // 纯虚函数
};

class 鸭子 : public 会飞的东西
{
public:
     void 飞() override { cout << "我拍着翅膀飞呀飞..." << endl; }
};

class 喷气机 : public 会飞的东西
{
public:
      void 飞() override  { cout << "我屁股着火地飞呀飞..." << endl; }
};

class UFO : public 会飞的东西
{
public:
      void 飞() override { cout << "我无头苍蝇似地窜着飞..." << endl; }
};

然后,我们想把不同的“会飞的东西”,放进同一容器,比如一个 std::list<T>;此时,T得是基类的指针,才方便放不同的派生类对象。

#include <iostream>
#include <list>

using namespace std;

/* 上面的 类定义 */

int main()
{
    list<会飞的东西*> lst = 
    {
        new 鸭子, new 喷气机, new UFO
    };
    
    for (会飞的东西* p : lst)
    {
        p->飞();
    }
}

注意 for 的循环变量 p ,它的类型是 “会飞的东西 *” ,但一旦调用(直接或间接)到虚函数,此处是直接调用 “ 飞() ”,则要优先走派生的版本(如果有),因此得到运行结果是:

我拍着翅膀飞呀飞...
我屁股着火地飞呀飞...
我无头苍蝇似地窜着飞...

这种现象(或效果),在语义上就被称为“多态”。直面含义是:看起来是普通的基类指针(或引用),但是在运行期间,依据它实质指向(绑定)到哪个派生类,就展现出哪个派生类的“形态”。

题3-发挥虚函数作用的语法

如果对象本来就是派生类类型,那么,此时它展现出派生类的形态,那是天经地义的,不被视为多态的表现,比如:

auto ufo1 = new UFO;
ufo1->飞(); // 我无头苍蝇似地窜着飞...

// 或者
UFO ufo2;
ufo2.飞(); // 也不算多态

此时,“飞()” 是不是虚的,都不影响 ufo1 或 ufo2 调用 class UFO 自己的实现版本。

除了基类指针指向派生类对象,基类引用绑定到派生类对象,同样可实现多态效果:

UFO ufo;

会飞的东西& ur = ufo;
ur.飞(); // 我无头苍蝇似地窜着飞...

引用形式的多态,通常用在函数参数传递上:

void 体检(会飞的东西& f) // f 是一个引用
{
    cout << "为证明体能,请一口气飞三分钟!" << endl;

    while( 时间不够三分钟 )
    {
        f.飞(); // 虚函数发挥作用
    }
}

都2023年了,面试官怎么会放过这一老一新的问题结合?问:智能指针会影响虚函数的作用吗?
答:智能指针(unique_ptr、shared_ptr……)包装并不影响虚函数作用发挥:

auto p = make_unique<会飞的东西>(new UFO);
p->飞(); // 走的是 UFO 的 “飞()”

现象是清楚的,原理是简单的:智能指针最终调用成员函数,无论是虚函数还是非虚函数,都还是要经由存储在智能指针内部的裸指针发起。

题4-虚函数的实现机制

C++ 标准并没有规定虚函数在底层应该如何实现。同时,我个人也不认为不了解C++虚函数的底层实现,对现实编程会有多大的影响。

学C++需要去深究它的底层实现,这大概是当年编程行业“开卷”的最早表现之一。

有一些面试官挺爱这些问题,所以我们也讲讲;并且我们不要人云亦云真的去谈太多特定的实现细节,我们从C语言的函数指针说起。

C++当年起步,就基于C语言。C语言中有很多优秀的惯用法,C++继承并且直接在语言层面加以实现——这样就可以让更多的程序员享用来自C语言的优秀惯用法。

C语言需不需要多态?当然也需要。C语言采用 结构+函数指针来实现多态;C++看到了这一点,并且干脆通过“虚函数”的语法点,从而实现,我们只需写一个“virtual”,就能让编译器帮我们在底层用C的方法加以提供虚函数的机制。

我们给一个超简化版的,纯C语言的多态实现例子:

#include <stdio.h>
#include <stdlib.h>

typedef void (* PFlyFunc) ();

typedef struct Flyable
{
    PFlyFunc fly;
} Flyable;

void UFOFly()
{
    printf("我无头飞!");
}

typedef struct UFO
{
    PFlyFunc fly;
} UFO;

UFO* MakeUFO()
{
    UFO *ufo = malloc(sizeof(UFO));
    ufo->fly = UFOFly; // 手工指定函数指针的指向
    
    return ufo;
}

int main()
{
    Flyable *pf =(Flyable *) MakeUFO();
    pf->fly();
    free(pf);
    
    return 0;
}

注意,“基类” Flyable 和 “派生类” UFO,都有一个数据成员:fly,这是一个函数指针,类型都是 “PFlyFunc”;并且,该成员都是结构体中的第一个数据成员(类内偏移一致)。基类 Flyable 的 fly 未初始化,可理解为指向为空,这就类似于 C++ 中基类的纯虚函数;而派生类 UFO 则借助 MakeUFO 函数,程序员手工将它的 fly 指向函数:UFOFly();这就相当于派生类提供了自己的“fly”实现。

这个版本的C模拟多态实现,至少有两个缺点:

  • 缺点1:必须由程序员手工设置好各个类(结构)中的函数指针的指向。
  • 缺点2:不管基类派生类,由它们创建出来的每个对象,都不得不拥有 fly 这个函数指针。这显然是在浪费内存。以派生类 UFO 为例。理论上,由 UFO 实例化出一万个对象,它们的 fly 都需要指向同一个函数(例中的 UFOFly)才对。当前,我们只举了一个“虚函数”为例,如果一个类(结构)需要有更多个虚函数,那么内存浪费的问题就会加剧。

C++的虚函数,就是让C++编译器来帮我们写代码,编译器不会忘记写代码,缺点1一举解决。
缺点2怎么解决呢?也好办。为每个有“多态”需求的类型(基类、派生类)额外生成一张函数地址表,再将对应用虚函数地址,一个个放进这张表;当需要调用时,先到这张表查询出所要调用函数的地址,再依据函数地址(相当于函数指针)调用实际函数。

比如,上面纯C例子中的 UFO 结构,它需要有一个“虚函数” fly,实质指向 UFOFly这个函数,那么我们就将后者的地址,存放到为 UFO struct 专门生成 一张数据表的第一行;之后需要调用 fly 方法时,通过该表查出 UFOFly 的地址,然后调用它。

假设例中的 Flyable 和 UFO 后面又多了一个“虚函数”,叫 “show”;并且,UFO 中的 show 函数需指向 UFOShow() 函数,那么,UFO 的 “ 虚函数 ”表,大概长这样子:

编号函数地址
#1&UFOFly
#2&UFOShow

编号是依据 函数在类(结构)中的声明次序决定的,fly 排第一,show 排第二。当我们代码写 ufo->fly()时,你大可以理解为:在编译器眼里,它将变成:“ufo 的1号虚函数调用……”,然后它就去找表中的第一行的第二列,找到 &UFOFly——实际肯定是一个数字,表示函数地址。

注意,整张表是对应 UFO 这个结构的;也就是说,所有 UFO 实例化出的对象,都拥有共同的一张虚函数表。这就解决了上面说的缺点2:内存浪费。当然,所有的UFO对象要如何才能知道这张表在哪里呢?这就迫使所有UFO对象仍然必须至少拥有一个指针,用于指向UFO类的虚函数表。

显然,假设整个类只有一个虚函数,那么,为这个类的额外生成虚函数表就只存放了一个函数地址;而这个类的所有对象,又都必须拥有一个指针用于记录函数表的地址……这时候,从节约的内存的角度考虑,还不如直接让这个指针指向唯一的虚函数地址。但是,如果一个类拥有比较多(比如三、四个)虚函数,大多数C++编译器所采用的,基于虚函数表的方案(再次强调,这并不是标准),就显得有利可图了。

题5-虚函数的性能影响

当然,有利可图指的是内存节约,在性能上,要调用一个函数,需要先找到虚函数表,再从虚函数中查出对应的函数地址,然后才能开始调用……尽管前面两个步骤复杂度都是O(1),但两次地址跳转,难免损耗了一点性能——在非极端应用下,完全可以忽略。另外,这样的性能损耗,仅发生成上面的说的多态语义成立的情况下;比如,下面的代码就不存在虚函数带来的性能损耗:

UFO ufo;
ufo.飞(); // 性能相当于非虚函数调用

按理说,ufo 要调用 飞() 的方法,一样要借助虚函数以查到“飞()”函数地址,一样会有两次跳转,为什么能够放心地说,这个过程不损耗性能呢?这是因为编译器不傻,它发现 ufo 就是 UFO,所以遇上这类非多态的虚函数调用,它会在编译其内,就直接查出函数地址。

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

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

相关文章

Docker 私有仓库 harbor 搭建

&#x1f388; 作者&#xff1a;Linux猿 &#x1f388; 简介&#xff1a;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我&#xff0c;关注我&#xff0c;有问题私聊&#xff01; &…

Vue操作Cookie

这里用的js-cookie。 很简单&#xff0c;安装一下就可以使用了。 npm install js-cookie --save 使用示例&#xff1a; import Cookies from js-cookieexport default {name: YourComponent,methods: {setCookie() {// 设置一个名为 name 的cookie&#xff0c;值为 value&a…

聚焦信息技术发展,博睿数据受邀出席产业链供需对接深度行北京站活动

7月6日&#xff0c;以“广聚群链 湾区启航”为主题的产业链供需对接深度行北京站活动圆满落幕。本次活动吸引了来自北京和广东的多家知名企业参与&#xff0c;博睿数据受邀出席了本次活动&#xff0c;同时携核心产品一体化智能可观测平台Bonree ONE参展&#xff0c;展示博睿数据…

Nginx配置springboot+vue项目http跳转https

java生成证书 添加依赖 <dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk15on</artifactId><version>1.69</version></dependency> import org.bouncycastle.asn1.ASN1Encodable; import org.bounc…

易微联2.4G通断器添加到手机步骤

蓝牙款无WIFI&#xff0c;按住通断器上的按钮&#xff0c;会先闪一下&#xff0c;再闪两下。闪一下的时候连手机&#xff0c;闪两下清码。 手机上打开易微联app&#xff0c;依次点击加号/轻智能遥控器/单按键遥控器/添加完成。 返回打开刚才添加的开关&#xff0c;在通断器闪…

基于Spring Boot的医院信息管理系统设计与实现(Java+spring boot+MySQL)

获取源码或者论文请私信博主 演示视频&#xff1a; 基于Spring Boot的医院信息管理系统设计与实现 使用技术&#xff1a; 前端&#xff1a;html css javascript jQuery ajax thymeleaf 后端&#xff1a;Java springboot框架 mybatis 数据库&#xff1a;mysql5.7 开发工具:IDEA…

Apollo星火计划城市交通大赛600分,断头路,特殊车辆绕行,借道绕行

现在解决了前六题&#xff0c;可以拿到600分&#xff0c;有兴趣一起讨论的可以私聊我交流一下。

Linux命令(41)之top

Linux命令之top 1.top介绍 linux命令top是用来实时监测服务器资源的使用状况&#xff0c;包含进程、cpu、内存等等 2.top用法 top [参数] top常用参数 参数说明-d屏幕刷新时间间隔-i<time>设置刷新时间间隔-u<user>指定用户名-p<pid>指定进程号 top命令中…

Android平台GB28181设备接入技术探讨

GB/T28181技术背景 在此之前&#xff0c;我们先对协议规范做个简单了解&#xff1a;GB28181协议是一种用于视频监控系统互联互通的国际标准&#xff0c;它定义了视频监控系统中的设备间如何进行通信、交换数据和协调控制。以下是GB28181协议的一些主要内容&#xff1a; 设备互…

有哪些数据结构与算法是程序员必须要掌握的?——“数据结构与算法”

一&#xff1a;引言 作为IT程序员&#xff0c;学习算法的原因主要有以下几点&#xff1a; 提升问题解决能力&#xff1a;算法可以帮助程序员分析、优化和解决复杂问题。了解算法原理和实现方式将有助于程序员更快地找到合适的解决方案。这对于解决实际工作中的问题是非常有帮助…

基于STM32FFT(快速傅里叶变换)音频频谱显示功能实现

+ v hezkz17进数字音频系统研究开发交流答疑 一实验效果 二 设计过程 要用C语言实现STM32频谱显示功能,可以按照以下步骤进行操作: 1 确保已经安装好了适当的开发环境和工具链,例如Keil MDK或者GCC工具链。 2 创建一个新的STM32项目,并选择适合的MCU型号。 3 配置G…

es8.8 集群安装笔记

es8.8 集群安装笔记 配置集群第一步 修改配置文件 本次安装使用centos8 3节点安装&#xff1a; 192.168.182.142 192.168.182.143 192.168.182.144 官网 可以查看详细的安装&#xff0c;安装步骤比较简单 https://www.elastic.co/guide/en/elasticsearch/reference/8.8/rpm.htm…

使用gradio库的Plot模块创建交互式绘图界面

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

Mongodb-5.0.18-zip下载与安装

1.下载地址 Download MongoDB Community Server | MongoDB 2.创建一个文件夹和master.cfg的文件 说明&#xff1a;目的是让mongodb存放数据库的位置在mongodb文件里面。 2.1创建名为mongodb的文件夹 2.2master.cfg 说明&#xff1a;在mongodb5.0.18目录下创建master.cfg。 …

DeepSpeed使用体会

现在的模型越来越大&#xff0c;动辄几B甚至几百B。但是显卡显存大小根本无法支撑训练推理。例如&#xff0c;一块RTX2090的10G显存&#xff0c;光把模型加载上去&#xff0c;就会OOM&#xff0c;更别提后面的训练优化。 作为传统pytorch Dataparallel的一种替代&#xff0c;D…

Spring Cloud Alibaba 之 Nacos精讲

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

ChatGPT炒股:批量自动提取股票公告中的表格并合并数据

首先&#xff0c;在ChatGPT中输入提示词&#xff1a; 写一段Python代码&#xff1a; F盘文件夹“新三板 2023年日常性关联交易20230704”中很多个PDF文件&#xff0c;用 Tabula提取这些PDF文件中第1页中的第2个表格&#xff0c;然后保存到表格文件中&#xff0c;文件标题名和…

选读SQL经典实例笔记03_DML和元数据

1. 复制数据到另一个表 1.1. sql insert into dept_east (deptno,dname,loc)select deptno,dname,locfrom deptwhere loc in ( NEW YORK,BOSTON ) 2. 复制表定义 2.1. 复制表结构&#xff0c;而不复制数据 2.2. MySQL 2.3. PostgreSQL 2.4. Oracle 2.5. sql create ta…

递归--Fibonacci数列 I

描述 众所周知&#xff0c;Fibonacci数列是一个著名数列。它的定义是&#xff1a; 本组题目共有 5 题&#xff0c;请分别用 5 种不同的方式来完成&#xff0c;并比较这些做法的时间。 本题要求采用第一种方法&#xff1a;递归&#xff0c;且不得使用数组记忆结果。 输入描述 …

备忘录方法--Fibonacci数列 IV

描述 众所周知&#xff0c;Fibonacci数列是一个著名数列。它的定义是&#xff1a; 本题要求采用第四种方法&#xff1a;备忘录方法&#xff0c;即记忆化搜索。 具体做法是&#xff1a;用数组把曾经求出来的 Fibonacci 数列保存下来&#xff0c;以后要的时候直接取出来。 输入…