新C++(7):多态那些事儿_下

news2024/12/25 0:30:20

"当人类悬浮到腐朽,有谁愿追随彗星漂流哦~"

一、多态原理

(1)虚函数表指针(虚表指针)

紧接上一篇sizeof(Base)这一小段说起。

class Base1
{
public:
    void func(){}
private:
    int _a;
};

class Base2
{
public:
    virtual void func() {}
private:
    int _a;
};

我们知道,两个Base虽然在成员上相差无几,但是因为虚函数的存在,Base2一定是 > Base1的。

前篇说了这里的原因在于,一旦有虚函数,类里面就会自动生成一个虚函数指针。那么什么是虚函数指针呢?

虚函数表指针是能够实现动态多态的根本原因,一个有虚函数的类里,会多出一个_ vfptr即虚函数表指针(虚表指针),它指向的 是一个函数指针数组_vtable,而这个指针数组,存储的是类里虚函数的地址。

(2)虚函数表

那么这个虚函数表在哪里呢?这个虚函数表有几份呢?

在图示中,我们清晰地看到,相同的类它的虚表指针是固定的,即它们共享一份虚函数表。不同的类,有不同的虚表指针。

但这些表存储在哪个地方呢?它们是在编译时生成还是在构造时生成呢?

原来虚函数表是存储在静态区、代码段区域。

虚函数表存储在常量、代码区域附近
虚函数表在编译时就已经存在。
虚函数表指针在构造函数初始化列表出初始化。

(3)重写覆盖

为什么说子类对基类虚函数的定义叫做重写,这个行为又被叫做覆盖呢?

class Base
{
public:
    virtual void Func1()
    {
        cout << "Base::Func1()" << endl;
    }

    virtual void Func2()
    {
        cout << "Base::Func2()" << endl;
    }
private:
    int _a;
};

class Derive : public Base
{
public:
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
private:
    int _d = 2;
};

这份份代码只对基类的func1虚函数进行了重写。

因此,一定程度上,就是可以这么理解。建立_vftble子类虚函数表的时候,是把基类的虚函数表拷贝一份过去,完成重写的部分,则"覆盖"式地填写进子类的虚函数表中。重写是语法的叫法,覆盖是原理层的叫法。

(4)静态绑定vs动态绑定

从概念上这两个定义很简单。

静态绑定又称为前期绑定(早绑定), 在程序编译期间确定了程序的行为 ,也称为静态多态。

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

我们从反汇编的角度来看看呢,

从指令的复杂程度,也就知道它们之间的差异其实挺大的。

对于静态的动态而言,如:函数重载。函数实现在代码区,编译期间就可以找到函数的地址,当执行到该函数时,直接call该保存的 地址即可(如图所示)。

但是对于动态的多态,如:虚函数重写,虽然虚函数表在代码段\静态区,但是你不知道调用子类的虚函数还是父类的虚函数。因为起决定的是,父类指针、引用接收的对象,由此当父类指针、引用接收到对象时,会根据该对象去虚函数表中找到适合的虚函数,再进行调用,从而实现动态的多态。

(5)子类虚函数

子类也定义一个虚函数,那是否会进入虚函数表呢?

我们写一个打印虚函数表的函数;

void PrintVFTable(VFPtr vft[])
{
    for (int i = 0; vft[i] != nullptr; ++i)
    {
        printf("[%d]:%p->", i, vft[i]);
        //我们拿到了函数的地址 就可以去调用函数
        vft[i]();
    }
    cout << endl;
}

(6)经典题目

我们以一道面试题来开启这一小段。

class A{
public:
    A(const char *s) { cout << s << endl; }
    ~A(){}
};
class B :virtual public A
{
public:
    B(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
    C(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class D :public C, public B
{
public:
    D(const char *s1,const char *s2,const char *s3,const char *s4) : B(s1, s2), C(s1, s3),A(s1)
    {
        cout << s4 << endl;
    }
};

这段打印什么?

初始化列表初始化数据的顺序:
①声明顺序
②继承顺序

为什么这里需要D去显式调用A的构造?

因为在B、C中A的部分都是一个。因此,让B、C其中哪一个去构造都不太合适,因为两人都合适,并且A的部分是公共的。所以,这个任务就交由D一定要去显式调用A的构造。完成对那部分的初始化。

此时我们把虚继承去掉,此时也就是菱形继承了:

class A{
public:
    A(const char *s) { cout << s << endl; }
    ~A(){}
};
class B :public A
{
public:
    B(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class C :public A
{
public:
    C(const char *s1,const char*s2) :A(s1) { cout << s2 << endl; }
};
class D :public C, public B
{
public:
    D(const char *s1,const char *s2,const char *s3,const char *s4) : B(s1, s2), C(s1, s3),A(s1)
    {
        cout << s4 << endl;
    }
};

这种题很考人,如果对多态与继承学得不是很扎实时,总不免会踩坑。

(7)为什么多态的条件要求是父类的指针或引用?

这也是为什么,多态的条件是基类的指针或者引用,而非对象。

二、多继承关系里的多态

需要注意的是,虚函数表与虚继承表无 任何关系,虽然它们都使用了一个 同样的关键字"virtual"。

(1)单继承中的虚函数表

从观察窗口看,我们看不见func3,这是vs调试窗口做了特殊处理进行了隐藏。那我们如何看到子类的func3虚函数呢? 我们就只好用之前写好的打印函数。

(2)多继承中的虚函数表

此外,多继承的派生类为重写的虚函数,会放在第一个继承的基类部分的虚函数表中

菱形继承、菱形虚拟继承产出的虚函数表更加地吓人,本节不会对此做过多赘述。你要设计菱形继承又要以此设计多态出来,只能奉劝你 "耗子尾汁"。

三、经典面试问答

(1)重载、重写(覆盖)、重定义(隐藏)

(2)inline函数可以是虚函数吗?

inline函数就是在函数调用处给出展开,但是我们虚函数是需要写入虚函数表的。因此不适宜展开。

虽然这个在语法上来说编译器不会报错,但是一旦构成多态,那么内联就没什么用了。毕竟inline只是给编译器提"建议"。而如果是普通调用,那么内联展开也是行得通的。

(3)静态成员函数可以是虚函数吗

我们写出来编译器就立马报错。静态成员函数最显著的一个特征时,可以不需要类对象的创建,就可以调用的函数,也就是该函数没有this指针。没this指针你怎么访问虚函数表,调用虚表指针呢?

(4)构造函数可以是虚函数?

在前面也说过,虚函数表是在编译期间就已经存在了。但是虚表指针是在构造函数的初始化列表中完成初始化的。虚表指针都没有,你怎么让构造函数是虚函数。

(5)析构函数可以是虚函数?

父类指针、引用 子类对象时,如果父类的虚函数没有完成重写,那么它就只会去调用它自己的析构函数,而不会去调用子类的析构函数。只有将父类的析构函数变为虚函数,才能正确地析构子类对象。

(6)对象访问普通函数快还是虚函数更快?

如果是普通调用。两个一样的块。难道声明了virtual的虚函数,每次调用都会去查找虚表?

只要你不构成多态的条件,对类里虚函数的调用跟普通调用没什么区别。当然,构成多态从反汇编的都知道它要去虚表里面查找合适的虚函数,肯定对效率有一定的影响。

总结:

①类里一旦有虚函数,就会自动生成虚函数表指针。

②虚函数表指针在初始化列表初始化,虚函数表是在编译阶段就生成的,一般情况

下存在代码段(常量区)的。

③多态分为静态的多态和动态的多态。一个是在编译期间确定的,一个是运行期间才能确定的。

本篇到此结束,感谢你的阅读

祝你好运,向阳而生~

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

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

相关文章

【我刚毕业,学习Java开发工程师能学会吗?没有基础?】

对于Java专业来说&#xff0c;学历还是有一定的要求。一般都是本科学历&#xff0c;至少也有个大专&#xff0c;其次就是年龄越年轻越好。现在转行Java的年轻人很多&#xff0c;学历这方面越高越有竞争力一些&#xff0c;尤其是在后期的职业晋升阶段。如果想走管理路线&#xf…

耗时一周整体,这4款黑科技电脑软件,功能强大到离谱

闲话少说&#xff0c;直上狠货。 1、有道云笔记 有道云这是一款国民级的文稿编辑器&#xff0c;俗话说得好&#xff0c;好记性不如烂笔头&#xff0c;强大实用的笔记软件&#xff0c;能让你的工作与学习事半功倍。5大文稿类型&#xff0c;让记录得心应手&#xff0c;随时进行创…

第十四章 集合(Set)

一、Set 接口&#xff08;P518&#xff09; 1. Set接口基本介绍 &#xff08;1&#xff09;无序&#xff08;添加和取出的顺序不一致&#xff09;&#xff0c;没有索引 &#xff08;2&#xff09;不允许重复元素&#xff0c;所以最多包含一个 null 2. Set接口的常用方法 和 …

SpringBoot中MQ使用

本文记录学习在Spring Boot中使用MQ。一 什么是MQMQ全称&#xff08;Message Queue&#xff09;又名消息队列&#xff0c;是一种异步通讯的中间件。它的作用类似于邮局&#xff0c;发信人(生产者)只需要将信(消息)交给邮局&#xff0c;然后由邮局再将信(消息)发送给具体的接收者…

博途1200PLC/1500PLCMODBUS从站通信应用

博途PLC作为MODBUS主站通信请参看下面的文章链接: 博途PLC 1200/1500PLC MODBUS-RTU通讯优化(状态机编程)_博途plc modbus-rtu通信优化_RXXW_Dor的博客-CSDN博客博途PLC 1200/1500PLC MODBUS-RTU通讯_RXXW_Dor的博客-CSDN博客_博图modbus通讯1、1200PLC的modbus通讯,可以参看…

HTTP协议(1)

1)HTTP协议是倾向于相遇业务层次上面的一种协议&#xff0c;传输层协议主要考虑的是端对端之间的一个传输过程&#xff0c;TCP重点进行关注的是可靠传输&#xff1b;咱们的HTTP/1&#xff0c;HTTP/2是基于TCP的&#xff0c;但是咱们的HTTP/3是基于UDP的&#xff0c;但是当下的互…

实战动态代理

代理模式介绍代理模式有点像老大和小弟&#xff0c;也有点像分销商。主要解决的是问题是为某些资源的访问、对象的类的易用操作上提供方便使用的代理服务。而这种设计思想的模式经常会出现在我们的系统中&#xff0c;或者你用到过的组件中&#xff0c;它们都提供给你一种非常简…

【CVPR 2018】PU-Net: Point Cloud Upsampling Network

文章目录PU-Net: Point Cloud Upsampling Network网络架构训练数据生成点特征嵌入Feature ExpansionCfoordinate Reconstruction端到端训练Joint Loss FunctionPU-Net: Point Cloud Upsampling Network 网络架构 PU-Net有四个组件&#xff1a;patch extraction, point feature…

「自定义类型」C语言中的构造数据类型如结构,联合,枚举

​​​​​​​&#x1f680;&#x1f680;&#x1f680;大家觉不错的话&#xff0c;就恳求大家点点关注&#xff0c;点点小爱心&#xff0c;指点指点&#x1f680;&#x1f680;&#x1f680;目录 &#x1f430;结构 &#x1f3e1; 前言 &#x1f338;数据类型的定义 &…

SpringBoot动态导出word文档(完美实整教程 复制即可使用,不能实现你找我)

背景 最近有一个需求是需要动态导出合同、订单等信息&#xff0c;导出一个word文档供客户进行下载查看。 需要导出的word文件&#xff0c;主要可以分为两种类型。 导出固定内容和图片的word文档导出表格内容不固定的word文档 经过对比工具&#xff0c;我实践过两种实现方式…

一文细说引导内存分配器

一、引导内存分配器 1.引导内存分配器的作用 因为内核里面有很多内存结构体&#xff0c;不可能在静态编译阶段就静态初始化所有的这些内存结构体。另外&#xff0c;在系统启动过程中&#xff0c;系统启动后的物理内存分配器本身也需要初始化&#xff0c;如伙伴分配器&#xff…

OD笔试题-空汽水瓶可以换汽水

/*** 某商店规定&#xff1a;三个空汽水瓶可以换一瓶汽水&#xff0c;允许向老板借空汽水瓶&#xff08;但是必须要归还&#xff09;。* 小张手上有n个空汽水瓶&#xff0c;她想知道自己最多可以喝到多少瓶汽水。* 数据范围&#xff1a;输入的正整数满足 1≤n≤100* <p>*…

springboot 多环境配置yml

创建多个配置文件 创建文件时注意&#xff0c;一定是 application-文件名称.yml 这种格式 application.yml #主配置文件 application-dev.yml #开发环境的配置 application-prod.yml #生产环境的配置application-prod.yml # 生产环境端口为90 server:port: 90applica…

Python实现将一段话txt生成字幕srt文件

Python实现将一段话txt生成字幕srt文件 作者&#xff1a;虚坏叔叔 博客&#xff1a;https://xuhss.com 早餐店不会开到晚上&#xff0c;想吃的人早就来了&#xff01;&#x1f604; 一、为什么要将txt转换成字幕 1.1方便到剪辑软件剪辑 有时获取到一段文本&#xff0c;想要直…

点分治学习笔记

有时候我们会碰到一些树上的路径问题&#xff0c;如果需要处理的规模很大的话&#xff0c;这时候点分治是一个很好的工具&#xff0c;往往可以在O(nlogn)的复杂度内完成操作&#xff0c;一般用于离线处理问题 前置芝士 树的重心&#xff1a;最大子树的值最小的点叫做重心。 …

【手撕面试题】HTML+CSS(高频知识点二)

目录 面试官&#xff1a;页面导入样式时&#xff0c;使用 link 和 import 有什么区别&#xff1f; 面试官&#xff1a;简要说说 title与h1的区别、b与strong的区别、i与em的区别&#xff1f; 面试官&#xff1a;img标签的title和alt有什么区别&#xff1f; 面试官&#xff…

给特别规则FeignClient增加统一的RequestInterceptor

需求背景&#xff1a; 在微服务横行天下的今天&#xff0c;Spring Cloud OpenFeign 早就成了我们服务间调度的主要远程调用手段。 在Feign进行远程调度的时候&#xff0c;难免会做一些心跳&#xff0c;权限的校验&#xff0c;来保证调度过程的相对安全。 但对于外部系统来说…

Unity 之 Addressable可寻址系统 -- 资源远程加载 | 资源预下载 -- 进阶(三)

可寻址系统远程加载 -- 资源预下载 -- 进阶&#xff08;三&#xff09;一&#xff0c;Unity 云资源分发 -- 使用介绍1.1 CCD 的介绍1.2 后台准备工作二&#xff0c;CDD的使用2.1 CCD可视化界面的使用2.2 CDD命令行界面使用2.2.1 准备工作2.2.2 CLI 用法三&#xff0c;AA CCD资…

Java中的快速排序

快速排序递归版本挖坑法Hoare法优化非递归相信即使大家并不知道快速排序究竟是个啥,但也一定听说过快排,今天我来给兄弟们讲讲快速排序!递归版本 快速排序的思想就是找基准,就比如我们以数组中的第一个数字12为基准,我们从最后往前面找,如果找到一个比12小的数字就用它覆盖12,但…

Linux—InstallOS-RedHat9.1

下载https://developers.redhat.com/products/rhel/download 需注册账号。安装正常安装就行。安装注意事项&#xff1a;(1)Software SelectionCentOS的摘录过来&#xff0c;通用。最小安装&#xff08;Minimal Install&#xff09;这个选项只提供运行CentOS 的基本软件包。最小…