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.mzph.cn/pingmian/8745.shtml

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

相关文章

【网络协议】----IPv6协议报文、地址分类

【网络协议】----IPv6协议简介 【网络协议】----IPv6协议简介IPv6特点IPv4 和 IPv6报文结构IPv6报文格式-拓展报头 IPv6地址分类IPv6地址表示IPv6单播地址可聚合全球单播地址链路本地地址唯一本地地址特殊地址补充 接口标识&#xff08;主机位&#xff09;生成方法通过EUI-64规…

CISCN 2023 初赛

Web unzip 文件上传页面 upload.php页面源码显示了出来 <?php error_reporting(0); highlight_file(__FILE__);$finfo finfo_open(FILEINFO_MIME_TYPE); if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) application/zip){exec(cd /tmp &am…

IP协议,网络层

一、IP协议报文 在网络层最主要的协议是IP协议&#xff0c;网络层的主要任务是进行&#xff1a;1.地址管理 2.路由选择 地址管理&#xff1a;使用一套地址体系&#xff0c;描述互联网中每个设备所处的位置。 IP地址有两个版本&#xff0c;1.IPV4 2.IPV6 &#xff0c;IP…

信奥数据“信息差”,让你惊掉下巴!

✅ 信奥红利分析 暑假信奥赛即将到来&#xff0c;在全国各地赛事也越来越受到重视&#xff0c;但是似乎关于红利这一块各地如何&#xff0c;并没有太多的老师给各位家长分析清楚。 那么今天曹老师就主要给从各位新手家长分析一下信奥红利地区&#xff0c;在开始分析之前请大家看…

使用 MediaMTX 和 FFmpeg 推拉 RTSP 流媒体

实时流传输协议 RTSP&#xff08;Real-Time Streaming Protocol&#xff09;是 TCP/IP 协议体系中的一个应用层协议&#xff0c;由哥伦比亚大学、网景和 RealNetworks 公司提交的 IETF RFC 标准。该协议定义了一对多应用程序如何有效地通过 IP 网络传送多媒体数据。RTSP 在体系…

初始化创建一个webpack项目

新建一个空的工程 -> % mkdir webpack-project 为了方便追踪执行每一个命令&#xff0c;最终产生了哪些变更&#xff0c;将这个空工程初始化成git项目 -> % cd webpack-project/-> % git init Initialized empty Git repository in /Users/lixiang/frontworkspace/…

初探MFC程序混合使用QT

一、背景 随着操作系统国产化替代的趋势越发明显&#xff0c;软件支持国际化、跨平台&#xff0c;已然是必须做的一件事情。原有的软件UI层用的是MFC&#xff0c;将其换成QT&#xff0c;想必是一种较好的方案。对于大型软件&#xff0c;特别是已发布&#xff0c;但还处于不断迭…

C++进阶之路:探索访问限定符、封装与this指针的奥秘(类与对象_上篇)

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

数据统计:词频统计、词表生成、排序及计数、词云图生成

文章目录 &#x1f4da;输入及输出&#x1f4da;代码实现 &#x1f4da;输入及输出 输入&#xff1a;读取一个input.txt&#xff0c;其中包含单词及其对应的TED打卡号。 输出 output.txt&#xff1a;包含按频率降序排列的每个单词及其计数&#xff08;这里直接用于后续的词云…

设计模式之单例模式详解

单例模式 描述&#xff1a;单例&#xff08;Singleton&#xff09;模式的定义&#xff1a;指一个类只有一个实例&#xff0c;且该类能自行创建这个实例的一种模式。 核心特点 单例类只有一个实例对象&#xff1b;该单例对象必须由单例类自行创建&#xff1b;单例类对外提供一…

OpenCV 入门(一) —— OpenCV 基础

OpenCV 入门系列&#xff1a; OpenCV 入门&#xff08;一&#xff09;—— OpenCV 基础 OpenCV 入门&#xff08;二&#xff09;—— 车牌定位 OpenCV 入门&#xff08;三&#xff09;—— 车牌筛选 OpenCV 入门&#xff08;四&#xff09;—— 车牌号识别 OpenCV 入门&#xf…

每日两题 / 23. 合并 K 个升序链表 94. 二叉树的中序遍历(LeetCode热题100)

23. 合并 K 个升序链表 - 力扣&#xff08;LeetCode&#xff09; 若lists有k个元素&#xff0c;调用k - 1次&#xff08;两个有序链表的合并&#xff09;即可 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNod…

探索鸿蒙开发:鸿蒙系统如何引领嵌入式技术革新

嵌入式技术已经成为现代社会不可或缺的一部分。而在这个领域&#xff0c;华为凭借其自主研发的鸿蒙操作系统&#xff0c;正悄然引领着一场技术革新的浪潮。本文将探讨鸿蒙开发的特点、优势以及其对嵌入式技术发展的深远影响。 鸿蒙操作系统的特点 鸿蒙&#xff0c;作为华为推…

一键接入电商API数据接口淘宝API通过商品ID、URL采集商品详情页实时数据API接入指南

一键接入电商API数据接口&#xff0c;尤其是淘宝API&#xff0c;通常需要遵循以下步骤&#xff1a; 注册账号&#xff1a;注册接入账号获取Api Key和Api Secret。 选择API&#xff1a;根据需要选择合适的API服务&#xff0c;如通过商品ID或URL采集商品详情页数据的API。 权限…

WPF鼠标拖拽的最佳实现

WPF鼠标拖拽的最佳实现 在很多项目中都会遇到鼠标拖拽控件移动的需求&#xff0c;常见的有从在列表中拖拽列表项移动&#xff0c;拖拽控件移动等。 本文将介绍2种拖拽的简单的实现 列表项的拖拽 本文将使用 gong-wpf-dragdrop 这个github上的库来实现列表的拖拽的效果&…

GitLab使用记录

GitLab 文章目录 1. 常用命令1.1 配置邮箱 用户名1.2 查看配置1.3 基本语法 2. 连接gitlab3. 直接拉去项目 1. 常用命令 1.1 配置邮箱 用户名 git config --global user.name ShangzheChen git config --global user.email 735511377qq.com1.2 查看配置 cat ~/.gitconfig这…

企业信使_登陆页

在当今数字化时代&#xff0c;企业与员工之间的沟通变得越来越重要。为了满足企业内部沟通的需求&#xff0c;一款功能强大而方便使用的企业信使_登陆页应运而生。企业信使_登陆页是一种专为企业内部使用而设计的通讯工具&#xff0c;可以帮助企业提高沟通效率&#xff0c;加强…

4.用python爬取保存在text中的格式为m3u8的视频

文章目录 一、爬取过程详解1.寻找视频的m3u8链接2.从网页源码中寻找视频的m3u8链接的第二部分内容3.从视频的m3u8链接获取视频 二、完整的代码 一、爬取过程详解 1.寻找视频的m3u8链接 这个文档承接了爬虫专栏的 第一节.python爬虫爬取视频网站的视频可下载的源url&#xff0…

车规级低功耗汽车用晶振SG-9101CGA

车规级晶振SG-9101CGA属于爱普生9101系列&#xff0c;是一款可编程晶振。SG-9101CGA车规级晶振采用2.5x2.0mm封装&#xff0c;利用PLL技术生产&#xff0c;此款振荡器的频率范围从0.67M~170MHZ任一频点可选&#xff0c;步进1ppm&#xff0c;采用标准CMOS输出&#xff0c;最大输…

为 Flutter 应用设置主题:ThemeData 和 ColorScheme 指南

在媒体和其他来源中有许多关于这个主题的文章&#xff0c;那么这篇文章的必要性是什么&#xff1f; 在本文中&#xff0c;我计划仅关注 ThemeData 小部件的关键点以及我的开发经验中最常用的参数&#xff0c;并且您将获得有关每个参数如何对您的应用程序执行操作的简要说明。 …