当前位置: 首页 > news >正文

【C++技能树】多态解析

在这里插入图片描述
Halo,这里是Ppeua。平时主要更新C++,数据结构算法,Linux与ROS…感兴趣就关注我bua!

文章目录

  • 0.多态的概念
    • 0.1 多态的定义
  • 1. 重写
  • 2.Final与Override
  • 3.抽象类
  • 4.多态中的内存分布.
    • 4.1虚表存在哪里?
  • 5.多态调用原理
    • 5.1 动态绑定与静态绑定
  • 6.继承中的虚函数表
    • 6.1单继承中的虚函数表
    • 6.2多继承中的虚函数表

在这里插入图片描述

0.多态的概念

试想下这个场景,不同身份的人去买票,相同的函数会执行不同的行为.这就需要多态去完成

多态:顾名思义一个类中函数的多种状态.

先来看看下面这个例子:

class Person{
public:virtual void BuyTicket(){cout<<"买票全价"<<endl;}virtual ~Person(){cout<<"~person()"<<endl;}
};
class Student:virtual public Person
{
public:virtual void BuyTicket(){cout<<"买票半价"<<endl;}virtual ~Student(){cout<<"~student()"<<endl;}
};
void buyticket(Person *p1)
{p1->BuyTicket();   
}

当在buyticket中传入student类型的地址与传入Person类型的地址会执行不同的行为.

传入Person:会输出买票全价

传入Student:会输出买票半价

这就是多态的具体行为.会根据传入对象的不同执行不同的行为.

0.1 多态的定义

在基类需要重写的函数前加上virtual.

在派生类中想要达到重写的函数前也加上virtual.(可加可不加),之后保持与基类中函数相同的返回值,函数名,参数列表即可完成重写.

在调用时需要通过基类的指针或者引用来调用(将想要调用的类赋值到父类的指针或者引用,调用相同的函数.即可完成多态)

有一个例外:返回值可不一定需要相同,可以为父类或子类对象的指针或引用(要同为指针,或者同为引用),称为协变

所以多态就是:不同对象传递,调用不同的函数.多态调用看指向的对象.具体是什么内容 ,而不是看当前类型.

48410c494a8f224dac0bd406a27a6dd

1. 重写

析构函数无论加不加virtual都完成重写

class Person{
public:virtual Person& BuyTicket(){cout<<"买票全价"<<endl;}~Person(){cout<<"~person()"<<endl;}
};
class Student:virtual public Person
{
public:virtual Student& BuyTicket() {cout<<"买票半价"<<endl;}~Student(){cout<<"~student()"<<endl;}
};

这是因为编译器对析构函数进行了处理,在编译阶段都重命名为了Destructor,所以他们为同名函数.

为什么要进行这么处理呢?

当用父类指针去调用子类对象时,使用delete时,若无多态则只会把父类的成员属性删除.并不会删除子类的.

Person* p=new Person;
delete p;
p=new Student;
//析构错误 不多态则没有调到派生类的析构
delete p;  //p->destructor+operator delete p

2.Final与Override

不想让一个函数被重写时可以在其后加上final,此时会从语法来检查该函数是否被重写.

检查一个函数是否满足重写的条件可以在其后加上override,用来检查是否满足重写的条件

class Person{
public:// virtual void BuyTicket() final// {//     cout<<"买票全价"<<endl;// }virtual Person& BuyTicket(){cout<<"买票全价"<<endl;}~Person(){cout<<"~person()"<<endl;}
};
class Student:virtual public Person
{
public:virtual Student& BuyTicket() override{cout<<"买票半价"<<endl;}~Student(){cout<<"~student()"<<endl;}
};

3.抽象类

多态也叫接口继承,也就是只继承基类的函数接口,内容自己重新写.普通函数的继承是一种实现继承

那么我们也可以设计一个只提供接口的类.那么就是抽象类

在一个虚函数中最后加上=0 则成为 纯虚函数,包含纯虚函数的类则称为抽象类.

class Car
{
public:virtual void Drive() = 0;
};

抽象类不能用来实例化对象,只能用来当作基类提供接口

4.多态中的内存分布.

根据上面的介绍,我们对多态的使用已经有了初步的了解.

即在重定义的基础上加上一个virtual 以及满足三同(同名 同参数 同返回值)

那么在内存中多态是如何存储的?

class Person{
public:virtual void fun(){cout << "hello person";}
};
class Student :public Person{virtual void fun()override {cout << "hello student";}virtual void fun3() {cout << "hello student3";}
};
int main()
{Person* p1;Student s1;p1 = &s1;p1->fun();
}

这是一个多态调用的例子.我们通过vs2022来看看其在内存中是如何存储的.

image-20230903123709063

可以看到,其有一个vfptr(virtual fun ptr)虚表指针.其和我们之前继承中的虚基表有点类似.当中存储了一个完成重写的函数.fun

我们在内存中输入这个地址,可以发现其存储了两段地址.

image-20230903124449931

  1. 第一个为完成重写的fun的地址

  2. 第二个为自己的虚函数fun3的地址,但这在上图中的结构模型中并没有被看到.所以结构模型有时并不是完全可信的

    所以,自己的虚函数会直接放在第一个虚表的最后.

  3. 第三个表示虚表的结束(在vs2022中是这样表示的)

    综上可以看出,在实例化的时候,会将基类的虚表复制一份到派生类当中,若有重写的函数,则用重写完的函数地址去覆盖虚表中原函数的地址.所以在原理层中:也叫做 覆盖.

4.1虚表存在哪里?

虚表是存储在 栈区 堆区 静态区 还是常量区呢?

我们可以通过以下这个函数来验证

int main()
{Person p1;Student s1;int a = 0;printf("栈:%p", &a);cout << endl;int* b = new int[10];printf("堆:%p", b);cout << endl;const char* c = "hello world";printf("常量区%p", c);cout << endl;static int d = 10;printf("静态区%p", &d);cout << endl;printf("虚表1:%p", *((int*)&p1));cout << endl;printf("虚表2:%p", *((int*)&s1));cout << endl;
}

为什么这样区能取到虚表地址呢?通过取对应对象的地址,之后在进行强制转换为int,此时访问宽度为前四个字节(因为一个指针大小为四个字节).之后在对这个指针进行解引用就为虚表的地址*

我们运行这段代码,就可以发现,虚表存储在常量区

image-20230903125353911

5.多态调用原理

上文我们已经知道了,如何去调用多态.以及虚表存储的模型.下面是一个多态调用例子:

class Person{
public:virtual void fun(){cout << "hello person";}
};
class Student :public Person{virtual void fun()override {cout << "hello student";}
};
int main()
{Person* p1;Student s1;p1 = &s1;p1->fun();
}

根据前面所学可以看出,当我调用p1->fun()时,由于p1里面存的是s1的地址,所以这里就会取到s1的虚表中f1的地址,完成多态调用

所以多态是在运行的时候动态确定需要执行的函数

5.1 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态.比如:函数重载

  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

6.继承中的虚函数表

下面将从两个部分:多继承与单继承中的虚函数表来划分

6.1单继承中的虚函数表

在上文中可以知道,自己定义的虚函数,还没被重写之前是不会被放进虚表的.所以我们要研究这个存储就必须手动访问函数地址.

我们先重命名下函数的指针,方便后期调用:

typedef void(*FUNC_PTR)();

注意:这里的函数返回值为void,参数为空.将其重命名为FUNC_PTR

所以我们通过这样的方式去强行访问类中虚表存储的函数

class Person{
public:virtual void fun1(){cout << "Person::fun1";}virtual void fun2(){cout << "Person::fun2";}virtual void fun3(){cout << "Person::fun3" ;}
};
class Student :public Person{virtual void fun1()override {cout << "Student::fun1()";}virtual void fun3(){cout << "Student::fun3()";}virtual void fun4(){cout << "Student::fun4()";}
};
typedef void(*FUNC_PTR)();
void printvft(FUNC_PTR* table)
{for (int i = 0; table[i] != nullptr; i++){printf("%d->%p", i, table[i]);FUNC_PTR f=table[i];f();cout << endl;} 
}
int main()
{Student s1;Person p1;cout << "person:" << endl;int vft = *((int*)&p1);printvft((FUNC_PTR*)vft);cout << "student:" << endl;vft = *((int*)&s1);printvft((FUNC_PTR*)vft);
}

其中vft的赋值原理是,我们知道虚表地址是存在对象中的前四个位,所以我们用int*去取.之后解引用就是他的地址,存入到int当中,此时的 vft就是存储的地址,如何用这个地址去访问其中的函数即可.上面我们知道,在虚表的结束位置,会设置为0.所以我们可以以此来判断

运行结果:

image-20230905161534648

在上面的函数中,只有fun1和fun3被Student完成了重写.

所以我们可以得出一个结论:

在单继承模型中,派生类会复制一份基类的虚表到自己中,若有重写函数,则用新的重写函数地址覆盖原函数地址 而不是直接对基类虚表直接进行修改,自己新的虚函数则跟在后面

6.2多继承中的虚函数表

class Person{
public:virtual void fun1(){cout << "Person::fun1";}virtual void fun2(){cout << "Person::fun2";}virtual void fun3(){cout << "Person::fun3" ;}
};
class People {
public:virtual void fun1(){cout << "People::fun1";}virtual void fun2(){cout << "People::fun2";}virtual void fun3(){cout << "People::fun3";}
};class Student :public Person,public People
{virtual void fun1() {cout << "Student::fun1()";}virtual void fun4(){cout << "Student::fun4()";}
};
typedef void(*FUNC_PTR)();
void printvft(FUNC_PTR* table)
{for (int i = 0; table[i] != nullptr; i++){printf("%d->%p", i, table[i]);FUNC_PTR f=table[i];f();cout << endl;} 
}
int main()
{Student s1;Person p1;People peo;cout << "person:" << endl;int vft = *((int*)&p1);printvft((FUNC_PTR*)vft);cout << "people:" << endl;vft = *((int*)&peo);printvft((FUNC_PTR*)vft);cout << "student:" << endl;vft = *((int*)&s1);printvft((FUNC_PTR*)vft);
}

运行结果:

image-20230905163221242

我们在内存中看一下模型:

student中的person:

image-20230905163302648

student中的people:

image-20230905163312561

我们可以很容易发现,student是对person进行了重写,并把自己的未重写虚函数放在了第一张虚表的最后.

也就是:多继承模型中,派生类会在第一个继承的基类上进行重写,并且将自己未重写的虚函数放在其表尾
image-20230905164632777

相关文章:

【C++技能树】多态解析

Halo&#xff0c;这里是Ppeua。平时主要更新C&#xff0c;数据结构算法&#xff0c;Linux与ROS…感兴趣就关注我bua&#xff01; 文章目录 0.多态的概念0.1 多态的定义 1. 重写2.Final与Override3.抽象类4.多态中的内存分布.4.1虚表存在哪里? 5.多态调用原理5.1 动态绑定与静…...

【爬虫笔记】Python爬虫简单运用爬取代理IP

一、前言 近些年来&#xff0c;网络上的爬虫越来越多&#xff0c;很多网站都针对爬虫进行了限制&#xff0c;封禁了一些不规则的请求。为了实现正常的网络爬虫任务&#xff0c;爬虫常用代理IP来隐藏自己的真实IP&#xff0c;避免被服务器封禁。本文将介绍如何使用Python爬虫来…...

IP协议-NAT机制(理解网络结构的关键要点)

前言 我们现在使用得最多的IP协议版本是IPv4&#xff0c;IPv4是4个字节&#xff0c;32位&#xff0c;也就是说我们的IP地址最多就只有2^32&#xff08;42亿&#xff09;个&#xff0c;在日常生活中&#xff0c;我们需要联网的设备都需要有IP地址才能进行通讯&#xff0c;很明显…...

Python UI自动化 —— 关键字+excel表格数据驱动

步骤&#xff1a; 1. 对selenium进行二次封装&#xff0c;创建关键字的库 2. 准备一个表格文件来写入所有测试用例步骤 3. 对表格内容进行读取&#xff0c;使用映射关系来对用例进行调用执行 4. 执行用例 1. 对selenium进行二次封装&#xff0c;创建关键字的库 from time imp…...

AI:06-基于OpenCV的二维码识别技术的研究

二维码作为一种广泛应用于信息传递和识别的技术,具有识别速度快、容错率高等优点。本文探讨如何利用OpenCV库实现二维码的快速、准确识别,通过多处代码实例展示技术深度。 二维码作为一种矩阵型的条码,广泛应用于各个领域,如商品追溯、移动支付、活动签到等。二维码的快速…...

Spring MVC Http Event Stream

什么是 Http Event Stream Event Stream 技术是一种实现服务器推送事件的方法&#xff0c;它通过在一个持续的 HTTP 连接上发送事件流来实现推送。具体来说&#xff0c;服务器发送一些事件到客户端&#xff0c;并将这些事件封装成一些指定格式的文本流。客户端通过监听这个流&…...

2023年亲测有效----树莓派启动时自动邮件上报ip

2023年亲测 树莓派启动时自动邮件上报ip 首先开启qq邮箱smtp服务shell文件内容启动自动执行python文件注意事项 首先开启qq邮箱smtp服务 然后点击开启就会有授权码 shell文件内容 在自己的shell里&#xff0c;运行echo $PATH&#xff0c;把内容覆盖下面的path。 功能 作用就…...

Direct3D颜色

在Direct3D中颜色用RGB三元组来表示&#xff0c;RGB数据可用俩种不同的结构来保存&#xff0c;第一种是D3DCOLOR&#xff0c;它实际上与DWORD类型完全相同&#xff0c;共有32位&#xff0c;D3DCOLOR类型种的各位被分成四个8位项&#xff0c;每项存储了一种颜色分量的亮度值。 由…...

LLM - 大模型速递 Baichuan2 快速入门

目录​​​​​​​ 一.引言 二.模型探索 1.模型下载 2.模型结构 ◆ Baichuan-1-13B 结构 ◆ Baichuan-2-13B 结构 3.模型测试 ◆ Baichuan-2-13B Chat 推理 ◆ Baichuan-2-13B 显存 4.模型量化 ◆ 在线量化 ◆ 离线量化 ◆ 量化效果 5.模型迁移 三.模型微调 …...

DB2和MYSQL的LOAD原理和比较测试

DB2 load的过程&#xff1a; &#xff08;1&#xff09;、装入阶段 装入阶段将源数据解析成物理数据页的格式&#xff0c;直接装入到数据页中。必要时还收集索引键和表统计信息。 &#xff08;2&#xff09;、构建索引阶段 根据在装入阶段收集的索引键创建表索引。 &#xff08…...

redisson常用api

redisson提供了很多对象类型的api&#xff0c;下面介绍下一些常用的对象api。 RBucket 可操作任何对象的api&#xff0c;前提是要确定好泛型&#xff0c;方法比较少。大小限制为512Mb。 RBucket<AnyObject> bucket redisson.getBucket("anyObject");bucket…...

MySQL——数据库以及数据表的创建

创建数据库 回到刚才创建数据库的问题&#xff0c;我们在创建数据库的时候可以通过添加一个参数&#xff0c;这个参数的意义在于当我们创建的数据库已经存在的时候则不会创建&#xff0c;也不会报错&#xff0c;如果不使用这个参数&#xff0c;则我们在重复创建一个已经存在的…...

智能配电房管理

智能配电房管理依托电易云-智慧电力物联网&#xff0c;利用先进技术手段&#xff0c;对配电房进行智能化、自动化的管理&#xff0c;以提高配电房的安全性、可靠性和效率。 智能配电房管理包括&#xff1a; 1.实时监测&#xff1a;通过传感器、监控设备等手段&#xff0c;对配…...

php如何解决高并发的问题?

在PHP中解决高并发问题可以采取以下几种策略&#xff1a; 使用缓存&#xff1a;通过使用缓存技术&#xff0c;可以将经常访问的数据存储在内存中&#xff0c;减轻数据库或其他资源的压力。常见的缓存技术包括Memcached和Redis。PHP提供了与这些缓存服务器进行交互的扩展和库。 …...

Linux操作系统

线程竞争 那么初始化一个整型为 0&#xff0c;使用一万个线程&#xff0c;每个线程都对该整型加 1&#xff0c;最后结果不一定会是 10000。这是因为整型变量的赋值操作不是原子操作&#xff0c;也就是说它不是一个不可分割的操作&#xff0c;而是由多条指令组成的。例如&#…...

华为OD:VLAN资源池

题目描述&#xff1a; VLANO 是一种对局域网设备进行逻辑划分的技术&#xff0c;为了标识不同的VLAN&#xff0c;引入VLAN ID(1-4094之间的整数)的概念。 定义一个VLAN ID的资源池&#xff08;下称VLAN资源池&#xff09;&#xff0c;资源池中连续的VLAN用开始VLAN-结束VLAN表…...

大学大创项目:手机室内AR导航APP项目思路

文章目录 一、最初的项目思路二、建图和定位分离的项目思路1、建图2、定位 个人见解&#xff0c;如有错误&#xff0c;请多包涵 一、最初的项目思路 在大创项目的开始&#xff0c;将手机确定为应用设备&#xff0c;传感器确定为相机。 由于知识储备的原因&#xff0c;在头一次…...

OpenSSL加解密算法使用方法

下面简单记录一下 Linux上openssl命令的使用方法&#xff0c;包括 OpenSSL中加解密算法的使用方法和性能测试方法&#xff0c;以便让新手朋友们能快速用起来。持续更新中 … sm3算法 $ openssl sm3 /tmp/1.txt SM3(/tmp/1.txt) baafadbe43559b7043abd1682a4e12be05692cae175…...

Excel VSTO开发10 -自定义任务面板

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 10 自定义任务面板 自定义任务面板&#xff08;有些地方称为侧边面板&#xff09;即CustomTaskPane&#xff0c;这个类在Microsoft…...

百度智能云千帆大模型丨未来人手必备的代码助手

文章目录 1. 前言2. 千帆大模型平台3. 十分友好的功能4. comate代码助手5. 总结 1. 前言 我之前给大家推荐过Poe这个网站&#xff0c;它用的人比较少&#xff0c;但一旦接触后会发现它其实挺强大的。 因为它是一个可以同时支持好几个大模型的在线聚合平台。常用的GPT4&#x…...

springboot 百货中心供应链管理系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;百货中心供应链管理系统被用户普遍使用&#xff0c;为方…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)

漏洞概览 漏洞名称&#xff1a;Apache Flink REST API 任意文件读取漏洞CVE编号&#xff1a;CVE-2020-17519CVSS评分&#xff1a;7.5影响版本&#xff1a;Apache Flink 1.11.0、1.11.1、1.11.2修复版本&#xff1a;≥ 1.11.3 或 ≥ 1.12.0漏洞类型&#xff1a;路径遍历&#x…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...

深度剖析 DeepSeek 开源模型部署与应用:策略、权衡与未来走向

在人工智能技术呈指数级发展的当下&#xff0c;大模型已然成为推动各行业变革的核心驱动力。DeepSeek 开源模型以其卓越的性能和灵活的开源特性&#xff0c;吸引了众多企业与开发者的目光。如何高效且合理地部署与运用 DeepSeek 模型&#xff0c;成为释放其巨大潜力的关键所在&…...

API网关Kong的鉴权与限流:高并发场景下的核心实践

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 引言 在微服务架构中&#xff0c;API网关承担着流量调度、安全防护和协议转换的核心职责。作为云原生时代的代表性网关&#xff0c;Kong凭借其插件化架构…...

Python常用模块:time、os、shutil与flask初探

一、Flask初探 & PyCharm终端配置 目的: 快速搭建小型Web服务器以提供数据。 工具: 第三方Web框架 Flask (需 pip install flask 安装)。 安装 Flask: 建议: 使用 PyCharm 内置的 Terminal (模拟命令行) 进行安装,避免频繁切换。 PyCharm Terminal 配置建议: 打开 Py…...

何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡

何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡 背景 我们以建设星云智控官网来做AI编程实践&#xff0c;很多人以为AI已经强大到不需要程序员了&#xff0c;其实不是&#xff0c;AI更加需要程序员&#xff0c;普通人…...

LTR-381RGB-01RGB+环境光检测应用场景及客户类型主要有哪些?

RGB环境光检测 功能&#xff0c;在应用场景及客户类型&#xff1a; 1. 可应用的儿童玩具类型 (1) 智能互动玩具 功能&#xff1a;通过检测环境光或物体颜色触发互动&#xff08;如颜色识别积木、光感音乐盒&#xff09;。 客户参考&#xff1a; LEGO&#xff08;乐高&#x…...

ubuntu系统 | docker+dify+ollama+deepseek搭建本地应用

1、docker 介绍与安装 docker安装:1、Ubuntu系统安装docker_ubuntu docker run-CSDN博客 docker介绍及镜像源配置:2、ubuntu系统docker介绍及镜像源和仓库配置-CSDN博客 docker常用命令:3、ubuntu系统docker常用命令-CSDN博客 docker compose安装:4、docker compose-CS…...

【Unity】R3 CSharp 响应式编程 - 使用篇(二)

一、通用的事件监听用法 using System;using R3;using UnityEngine;namespace Aladdin.Standard.Observable.Common{public class CommonObservable : MonoBehaviour{// 默认会调用1次public SerializableReactiveProperty<int> serializableReactiveProperty;…...