C++ 多态

news/2024/2/29 16:14:06/文章来源:https://blog.csdn.net/2201_75352211/article/details/135396061

文章目录

  • 多态的概念
  • 虚函数
  • 抽象类
  • 多态的原理
    • 虚函数指针与虚函数表
    • 多继承与虚函数表
    • 菱形虚拟继承
  • 动态绑定与静态绑定

多态的概念

多种状态 —— 不同对象去完成某个行为,会有不同的状态

虚函数

virtual void Func(){;} //虚函数,和之前的虚继承没什么关联,只是都用了 virtual 关键字
//关于买票的多态
class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
//子类父类中函数名、参数、返回值相同,带有 virtual,此时两函数不构成隐藏,而是重写/覆盖
void Func(Person& p)//父类的指针或引用去调用
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);//输出 "买票-全价" Func(st);//输出 "买票-半价" return 0;
}

多态的条件(达到多态的效果所需要的条件)

  1. 虚函数重写
  2. 父类的指针或引用去调用

★ 有下面这样一段代码:

class Person {
public:~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:~Student() { cout << "~Student()" << endl; }
};int main()
{Person* ptr1 = new Person;Person* ptr2 = new Student;delete ptr1;delete ptr2;return 0;
}

输出结果(发现子类析构有问题):
在这里插入图片描述
原因:
首先,我们之前学过 delete 的原理:调用析构函数,调用 operator delete 释放空间

在不考虑多态的情况下:相应类型的指针调用相应的函数(即如图所示调用了两个Person的析构)
对此,设计者用多态解决了这个问题:析构函数函数名会被统一处理成destructor(以此重写函数)(但这也使得不加 virtual 时 destructor 函数构成隐藏,这也就是前面学习继承时遇到的问题的来源),然后在两析构函数前加上 virtual 就构成了多态,问题解决

关于 “相应类型的指针调用相应的函数(访问相应的成员)”:
在这里插入图片描述
不满足多态时,函数构成隐藏,但子类对象调用时隐藏才会被体现,父类调用时没这回事

补充:
不满足多态:看调用者的类型,调用这个类型的成员函数(指针/引用变量本身的类型是谁就调用谁的成员函数)
满足多态: 看指向的对象的类型,调用这个类型的成员函数(指针/引用变量指向/引用的是谁就调用谁的成员函数)

● 多态(虚函数重写)的特殊情况:

  1. 子类中可以不加 virtual:虚函数重写的原理是子类继承父类的接口(函数声明),重写实现
  2. 协变:虚函数重写时,返回值可以不同,但必须是父子关系的指针或引用
class A{};
class B : public A {};
class Person {
public:virtual A* f() {return new A;}
};
class Student : public Person {
public:virtual B* f() {return new B;}
};

相关习题 1:
在这里插入图片描述

解析:
在这里插入图片描述
相关习题 2:在这里插入图片描述
解析:
在这里插入图片描述

重载、覆盖(重写)、隐藏(重定义)
在这里插入图片描述

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象

class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};

抽象类的子类会继承纯虚函数,子类可以通过重写覆盖纯虚函数

多态的原理

虚函数指针与虚函数表

普通的成员函数:

class Person
{
public:void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:void BuyTicket() { cout << "买票-半价" << endl; }//重写
};
void Func(Person& p) {//父类引用调用,构成多态p.BuyTicket();
}
int main() {Person p;  Func(p);Student s; Func(s);return 0;
}

调试时发现对象里什么都没有:
在这里插入图片描述
构成多态时:

class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void func1() { ; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }//重写virtual void func1() { ; }
};
void Func(Person& p) {//父类引用调用,构成多态p.BuyTicket();
}
int main() {Person p;  Func(p);Student s; Func(s);return 0;
}

调试时发现对象中都有一个叫做_vfptr的指针(virtual function (table) pointer – 虚函数表指针)
在这里插入图片描述

虚函数表存储着父/子类中用 virtual 修饰的函数的地址

我们写的调用函数的代码在编译期间被处理成相应的指令:

BuyTicket(地址)

普通函数调用,编译期间直接将相应的函数地址填入
构成多态时,编译期间通过传入函数的参数(即父类的指针或引用)中的虚函数指针找到相应的虚函数表里存储的虚函数地址并将其填入

● 编译器根据是否构成多态,编译时将对应的函数调用处理成不同的指令

通过调试我们可以看到父类的指针/引用 指向/表示子类对象时能使用子类的虚函数表指针:
在这里插入图片描述
因为这样的设计,如果传入父类,编译器可以直接访问父类的虚函数表指针;如果传入子类,编译器也照样能从父类的指针/引用里获取子类的虚函数表指针,从而实现传入什么调用什么

● 为什么要规定形参必须是父类的指针或引用?直接传对象给形参不行吗(指针、引用是直接访问虚函数表指针,那直接传参构建一个新的对象的内部就有该指针,照样可以访问不是吗)?
如果允许这么做,你就有机会得到一个父类对象,但其内部存储的却是子类的虚函数表指针 —— 全乱了

● 为什么要设计成用虚函数指针指向虚函数表而不是直接把虚函数表写在对象里?
类的成员共用一份虚函数表,节省空间

● 虚函数表什么时候初始化?
构造函数的初始化列表

● 虚函数表存在哪里?
常量区

补充:只要有虚函数就有虚表(虚表的有无与是否构成多态无关),通过虚函数表指针去寻找虚函数地址的行为称为 多态调用;

多继承与虚函数表

● 打印虚函数表(测试用,了解即可)

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
};
//用程序打印虚表
typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR table[])//VF_PTR* table
{for (int i = 0; table[i] != nullptr; ++i)//VS中的虚函数表最后以nullptr结尾,所以我们可以这么写{printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}int main()
{Derive d;//打印子类中Base1的虚函数表PrintVFTable((VF_PTR*)(*(int*)&d));//打印子类中Base2的虚函数表//PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1))));Base2* ptr2 = &d;PrintVFTable((VF_PTR*)(*(int*)(ptr2)));return 0;
}

多继承时存在的问题:

//这部分就是摘自上面的代码
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
};
int main() {Derive d;Base1* ptr1 = &d;Base2* ptr2 = &d;ptr1->func1();ptr2->func1();return 0;
}

子类中func1被重写,但调试时我们发现ptr1与ptr2中的虚函数指针指向的虚函数表中func1的函数地址却不同(如下图),这是为什么呢?(忘记从x64切到x86的环境了,不过过程大差不差)
在这里插入图片描述

值得(给自己)一提的是,Derive多继承了Base1和Base2,也就有了2张虚表(如上图)
还有就是如果你去打印Derive里的两个虚表,你会发现func3只被记录在Base1的虚表里(了解就行,毕竟编译器就是这么写的):
在这里插入图片描述

我们可以查看反汇编的指令:

在这里插入图片描述

分析:Base2* ptr 表示的地址经过了封装。可以看到Base1*ptr2 在调用函数过程指令较多,其中有一步为“ sub ”,这一步是在修正this指针
在这里插入图片描述

func1构成了多态,而ptr2理应指向子类对象d,但Base2*指针会发生偏移(如图),因此需要修正其指向的位置

菱形虚拟继承

这部分可以略过,说起来太麻烦,而且用的少考不到的,索性就不具体说了

class A
{
public:virtual void func1(){}
public:int _a;
};class B : virtual public A
{
public:virtual void func1(){}virtual void func2(){}
public:int _b;
};class C : virtual public A
{
public:virtual void func1(){}virtual void func3(){}
public:int _c;
};class D : public B, public C
{
public:virtual void func1()//此处必须重写,不然不知道D对象的虚函数表里的func{}
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在这里插入图片描述

在这里插入图片描述

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
    比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
    行为,调用具体的函数,也称为动态多态

动态多态(也就是这讲了半天的多态)的虚函数表在编译期间也就确定了,运行时确定的是虚函数表指针

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

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

相关文章

华为设备端口镜像设置

核心代码&#xff1a; observe-port int 编号 int 编号 mirror to observe-port both | inbound | outbound #both:将镜像端口的入和出流量同时复制到观察者端口 #inbound:将镜像端口的入流量复制到观察者端口 #outbound:将镜像端口的出流量复制到观察者端口配置后可使出入端口…

解决ELK日志收集中Logstash报错的关键步

ElK执行日志收集的时候logstash报错&#xff1a; Failed to execute action {:action>LogStash::PipelineAction::Create/pipeline_id:main, :exception>“LogStash::ConfigurationError”, :message>“Expected one of [^\r\n], “\r”, “\n” at line 88, column 4…

C++学习笔记——标准模板库

目录 一、简介 二、STL概述 2.1STL是什么&#xff1f; 2.2STL的优势 三、容器&#xff08;Containers&#xff09; 3.1序列式容器&#xff08;Sequence Containers&#xff09; 3.2关联式容器&#xff08;Associative Containers&#xff09; 3.3容器适配器&#xff08;…

python统计分析——操作案例(模拟抽样)

参考资料&#xff1a;用python动手学统计学 import numpy as np import pandas as pd from matplotlib import pyplot as plt import seaborn as snsdata_setpd.read_csv(r"C:\python统计学\3-4-1-fish_length_100000.csv")[length] #此处将文件路径改为自己的路…

centos 7 上如何安装chrome 和chrome-driver

centos 7 上如何安装chrome 和chrome-driver 查找自己的服务器是什么系统 cat /etc/os-release这里以centos linux 7为例 下载google-chrome安装包 wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm安装chrome sudo yum localinstall go…

从 PDF 删除PDF 页面的 10 大工具

PDF 文件是全世界几乎每个人最常用的页面之一。借助 PDF 文件&#xff0c;您可以通过任何在线或离线媒体轻松共享信息。但是&#xff0c;如果您想编辑这些 PDF 文件&#xff0c;那么这个过程就很难改变&#xff0c;因为保持文件的原始形式和质量很重要。应该注意的是&#xff0…

机器学习激活函数

激活函数 激活函数是人工神经网络中的一个重要组成部分。它们用于向神经网络中添加非线性因素&#xff0c;使得网络能够解决复杂问题&#xff0c;如图像识别、语言处理等。激活函数的作用是决定一个神经元是否应该被激活&#xff0c;也就是说&#xff0c;它帮助决定神经元的输…

NLP论文阅读记录 - 2022 | WOS 用于摘要法律文本的有效深度学习方法

文章目录 前言0、论文摘要一、Introduction1.1目标问题 二.相关工作三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结 前言 Effective deep learning approaches for summarization of legal texts&#xff08;22&#x…

android 13.0 Launcher3长按app弹窗设置为圆角背景功能实现二

1.前言 在13.0的系统ROM定制化开发中,在进行一些Launcher3的定制化开发中,在使用app的弹窗的功能时,会弹出应用信息和 微件之类的内容,所以在定制需求中,需要默认设置为圆角背景,接下来就来分析下相关功能的实现如图: 2.Launcher3长按app弹窗设置为圆角背景功能实现二的…

基于深度学习的婴儿啼哭识别项目详解

基于深度学习的婴儿啼哭识别项目详解 基于深度学习的婴儿啼哭识别项目详解一、项目背景1.1 项目背景1.2 数据说明 二、PaddleSpeech环境准备三、数据预处理3.1 数据解压缩3.2 查看声音文件3.3 音频文件长度处理 四、自定义数据集与模型训练4.1 自定义数据集4.2 模型训练4.3 模型…

【ACL 2023】 The Art of Prompting Event Detection based on Type Specific Prompts

【ACL 2023】 The Art of Prompting: Event Detection based on Type Specific Prompts 论文&#xff1a;https://aclanthology.org/2023.acl-short.111/ 代码&#xff1a;https://github.com/VT-NLP/Event_APEX Abstract 我们比较了各种形式的提示来表示事件类型&#xff0…

C#,入门教程(66)——枚举Enum的高等用法

前言&#xff1a;国内码农与国外优秀程序员的最大区别是&#xff0c;我们的专家、教授喜欢唾沫横飞地&#xff0c;夸夸其谈语言特性、框架、性能&#xff0c;唯一目的是带私货&#xff08;书籍或教程&#xff09;&#xff0c;很少能写出真有用的程序。差距在哪呢&#xff1f;基…

扫雷游戏棋盘的打印,判断输赢,深度分析

少年们&#xff0c;大家好&#xff0c;我是博主那一脸阳光&#xff0c;我来分享扫雷的打印和判断输赢&#xff0c;代码如何编写&#xff0c;如何使用&#xff0c;深度理解扫雷的游戏。 数据结构的分析和理论 我上次介绍棋盘的初始化&#xff0c;但是如果不打印出来&#xff0…

图解智慧:数据可视化如何助你高效洞悉信息?

在信息爆炸的时代&#xff0c;数据扮演着越来越重要的角色&#xff0c;而数据可视化则成为解读和理解海量数据的得力工具。那么&#xff0c;数据可视化是如何帮助我们高效了解数据的呢&#xff1f;下面我就以可视化从业者的角度来简单聊聊这个话题。 无需深奥的专业知识&#x…

【Leetcode 2707】字符串中的额外字符 —— 动态规划

2707. 字符串中的额外字符 给你一个下标从0开始的字符串s和一个单词字典dictionary。你需要将s分割成若干个互不重叠的子字符串&#xff0c;每个子字符串都在dictionary中出现过。s中可能会有一些额外的字符不在任何子字符串中。 请你采取最优策略分割s&#xff0c;使剩下的字…

【ONE·MySQL || 常见的基本函数】

总言 主要内容&#xff1a;介绍了MySQL中常用的基本函数。一些聚合函数、时间日期函数、字符串函数、数字函数等。       文章目录 总言1、聚合函数1.1、汇总1.2、COUNT()函数1.2.1、基本说明1.2.2、使用演示 1.3、SUM( )函数1.3.1、基本说明1.3.2、使用演示 1.4、AVG( )函…

java基础知识点系列——数据输入(五)

java基础知识点系列——数据输入&#xff08;五&#xff09; 数据输入概述 Scanner使用步骤 &#xff08;1&#xff09;导包 import java.util.Scanner&#xff08;2&#xff09;创建对象 Scanner sc new Scanner(System.in)&#xff08;3&#xff09;接收数据 int i sc…

PHP开发日志 ━━ php8.3安装与使用组件Xdebug

今天开头写点历史&#xff1a; 二十年前流行asp&#xff0c;当时用vb整合常用函数库写了一个dll给asp调用&#xff0c;并在此基础上开发一套仿windows界面的后台管理系统&#xff1b;后来asp逐渐没落&#xff0c;于是在十多年前转投php&#xff0c;不久后用php写了一套mvc框架&…

C++——map和set的基本使用

目录 一&#xff0c;关联式容器 二&#xff0c;键值对 三&#xff0c;set的使用 3.1 set介绍 3.2 set的插入和删除 3.3 set的pair 3.4 multiset 四&#xff0c;map的使用 4.1 map介绍 4.2 map实现简易字典 4.3 map实现统计次数 4.4 map的[] 五&#xff0c;使用map或…

TRB 2024论文分享:基于生成对抗网络和Transformer模型的交通事件检测混合模型

TRB&#xff08;Transportation Research Board&#xff0c;美国交通研究委员会&#xff0c;简称TRB&#xff09;会议是交通研究领域知名度最高学术会议之一&#xff0c;近年来的参会人数已经超过了2万名&#xff0c;是参与人数和国家最多的学术盛会。TRB会议几乎涵盖了交通领域…