c++多态与虚函数

多态是什么?

多态(Polymorphism)是面向对象编程中的一个核心概念,它来源于希腊语,意为“多种形态”。

从字面意思理解,多态是指函数有多种形态(实现)。换句话说,运行阶段同一条函数调用语句可能会调用不同的函数实现。例如

struct Shape {
    virtual float area() = 0;
};

struct Rectangle : Shape {
    float area() { // 计算并返回长方形面积 }
};

struct Circle : Shape {
    float area() { // 计算并返回圆形面积 }
};

float CalcRatio(Shape& shape) {
    ...
    float area = shape.area();
    ...
}

在运行阶段,语句float area = shape.area();调用到的函数实现可能是Rectangle::area(),也可能是Circle::area(),还可能是其他Shape的子类实现的area()

多态有什么用?

多态的最终目的是复用代码(面向对象本身就是为了复用代码而出现的,而多态又是面向对象的一个关键概念)。多态允许我们在不关注实现细节的情况下编写更通用的流程和框架,从而达到复用代码的目的。例如,计算不同图形周长和面积的比值

struct Shape {
    float perimeter() { ... }
    float area() { ... }
};

struct Rectangle : Shape {
    float perimeter() { // 计算并返回长方形周长 }
    float area() { // 计算并返回长方形面积 }
};

struct Circle : Shape {
    float perimeter() { // 计算并返回圆形周长 }
    float area() { // 计算并返回圆形面积 }
};

// 同理,定义 Hexagon 和 Ellipse

float CalcRatio(Shape& shape) {
    float ratio = 0.0f;
    if (长方形) {
        Rectangle realShape = (Rectangle)shape;
        float perimeter = realShape.perimeter();
        float area = realShape.area();
        ratio = perimeter / area;
    } else if (六边形) {
        Hexagon realShape = (Hexagon)shape;
        float perimeter = realShape.perimeter();
        float area = realShape.area();
        ratio = perimeter / area;
    } else if () {
        Circle realShape = (Circle)shape;
        float perimeter = realShape.perimeter();
        float area = realShape.area();
        ratio = perimeter / area;
    } else if (椭圆) {
        Ellipse realShape = (Ellipse)shape;
        float perimeter = realShape.perimeter();
        float area = realShape.area();
        ratio = perimeter / area;
    } 
    // ... 其他更多形状

    return ratio;
}

不难发现,求每个图形比例系数的步骤都是一样的:1)求周长;2)求面积;3)计算周长和面积的比例。这个过程被重复编写,既不美观也不易维护。这些重复代码似乎写一遍就可以了,像这样

float CalcRatio(Shape& shape) {
    float perimeter = shape.perimeter();
    float area = shape.area();
    float ratio = perimeter / area;
    return ratio;
}

但是,在不使用多态的情况下shape.perimeter()shape.area()调用的函数实现是固定的,每个图形都有对函数perimeter()area()的实现,shape.perimeter()shape.area()应该调用哪个函数实现?似乎调用哪个都不可行。

多态正是为解决这个问题而出现的。因此在使用了多态的情况下,代码可以简化成后面的形式。

当然多态也有其他的一些好处(实际上这些好处的最终目的还是复用代码):

  • 提高灵活性和可扩展性。在不更改现有代码的情况下添加新的类或子类。让新的子类对象与现有代码一起工作。
  • 提高可维护性。多态性促进了代码的模块化和分离。通过将公共功能放在父类中,并在子类中重写特定的功能,可以更容易地维护和更新代码。
  • 支持设计模式。许多设计模式,如策略模式、工厂模式和观察者模式,都依赖于多态性。

个人理解:代码设计技术比如OOP、设计模式最终目的都是为了复用代码。

怎么使用多态?

c++使用虚函数提供多态能力,虚函数是指用关键字virtual修饰的函数。具体有两个步骤:

  • 在父类中声明虚函数。
  • 在子类中重写这个虚函数。

注意子类覆写虚函数的时候需要确保函数的类型、名称、参数列表等与基类保持一致,否则无法使用多态。c++11引入的关键字override就是为了让编译器自动检查覆写的正确性,这个关键字是可选的。

比如前面的例子,如果要利用多态特性将CalcRatio()改写成复用版本,就需要使用关键词virtual修饰父类Shape中的函数perimeter()area(),例如

struct Shape {
    virtual float perimeter() { ... }
    virtual float area() { ... }
};

struct Rectangle : Shape {
    float perimeter() override { // 计算并返回长方形周长 }    // override 可选
    float area() override { // 计算并返回长方形面积 }    // override 可选
};

struct Circle : Shape {
    float perimeter() override { // 计算并返回圆形周长 }    // override 可选
    float area() override { // 计算并返回圆形面积 }    // override 可选
};

// 同理,定义 Hexagon 和 Ellipse

一些补充
  • 虚函数一定是成员函数。
  • 纯虚函数。纯虚是没有函数体的虚函数,它的定义类似virtual float perimeter() = 0;。注意包含纯虚函数的类不能实例化。
  • 返回类型协变。返回类型协变是指在子类中重写基类的虚函数时,允许返回类型是基类函数返回类型的子类型。

多态的实现原理?

实现多态的一个关键技术是动态绑定。相对于静态绑定(在编译期间就能确定调用哪个函数实现)而言,动态绑定是指在运行阶段确定将调用哪个函数实现的过程。而c++的动态绑定能力是由虚函数提供的,所以我们要研究的实际上是虚函数的实现原理。

虚函数的实现原理
  • 编译器会给有虚函数的类分配一个虚函数表,虚函数表里存储了这个类所有虚函数的函数指针。
  • 编译器会给有虚函数的类的对象分配一个指向这个虚函数表的指针。

例如下面的代码

struct Shape {
    int getId();
    virtual float perimeter();
    virtual float area();
};

struct Rectangle : Shape {
    float perimeter();
    float area();
};

struct Circle : Shape {
    float perimeter();
    float area();
};

// 定义getId、perimeter、area的函数体

float CalcRatio(Shape& shape) {
    int id = shape.getId();
    float perimeter = shape.perimeter();
    float area = shape.area();
    float ratio = perimeter / area;
    return ratio;
}

对应的汇编代码为

Shape::getId():
        ...
        ret
Shape::perimeter():
        ...
        ret
Shape::area():
        ...
        ret
Rectangle::perimeter():
        ...
        ret
Rectangle::area():
        ...
        ret
Circle::perimeter():
        ...
        ret
Circle::area():
        ...
        ret

CalcRatio(Shape&):
        ...
        # int id = shape.getId();
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    Shape::getId()
        mov     DWORD PTR [rbp-4], eax

        # float perimeter = shape.perimeter();
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        mov     rdx, QWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    rdx
        movd    eax, xmm0
        mov     DWORD PTR [rbp-8], eax

        # float area = shape.area();
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        add     rax, 8
        mov     rdx, QWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    rdx
        movd    eax, xmm0
        mov     DWORD PTR [rbp-12], eax
        
        ...
        ret

# Shape类的虚函数表
vtable for Shape:
        .quad   0
        .quad   typeinfo for Shape
        .quad   Shape::perimeter()
        .quad   Shape::area()

# Circle类的虚函数表
vtable for Circle:
        .quad   0
        .quad   typeinfo for Circle
        .quad   Circle::perimeter()
        .quad   Circle::area()

# Rectangle类的虚函数表
vtable for Rectangle:
        .quad   0
        .quad   typeinfo for Rectangle
        .quad   Rectangle::perimeter()
        .quad   Rectangle::area()

...

通过汇编代码可以看到Shape类、Rectangle类和Circle各有一张虚函数表,表内存放的是各自对perimeter()area()两个函数的实现。

# Shape类的虚函数表
vtable for Shape:
        .quad   0
        .quad   typeinfo for Shape
        .quad   Shape::perimeter()
        .quad   Shape::area()

# Circle类的虚函数表
vtable for Circle:
        .quad   0
        .quad   typeinfo for Circle
        .quad   Circle::perimeter()
        .quad   Circle::area()

# Rectangle类的虚函数表
vtable for Rectangle:
        .quad   0
        .quad   typeinfo for Rectangle
        .quad   Rectangle::perimeter()
        .quad   Rectangle::area()

三条函数调用语句编译后对应三个call操作
在这里插入图片描述
函数调用过程释义如下图
在这里插入图片描述

  • 第一个call操作是对非虚函数Shape::getId()的直接调用。
  • 后两个call操作是对虚函数的调用。虚函数调用被编译为两个查找操作和一个调用函数指针call rdx操作(寄存器rdx存放的是函数地址)。

可以看到,调用虚函数比调用非虚函数多了两个查找操作,c++虚函数实现原理的核心正是多出来的这两次查找操作。第一个查找操作是为了找到对象实际类型的虚函数表;第二个查找操作是为了在虚函数表中找到真正需要调用的函数实现。

由于shape对象的实际类型未知,所以第一次查找操作找到的虚函数表是不确定的;而虚函数表中注册的函数是确定的,所以只要能找到这个对象对应的虚函数表那么函数实现也就是确定的,因此多态实际发生在第一次查找操作。

看到这里,有些同学会发现:这TM不是函数注册表吗?没错,虚函数的原理实际上就是函数注册表,只不过建表和查表的过程由编译器代劳了。

用一个例子来结束本文

对于下面的代码

struct Shape {
    virtual float perimeter() { ... }
    virtual float area() { ... }
};

struct Rectangle : Shape {
    float perimeter() { ... }
    float area() { ... }
};

struct Circle : Shape {
    float perimeter() { ... }
    float area() { ... }
};

float CalcRatio(Shape& shape) {
    float perimeter = shape.perimeter();
    float area = shape.area();
    float ratio = perimeter / area;
    return ratio;
}

编译器会为类型Shape及其子类RectangleCircle各分配一张虚函数表,表里存储了各自对虚函数perimeter()area()的实现。

如果CalcRatio()的入参shape实际类型是Circle,函数调用语句float perimeter = shape.perimeter();会触发下面一系列操作

  • 访问shape对象的虚函数指针,找到shape实际类型的虚函数表;这里shape的真实类型为Circle所以找到的是Circle的虚函数表;
  • 访问虚函数表,查找函数perimeter()的实现;这里是在Circle的虚函数表里查找,所以到的是Circle::perimeter()
  • 调用找到的函数;即调用Circle::perimeter()
    如果入参shape的实际类型是Rectangle,那么将会在是Rectangle的虚函数表里查找perimeter()的实现,最终调用的函数就是Rectangle::perimeter()

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【项目实战】Postgresql数据库中出现锁表如何解决

🌹作者主页:青花锁 🌹简介:Java领域优质创作者🏆、Java微服务架构公号作者😄 🌹简历模板、学习资料、面试题库、技术互助 🌹文末获取联系方式 📝 系列专栏目录 [Java项目…

地震预测系统项目实现

整个项目思路即在一组观测数据中&#xff0c;地震专家&#xff08;即用户&#xff09;输入观测窗口的最小数量和最大数量&#xff0c;进行预测峰值点 数据文件如图所示&#xff1a; #define _CRT_SECURE_NO_WARNINGS #include<fstream> #include<string> #include&…

IOS-高德地图SDK接入-Swift

申请key 这个要前往高德开发平台注册成为个人开发者然后在控制台创建一个应用&#xff1a; 高德开发平台 注册步骤就不写了&#xff0c;写一下创建应用的步骤&#xff1a; 1、点击应用管理——>我的应用 2、点击右上角的创建新应用 3、输入内容&#xff1a; 4、点击添加ke…

在全志T113-i平台上实现H.265视频解码步骤详解

H.265&#xff0c;也被称为HEVC(HighEfficiency Video Coding)&#xff0c;作为H.264的继任者&#xff0c;提供了更好的视频压缩和更高的视频质。H.265通过引入更多先进的编码技术&#xff0c;如更强大的运动估计和更高效的变换编码&#xff0c;对比H.264进行了改进。这些改进使…

设计大师的秘密武器:色彩搭配的奇妙技巧

在设计中&#xff0c;色彩搭配扮演着至关重要的角色。色彩搭配的选择和设计是设计师创作过程中不可或缺的一部分。本文将介绍色彩搭配的重要性&#xff0c;如何设计出令人惊叹的色彩搭配以及色彩对设计师的作用。 色彩卡 | 一个覆盖广泛主题工具的高效在线平台(amd794.com) h…

月薪2W的软件测试工程师,到底是做什么的?

在生活中&#xff0c;我们常常会遇到以下几种窘迫时刻&#xff1a; 准备骑共享单车出行&#xff0c;却发现扫码开锁半天&#xff0c;车子都没有反应&#xff1b;手机导航打车&#xff0c;却发现地图定位偏差很大&#xff0c;司机总是跑错地方&#xff1b;买个水&#xff0c;却…

一个小程序跳转到另一个小程序中如何实现

小程序 保证两个小程序是一样的主体才可以跳转。怎么知道是不是同样的主体呢&#xff1f; 小程序的后台管理-设置-基本设置-基本信息。查看主体信息。 跳转 <button clicktoOtherMini()>跳转到另一个小程序</button> function toOtherMini(){wx.navigateToMini…

Revealing the Dark Secrets of MIM

论文名称&#xff1a; Revealing the Dark Secrets of Masked Image Modeling 发表时间&#xff1a;CVPR2022 作者及组织&#xff1a;Zhenda Xie, Zigang Geng, Hu Han等&#xff0c;来自清华&#xff0c;中科院&#xff0c;微软亚洲研究院。 前言 本文尝试探讨MIM为何有效的原…

数据库的安全管理

数据库的安全管理 一、实验目的 掌握用户账号的创建、查看、修改、删除的方法。掌握用户权限设置方法。掌握角色的创建、删除方法。 二、实验内容用户账号的创建、查看、修改、删除的SQL语句。用户权限设置SQL语句。角色的创建、删除SQL语句。 三、实验步骤在本地主机创建用户…

使用vite框架封装vue3插件,发布到npm

目录 一、vue环境搭建 1、创建App.vue 2、修改main.ts 3、修改vite.config.ts 二、插件配置 1、创建插件 2、开发调试 3、打包配置 4、package.json文件配置 5、执行打包命令 pnpm build 6、修改index.d.ts 目录 一、vue环境搭建 1、创建App.vue 2、修改main.ts 3…

NOIP2018提高组day2 - T1:旅行

题目链接 [NOIP2018 提高组] 旅行 题目描述 小 Y 是一个爱好旅行的 OIer。她来到 X 国&#xff0c;打算将各个城市都玩一遍。 小 Y 了解到&#xff0c;X 国的 n n n 个城市之间有 m m m 条双向道路。每条双向道路连接两个城市。 不存在两条连接同一对城市的道路&#xff…

企事业单位宣传任务的考核稿和投稿有哪些网站?

企事业单位在宣传任务方面扮演着重要角色&#xff0c;他们不仅要向公众展示自己的实力和影响力&#xff0c;也需要提高自己的知名度和形象。在这个信息化时代&#xff0c;涌现出了许多网络平台&#xff0c;为企事业单位提供了更多的宣传机会。本文将介绍一家被广泛认可的投稿平…

模型Model:文件系统模型QFileSystemModel

一、 1、常用函数 QFileSystemModel自带目录变化监听 1)、 QModelIndex setRootPath(const QString &path); 设置检索根目录 2)、 bool isDir(const QModelIndex &index) const; 选中索引是否为目录节点 3)、 QString filePath(const QModelIndex &index) const;…

算法和数据结构--树状数组

概念&#xff1a; 树状数组的初衷是解决状态压缩空间里的累积频率&#xff0c;现在多用于求前缀和与后缀和(方便计算)&#xff0c;它可以以 O(logN)的时间得到任意前缀和&#xff0c;并同时支持在 O(logN)时间内支持动态单点值的修改。空间复杂度 O(N)。 树状数组的引用&#…

如何根据自己的数据集微调一个 Transformer 模型

将通过 NLP 中最常见的文本分类任务来学习如何在自己的数据集上利用迁移学习&#xff08;transfer learning&#xff09;微调一个预训练的 Transformer 模型—— DistilBERT。DistilBERT 是 BERT 的一个衍生版本&#xff0c;它的优点在它的性能与 BERT 相当&#xff0c;但是体积…

Unity3d C#实现场景编辑/运行模式下3D模型XYZ轴混合一键排序功能(含源码工程)

前言 在部分场景搭建中需要整齐摆放一些物品&#xff08;如仓库中的货堆、货架等&#xff09;&#xff0c;因为有交互的操作在单个模型上&#xff0c;每次总是手动拖动模型操作起来也是繁琐和劳累。 在这背景下&#xff0c;我编写了一个在运行或者编辑状态下都可以进行一键排序…

【嘉立创EDA-PCB设计指南】3.网络表概念解读+板框绘制

前言&#xff1a;本文对网络表概念解读板框绘制&#xff08;确定PCB板子轮廓&#xff09; 网络表概念解读 在本专栏的上一篇文章【嘉立创EDA-PCB设计指南】2&#xff0c;将设计的原理图转为了PCB&#xff0c;在PCB界面下出现了所有的封装&#xff0c;以及所有的飞线属性&…

从0开始python学习-48.pytest框架之断言

目录 1. 响应进行断言 1.1 在yaml用例中写入断言内容 1.2 封装断言方法 1.3 在执行流程中加入断言判断内容 2. 数据库数据断言 2.1 在yaml用例中写入断言内容 2.2 连接数据库并封装执行sql的方法 2.3 封装后校验方法是否可执行 2.4 使用之前封装的断言方法&#xff0c…

austin-admin 消息推送平台前端项目依赖低代码平台Amis 怎么使用

austin-admin 消息推送平台前端项目&#x1f525;依赖低代码平台Amis 怎么使用 收到一个通知&#xff0c;要将部署一个开源的消息系统 :austin的前端开源&#xff1a;https://gitee.com/zhongfucheng/austin-admin 本地运行 1、使用npm或者yarn这些咯 yarn yarn start2、使用…

【LabVIEW FPGA入门】FPGA中的数学运算

数值控件选板上的大部分数学函数都支持整数或定点数据类型&#xff0c;但是需要请注意&#xff0c;避免使用乘法、除法、倒数、平方根等函数&#xff0c;此类函数比较占用FPGA资源&#xff0c;且如果使用的是定点数据或单精度浮点数据仅适用于FPGA终端。 1.整数运算 支持的数…