C++:多态-虚函数

news2024/11/18 11:33:18

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.coloradmin.cn/o/1655749.html

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

相关文章

容联云孔淼:大模型落地与全域营销中台建设

近日&#xff0c;由金科创新社主办的2024区域性商业银行数智化转型研讨会顺利召开&#xff0c; 容联云产业数字云事业群副总经理、诸葛智能创始人孔淼受邀出席&#xff0c;并分享数智化转型实践经验。 他分享了容联云两大核心产品&#xff0c;“大模型应用容犀Copilot”在金融营…

SpringBoot Actuator未授权访问漏洞的解决方法

1. 介绍 Spring Boot Actuator 是一个用于监控和管理 Spring Boot 应用程序的功能模块。它提供了一系列生产就绪的功能&#xff0c;帮助你了解应用程序的运行状况&#xff0c;以及在运行时对应用程序进行调整。Actuator 使用了 Spring MVC 来暴露各种 HTTP 或 JMX 端点&#x…

嘴尚绝卤味:传承经典,缔造美食新风尚

卤味&#xff0c;作为中国传统美食的代表之一&#xff0c;历经千年的传承与发展&#xff0c;早已成为无数食客餐桌上的宠儿。而在这个美食盛行的时代&#xff0c;嘴尚绝卤味凭借其独特的口感和精湛的工艺&#xff0c;成为卤味市场中的佼佼者&#xff0c;引领着卤味文化的新潮流…

Linux(openEuler、CentOS8)常用的IP修改方式(文本配置工具nmtui+配置文件+nmcli命令)

----本实验环境为openEuler系统<以server方式安装>&#xff08;CentOS类似&#xff0c;可参考本文&#xff09;---- 一、知识点 &#xff08;一&#xff09;文本配置工具nmtui(openEuler已预装) nmtui&#xff08;NetworkManager Text User Interface&#xff09;是一…

【系统架构师】-案例篇-UML用例图

1、概述 用于表示系统功能需求&#xff0c;以及应用程序与用户或者与其他应用程序之间的交互关系。 2、组成 参与者&#xff08;Actors&#xff09;&#xff1a;与系统交互的用户或其他系统。用一个人形图标表示。用例&#xff08;Use Cases&#xff09;&#xff1a;系统需要…

大家都是怎么写毕业论文的? 推荐4个AI工具

写作这件事一直让我们从小学时期就开始头痛&#xff0c;初高中时期800字的作文让我们焦头烂额&#xff0c;一篇作文里用尽了口水话&#xff0c;拼拼凑凑才勉强完成。 大学时期以为可以轻松顺利毕业&#xff0c;结果毕业前的最后一道坎拦住我们的是毕业论文&#xff0c;这玩意不…

c++ 入门2

五. 函数重载 函数重载&#xff1a;是函数的一种特殊情况&#xff0c;C允许在同一作用域中声明几个功能类似的同名函数&#xff0c;这 些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同&#xff0c;常用来处理实现功能类似数据类型 不同的问题。 1、参数类型不同 #inc…

【ITK配准】第十一期 空间对象的模糊构建配准样例

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 公众号:VTK忠粉 前言 本文分享ITK配准中的空间对象的模糊构建配准样例,希望对各位小伙伴有所帮助! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! 你的点赞就是我的动力(^U^)ノ~YO 空间…

2024软件测试自动化面试题(含答案)

1.如何把自动化测试在公司中实施并推广起来的&#xff1f; 选择长期的有稳定模块的项目 项目组调研选择自动化工具并开会演示demo案例&#xff0c;我们主要是演示selenium和robot framework两种。 搭建自动化测试框架&#xff0c;在项目中逐步开展自动化。 把该项目的自动化…

【高阶数据结构(二)】初识图论

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:高阶数据结构专栏⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Go语言知识   &#x1f51d;&#x1f51d; 高阶数据结构 1. 前言2. 图的基…

vector、heap数组、stack数组访问性能验证

测试目的 本次测试旨在比较不同数据结构&#xff08;vector、数组&#xff09;以及不同访问方法&#xff08;[]、at()、offset&#xff09;在性能上的差异&#xff0c;从而为开发者提供在特定情境下做出最佳选择的依据。 测试代码 测试网址:Quick C Benchmarks 使用GCC9.5 …

基于YOLO的车牌与车型识别系统

一、项目背景与意义 随着智能交通系统的快速发展&#xff0c;车辆识别技术在交通管理、安防监控、自动收费、停车管理等领域发挥着至关重要的作用。车牌识别和车型识别作为车辆识别技术的核心组成部分&#xff0c;能够有效提升交通运营效率&#xff0c;加强公共安全监控&#…

Baidu Comate智能编码助手:引领编码新时代的智能伙伴

前言 在数字化高速发展的今天&#xff0c;编程技术已成为推动创新与技术革新的核心动力。伴随着软件项目的复杂性和规模不断扩大&#xff0c;编码过程中的挑战也日益增加。为了解决这些问题以达到降本增效的目的&#xff0c;百度推出了基于文心大模型的Baidu Comate智能编码助…

语义分割——前列腺分割数据集

引言 亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 …

SQL统计语句记录

1.达梦数据库 统计指定单位的12个月份的业务数据 SELECT a.DEPT_ID, b.dept_name, a.USER_NAME, count(a.dept_id) as count, sum(case when to_char(a.CREATE_TIME,yyyy-mm) 2023-01 THEN 1 else 0 end) as one,sum(case when to_char(a.CREATE_TIME,yyyy-mm) 2023-02 T…

【前端热门框架【vue框架】】——对组件进行更加简洁合理的处理和解释(一)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;程序员-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

量化交易T0策略:非凸T0算法

T0策略又称日内交易策略&#xff0c;它的持仓时间较短&#xff0c;基于对未来短期股价走势的判断&#xff0c;通过低位买入、高位卖出的方式来获得价差收益&#xff0c;并且买入卖出交易在日内完成。 分类 策略逻辑分类(融券T0和底仓T0) 融券T0在券商创立两融账号&#xff0c…

# 从浅入深 学习 SpringCloud 微服务架构(八)Sentinel(2)

从浅入深 学习 SpringCloud 微服务架构&#xff08;八&#xff09;Sentinel&#xff08;2&#xff09; 一、sentinel&#xff1a;通用资源保护 1、Rest 实现熔断 Spring Cloud Alibaba Sentinel 支持对 RestTemplate 的服务调用使用 Sentinel 进行保护, 在构造 RestTemplate…

Lazada商品详情API接口:深度解析与应用

前言 在当今电子商务的繁荣时代&#xff0c;对于电商平台来说&#xff0c;提供一套高效、稳定的API接口是非常重要的。Lazada&#xff0c;作为东南亚领先的电商平台之一&#xff0c;其API接口体系为卖家、开发者以及第三方服务提供了丰富的功能和数据支持。其中&#xff0c;商品…

多线程学习Day09

10.Tomcat线程池 LimitLatch 用来限流&#xff0c;可以控制最大连接个数&#xff0c;类似 J.U.C 中的 Semaphore 后面再讲 Acceptor 只负责【接收新的 socket 连接】 Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】 一旦可读&#xff0c;封装一个任务对象&#x…