c++多态与虚函数

news2025/1/16 14:05:19

多态是什么?

多态(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()

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

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

相关文章

统计学-R语言-5.1

文章目录 前言随机性和规律性概率变量的分布离散型--二项、泊松、几何二项分布几何分布泊松分布 连续型--均匀、正态均匀分布正态分布 其它统计分布--χ2分布、t分布、F分布χ2分布t分布F分布 练习 前言 从本篇文章开始介绍有关概率与分布的介绍。 随机性和规律性 当不能预测…

2024阿里云服务器常用配置价格表(原价及活动报价)

2024年阿里云服务器租用费用,云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、轻量应用服务器2核2G3M带宽轻量服务器一年61元,2核4G4M带宽轻量服务器一年165元12个月,ECS云服务器e系列2核2G配置99元一年、2核4G服务器30元3个月、2核4G配置…

2024年外贸新兴市场有哪些 | 箱讯科技国际贸易平台

当前欧美市场经济增速放缓,通胀持续高位导致物价普遍上涨,进一步引发消费疲软。此外,受原材料价格、劳动力、土地等经营成本上升影响,外贸出口企业利润被进一步压缩。 困顿之中,新兴市场成为破局关键,巨大的…

Quartus 软件界面介绍与部分使用技巧

内容太多,只能慢慢补充完善了~ 对一个软件的熟练掌握,不仅在于完成项目工程,还在于对一个软件的各个功能的位置与使用要熟稔于心(个人看法)。 软件界面 默认打开的软件界面如下: 关掉所有能关闭的窗口&am…

IDEA中启动项目报堆内存溢出或者没有足够内存的错误

1.报错现象 java.lang.OutOfMemoryError: Java heap space 或者 Could not reserve enough space for object heap 2.解决办法 在运行配置中VM选项后加下面的配置: -server -XX:MaxHeapSize256m -Xms512m -Xmx512m -XX:PermSize128M -XX:MaxPermSize256m

模型Model:文件系统模型QFileSystemModel

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

LabVIEW图像识别检测机械零件故障

项目背景: 在工业生产中,零件尺寸的准确检测对保证产品质量至关重要。传统的人工测量方法不仅耗时费力,精度低,还容易导致零件的接触磨损。为了解决这些问题,开发了一套基于LabVIEW和机器视觉的机械零件检测系统。该系…

新火种AI|程前怼大佬引发闹剧,但更值得关注的,是大佬的AI观点

作者:小岩 编辑:彩云 在2024年开年的一场演讲大会中,知名网红程前直接炮轰了大佬周鸿祎。事件快速发酵,引起了全网的热烈讨论。很多人都在吐槽程前的情商有多低,大佬有多真性情,却全然忘记了周鸿祎此次参…

unity C#什么是线程锁,以及使用案例

文章目录 原理1. **互斥**(Mutual Exclusion):2. **缓存一致性与内存屏障**:3. **操作系统的支持**:4. **编程语言级别的实现**:5. **避免死锁**:图示 实例1实例2 原理 线程锁的原理主要是为了在多线程环境…

美国智库发布《用人工智能展望网络未来》的解析

文章目录 前言一、人工智能未来可能改善网络安全的方式二、人工智能可能损害网络安全的方式三、人工智能使用的七条建议四、人工智能的应用和有效使用AI五、安全有效地使用人工智能制定具体建议六、展望网络未来的人工智能(一)提高防御者的效率&#xff…

如何优雅的实现主机与虚拟机文件共享?

我们在嵌入式开发中使用虚拟机时,经常需要在主机和虚拟机操作系统之间传输文件。以常用的虚拟机软件 Vmware 为例,如果安装了虚拟机工具,我们可以通过直接拖放文件的方式快速实现文件的交互,但这种方式做不到文件的同步、修改&…

华为路由设备DHCPV6配置

组网需求 如果大量的企业用户IPv6地址都是手动配置,那么网络管理员工作量大,而且可管理性很差。管理员希望实现公司用户IPv6地址和网络配置参数的自动获取,便于统一管理,实现IPv6的层次布局。 图1 DHCPv6服务器组网图 配置思路 …

Android 系统启动过程纪要(基于Android 10)

前言 看过源码的都知道,Launcher系统启动都会经过这三个进程 init ->zygote -> system_server。今天我们就来讲解一下这三个进程以及Launcher系统启动。 init进程 准备Android虚拟机环境:创建和挂载系统文件目录;初始化属性服务&…

Halcon基于灰度值的模板匹配

Halcon基于灰度值的模板匹配 基于灰度值的模板匹配是最经典的模板匹配算法,也是最早提出来的模板匹配算法。这种算法的根本思想是,计算模板图像与检测图像之间的像素灰度差值的绝对值总和(SAD方法)或者平方差总和(SSD…

Java NIO (一)简介

1 NIO简介 在1.4版本之前,Java NIO类库是阻塞IO,从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO。New IO类库的目的 就是要让Java支持非阻塞IO。 Java NIO类库包含三个核心组件: …

Vue3中provide,inject使用

一,provide,inject使用: 应用场景:向孙组件传数据 应用Vue3碎片: ref,reactive,isRef,provide, inject 1.provide,inject使用 a.爷组件引入 import {ref,provide} from vue const drinkListre…

LeetCode、2542. 最大子序列的分数【中等,排序+小顶堆】

文章目录 前言LeetCode、2542. 最大子序列的分数【中等,排序小顶堆】题目及类型思路及代码实现 资料获取 前言 博主介绍:✌目前全网粉丝2W,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领…

安泰射频功率放大器的主要的类型及主要参数

射频功率放大器是一种将射频信号放大到更高功率级别的电子设备。根据其工作原理和应用要求,射频功率放大器可以分为不同的类型。下面西安安泰将介绍一些常见的射频功率放大器类型和相关的主要参数。 A类功率放大器:A类功率放大器是一种广泛使用的线性放大…

Navicat教程

下载连接(无限使用版) 链接:https://pan.baidu.com/s/1IprYLRv0bSnW-XKn0trRtw 提取码:j6qx 连接使用 1.1 连接数据库 打开navicat,点击连接,选择数据库 1.2 操作数据库 右键连接,点击新建数…

Excel 动态可视化图表分享

AIGC ChatGPT 职场案例 AI 绘画 与 短视频制作 PowerBI 商业智能 68集 数据库Mysql 8.0 54集 数据库Oracle 21C 142集 Office 2021实战应用 Python 数据分析实战, ETL Informatica 数据仓库案例实战 Excel 2021实操 100集, Excel 2021函数大全 80集 Exc…