C++:多态-虚函数

C++ 中的多态性是面向对象编程中的一个重要概念,它允许在运行时选择不同的函数实现,以适应不同类型的对象。

多态的种类
编译时多态性(Compile-time Polymorphism):也称为静态多态性或早期绑定,指在编译时确定程序应该调用的函数或运算符版本的能力;主要通过函数重载(Function Overloading)和运算符重载(Operator Overloading)来实现。
​
运行时多态性(Runtime Polymorphism):也称为动态多态性或晚期绑定,指在程序运行时根据对象的实际类型来确定调用的函数版本的能力,主要通过虚函数(Virtual Functions)和继承来实现,在运行时根据对象的实际类型来确定调用哪个函数。
​
参数多态性(Parametric Polymorphism):也称为泛型编程(Generic Programming);是指一种通用的编程技术,它允许在编写代码时不指定具体的数据类型,而是以一般的方式编写代码,稍后根据需要使用具体的类型实例化代码。

关于函数重载、运算符重载和继承/虚继承在之前的文章就已经有阐述过了,接下去说一下运行时多态性中的虚函数。

虚函数

虚函数(Virtual Function)是在基类中声明为虚函数的成员函数,它的特点是可以被派生类重写(覆盖)。虚函数为实现运行时多态性提供了基础,它允许在派生类中重新定义基类的函数,并通过基类指针或引用调用时动态地选择调用哪个函数版本。

以下是一个简单的示例:

代码定义了一个基类 Animal 和一个派生类 Cat,其中 CatAnimal 的子类。每个类都有构造函数和析构函数,并且 Animal 类中定义了一个 eat() 函数,Cat 类中重写了这个函数。

//父类
class Animal
{
public:
    Animal(){};
    
    ~Animal(){};
    
    void eat(){
        std::cout << "Animal Eat 函数" << std::endl;
    };
};
​
//派生类
class Cat : public Animal
{
public:
    Cat(){};
    
    ~Cat(){};
    
    void eat(){
        std::cout << "cat Eat 函数" << std::endl;
    };
}
​
int main() {
​
    Animal * catObj = new Cat;
    catObj->eat();
    system("pause");
    return 0;
}

main() 函数中,创建了一个指向 Cat 对象的 Animal 指针 catObj。这是因为派生类对象可以被赋值给基类指针,因为派生类对象包含了基类对象的所有成员。然后,通过这个指针调用 eat() 函数,因为这是一个Cat对象所以在结果出来之前我会认为运行的时Cat类中的eat()方法,但事实上此时程序的输出内容为:

可以看到此时输出的内容时Animal类中的eat()函数而不是Cat类中的eat()函数,这是由于 eat() 函数在基类中被声明为非虚函数,而派生类中重新定义了这个函数,所以在运行时,尽管 catObj 指向的是 Cat 对象,但实际上调用的是基类 Animal 中的 eat() 函数,而不是派生类 Cat 中的版本。这是因为非虚函数的调用是静态绑定的,编译器在编译时就已经确定了调用的函数版本。

这个结果很明显时不符合我们的预期的,这个时候如果希望运行的是子类中的方法,那么此时我们可以将父类中的eat()函数设置为虚函数:

在 C++ 中,将一个成员函数声明为虚函数的方法是在函数声明前面加上 virtual 关键字。
//父类
class Animal
{
public:
    Animal(){};
    
    ~Animal(){};
    
    //虚函数
    virtual void eat(){
        std::cout << "Animal Eat 函数" << std::endl;
    };
};
​
//派生类
class Cat : public Animal
{
public:
    Cat(){};
    
    ~Cat(){};
    
    void eat(){
        std::cout << "cat Eat 函数" << std::endl;
    };
}
​
int main() {
​
    Animal * catObj = new Cat;
    catObj->eat();
    system("pause");
    return 0;
}

将父类中的eat()方法设置为虚函数后,此时再进行程序的运行,得到的结果为:

因为 eat() 函数在基类中被声明为虚函数,而且派生类中重新定义了这个函数,所以在运行时,通过指向派生类对象的基类指针调用 eat() 函数时,实际上会调用派生类 Cat 中的版本。这是因为虚函数的调用是动态绑定的,会根据对象的实际类型来确定调用的函数版本。

虚函数的使用原理涉及到动态绑定(Dynamic Binding)和虚函数表(Virtual Function Table)。
动态绑定:
动态绑定是指在运行时确定应该调用的函数版本,而不是在编译时确定;当通过基类指针或引用调用虚函数时,实际调用的是对象的实际类型对应的函数版本。

虚函数表
虚函数表(Virtual Function Table,简称 vtable)是 C++ 实现运行时多态性的重要机制之一;每个含有虚函数的类都有一个虚函数表,其中存储了指向各个虚函数的指针,当对象被创建时,会包含一个指向正确虚函数表的指针,通过这个指针,程序能够在运行时根据对象的实际类型来确定调用的虚函数版本。

我们们可以描绘出虚函数表的示意图,以便更好地理解虚函数的工作原理。

虚函数表示例:

每个含有虚函数的类都有一个虚函数表,其中存储了指向各个虚函数的指针,其中的指针顺序与虚函数在类定义中的声明顺序相同。要注意:当一个虚函数在父类中声明为虚函数时,它会自动成为子类中的虚函数。

1.Animal类虚函数表
+----------------------------------------+
|             虚函数表 (Animal)          |
+----------------------------------------+
|  指向 Animal::eat() 的指针             |
+----------------------------------------+
|  指向 类中某虚函数 的指针                |
+----------------------------------------+
|               ....                   |
+----------------------------------------+
​
2.Cat类虚函数表
+----------------------------------------+
|             虚函数表 (Cat)             |
+----------------------------------------+
|  指向 Cat::eat() 的指针                |
+----------------------------------------+
|  指向 类中某虚函数 的指针                |
+----------------------------------------+
|                    ....              |
+----------------------------------------+

含有虚函数的类实例化的对象:对象的前四个字节存储空间存储的是指向虚函数表的指针(对指针取值即可获得到对应类虚函数表的地址)

Animal对象
+-----------------------+
|       Animal 对象      |
+-----------------------+
|  指向虚函数表 (Animal) |
+-----------------------+
|    其他成员变量        |
+----------------------+
​
​
Cat对象
+-----------------------+
|         Cat 对象       |
+-----------------------+
|  指向虚函数表 (Cat)     |
+-----------------------+
|    其他成员变量         |
+-----------------------+

现在让我们来解释一下虚函数调用的过程:

int main() {
    Animal * catObj = new Cat;
    catObj->eat();
    system("pause");
    return 0;
}
  1. 创建对象:首先,我们创建了一个 Cat 类的对象,并将其地址赋给了一个 Animal 类型的指针 catObj。由于 eat() 函数在基类 Animal 中声明为虚函数,因此在 Cat 类的对象中,会包含一个指向正确虚函数表的指针,这个指针会指向 Cat 类的虚函数表。

  2. 调用虚函数:当我们通过 catObj 指针调用 eat() 函数时,编译器会根据指针所指向的对象的实际类型来决定应该调用哪个虚函数版本。然后,程序会使用对象中存储的虚函数表指针来找到正确的虚函数表。

  3. 查找函数指针:在找到了正确的虚函数表后,程序会在虚函数表中查找 eat() 函数对应的函数指针。由于 Cat 类中重写了 eat() 函数,因此在 Cat 类的虚函数表中,指向 Cat::eat() 函数的指针会被存储在相应位置上。

  4. 调用函数:最后,程序会通过找到的函数指针来调用 Cat::eat() 函数,输出 "cat Eat 函数"。

根据上面的虚函数的调用过程和原理我们也可以不使用->调用符号,而通过手动地址寻找得到循行的函数。

int main() {
​
    Animal * catObj = new Cat;
    
    //手动调用虚函数
    typedef void(*MyEat)();
    MyEat  myeat = (MyEat)*(int *)*(int *)catObj;
    myeat();
​
    delete catObj;
    system("pause");
    return 0;
}
1.*(int *)*(int *)catObj;解释:

虚函数表指针通常位于对象的内存布局的开始位置(可能是第一个成员或对象的隐藏成员);

(int *)catObj:这一步是将指向对象的指针 catObj 进行了类型转换,将其转换为 int* 类型指针

*(int *)catObj:接着,我们对转换后的指针进行了解引用操作;根据 C++ 中的指针运算规则,解引用操作会取出指针所指向的虚函数表内存地址处的值。

(int *)*(int *)catObj:将虚函数表内存地址转化为int* 类型指针,此时指针指向虚函数表内存地址。

*(int *)*(int *)catObj:最后,我们对转换后的整数地址进行解引用操作,得到的是该地址存储的值,也就是Cat类虚函数表中的第一个虚函数eat()的函数指针地址。

因为我们通过上述方法获得到了虚函数的函数地址,所以此时需要使用一个函数指针去指向该地址,对该虚函数进行调用。

2.typedef void(*MyEat)();解析:

这段代码定义了一个函数指针类型 MyEat,它可以指向一个没有参数且返回类型为 void 的函数。

void(*MyEat)();:这是一个函数指针的声明。在 typedef 关键字后面,我们声明了一个名为 MyEat 的新类型,它是一个指向函数的指针。括号中的 *MyEat 表示这是一个指针类型,而括号外的 () 表示这个指针所指向的函数的参数列表。(如果指向的函数地址有参数,那么再进行函数指针类型定义的时候也需要跟上参数列表)
3.(MyEat)*(int *)*(int *)catObj;解析:

此时我们将上述获得到的虚函数eat()的函数指针地址(此时类型为整型)类型强制转化为函数指针类型MyEat

4.MyEat myeat = (MyEat)*(int *)*(int *)catObj;解析:

并声明一个函数指针类型MyEat对象myeat,接着将该指针指向虚函数eat()的函数指针地址。

5.myeat();解析:

最后运行myeat()函数获得最后的结果:

最后得到的结果也是cat对象的eat()方法。

再此处下断点,查看函数指针的地址值;

在反汇编窗口查看该地址值的相关汇编代码,可以看到该地址指向的就是Cat类的Eat函数。

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

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

相关文章

java.lang.Exception: Test class should have exactly one public zero-

1.原因 Test方法所在类中,不能存在有参数构造函数,无参构造可以存在。JUnit在运行测试之前&#xff0c;会对测试类做一些初始化和验证工作。对于普通的非参数化测试&#xff0c;JUnit期望测试类有一个无参的公共构造函数&#xff0c;这样它才能够实例化测试类并执行其中的测试方…

K8S快速入门

K8S快速入门 在学习k8s的过程&#xff0c;虽然官网给出的示例教程很简单&#xff0c;但是由于网络和环境的差异&#xff0c;导致实际操作的时候踩了很多坑&#xff0c;下面记录一下自己的操作步骤&#xff0c;方便需要的人参考&#xff0c;也方便以后的自己。 参考官网的资料…

uni-app+vue3 +uni.connectSocket 使用websocket

前言 最近在uni-appvue3websocket实现聊天功能&#xff0c;在使用websocket还是遇到很多问题 这次因为是app手机应用&#xff0c;就没有使用websocket对象&#xff0c;使用的是uni-app的uni.connectSocket 为了方便测试这次用的是node.js一个简单的dom&#xff0c;来联调模拟…

五分钟解决Springboot整合Mybaties

SpringBoot整合Mybaties 创建maven工程整合mybaties逆向代码生成 创建maven工程 1.通过idea创建maven工程如下图 2.生成的工程如下 以上我们就完成了一个maven工程&#xff0c;接下来我们改造成springboot项目。 这里主要分为三步&#xff1a;添加依赖&#xff0c;增加配置&…

Spring_概述

Spring 官网Spring Framework&#xff08;Spring&#xff09;文档位置重点内容Overview 官网 Spring官网 Spring Framework&#xff08;Spring&#xff09; 文档位置 重点 IoC容器AOP&#xff1a;面向切面编程AOT&#xff1a;ahead of time&#xff0c;提前编译Web 框架&…

20240507 ubuntu20.04+ros noetic 跑通lioslam

任务&#xff1a;跑通lioslam 主要参考博客 IMU激光雷达融合使用LIO-SAM建图学习笔记——详细、长文、多图、全流程_ubuntu_AIDE回归线-GitCode 开源社区 (csdn.net) 1.不要用这一句 wget -O ~/Downloads/gtsam.zip https://github.com/borglab/gtsam/archive/4.0.0-alpha2…

电商大数据的采集||电商大数据关键技术【基于Python】

.电商大数据采集API 什么是大数据&#xff1f; 1.大数据的概念 大数据即字面意思&#xff0c;大量数据。那么这个数据量大到多少才算大数据喃&#xff1f;通常&#xff0c;当数据量达到TB乃至PB级别时&#xff0c;传统的关系型数据库在处理能力、存储效率或查询性能上可能会遇…

Mac idea gradle解决异常: SSL peer shut down incorrectly

系统&#xff1a;mac 软件&#xff1a;idea 解决异常: SSL peer shut down incorrectly 查看有没有安装 gradle -v安装 根据项目gradle提示安装版本 brew install gradle7idea的配置 在settings搜索gradle&#xff0c;配置Local installation&#xff0c;选择自己的安装目录…

c++编程(10)——string

欢迎来到博主的专栏——c编程 博主ID&#xff1a;代码小豪 文章目录 <string>string类的接口构造、析构、与赋值重载构造函数赋值重载运算符 元素访问operator[] 容量修改器对string对象的操作迭代器 std::string是定义在c标准的一个类&#xff0c;定义在标准库<strin…

【SAP ME 34】POD操作面板打开内部异常500内部异常

解决方案&#xff1a; 切换到configtool目录&#xff0c;打开configtool可执行文件

win10使用问题

ThinkPad进入bios一种方式是F1 win10有了一个BitLocker&#xff0c;所以在更改bios里面的一些设置&#xff0c;会要求输入恢复密钥&#xff0c;才能生效。 恢复密钥在Microsoft账户里可以找到。 1. 坑爹的Secure Boot设置 坑爹的Secure Boot设置 - 简书 2. 在安装昆仑通态…

小程序搜索排名优化 三步操作提升

搜索排名优化最直接的一个目的就是为了提升小程序的排名和流量&#xff0c;获取用户的信任度。当用户在搜索关键词的时候&#xff0c;能让用户看到小程序&#xff0c;增加被发现和点击的机会。 一、关键词优化&#xff1a; 1.选择合适的关键词&#xff1a;选择与小程序内容高…

昂科烧录器支持Infineon英飞凌的三相电机驱动器TLE9877QXA40

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中Infineon英飞凌的三相电机驱动器TLE9877QXA40已经被昂科的通用烧录平台AP8000所支持。 TLE9877QXA40是一款单芯片三相电机驱动器&#xff0c;集成了行业标准的ARMCortex™M3 内…

Poisson_Image-Editing

1.算法介绍 快速泊松图像编辑&#xff08;Fast Poisson Image Editing&#xff09;是一种图像处理算法&#xff0c;用于将源图像的某个区域无缝地嵌入到目标图像中。它基于泊松方程的性质&#xff0c;通过求解离散化的泊松方程来实现图像的融合。该算法的核心思想是&#xff0c…

[Linux][网络][TCP][四][流量控制][拥塞控制]详细讲解

目录 1.流量控制2.拥塞控制0.为什么要有拥塞控制&#xff0c;不是有流量控制么&#xff1f;1.什么是拥塞窗口&#xff1f;和发送窗口有什么关系呢&#xff1f;2.怎么知道当前网络是否出现了拥塞呢&#xff1f;3.拥塞控制有哪些算法&#xff1f;4.慢启动5.拥塞避免6.拥塞发生7.快…

【XR806开发板试用】阻塞式串口发送与接收教程

本文基于wsl2搭建的ubuntu18.04 vscode编辑器 很奇怪啊&#xff0c;找了半天居然没人发串口的教程&#xff0c;于是只能自己试一试了&#xff0c;在此发一个阻塞式的串口发送与接收的教程。并且&#xff0c;感谢.ACE彭洪权大佬在我配置环境遇到几十个报错的时候帮我远程搭建环…

校园论坛系统基于PHP的校园管理系统毕设校园好感度系统 校园文化建设系统APP小程序H5前后端源码交付支持二开,一次付款,终生使用

APP小程序H5前后端源码交付&#xff0c;支持二开&#xff0c;一次付款&#xff0c;终身使用&#xff0c;免费更新系统本身源码。 校园社交网络系统开发是一个复杂且综合性的项目&#xff0c;旨在为学生、教师和管理人员提供一个互动、分享和交流的平台。以下是一个关于校园社交…

燃料电池发电系统详解

目录 前言 组成结构 系统参数 常见问题 参考资料 前言 见《氢燃料电池技术综述》 见《燃料电池工作原理详解》 组成结构 燃料电池发电系统&#xff0c;由多个子系统和子模块组成&#xff0c;示例如下&#xff1a; 燃料处理系统&#xff08;fuel processing system&#xf…

使用 Kubeadm 搭建个公网 k8s 集群(单控制平面集群)

前言 YY&#xff1a;国庆的时候趁着阿里云和腾讯云的轻量级服务器做促销一不小心剁了个手&#x1f60e;&#x1f622;&#xff0c;2 Cores&#xff0c;4G RAM 还是阔以的&#xff0c;既然买了&#xff0c;那不能不用呀&#x1f6a9;&#xff0c;之前一直想着搭建个 k8s 集群玩…

详解MySQL常用的数据类型

前言 MySQL是一个流行的关系型数据库管理系统&#xff0c;它支持多种数据类型&#xff0c;以满足不同数据处理和存储的需求。理解并正确使用这些数据类型对于提高数据库性能、确保数据完整性和准确性至关重要。本文将详细介绍MySQL中的数据类型&#xff0c;包括数值类型、字符…
最新文章