【C++进阶】继承、多态的详解(多态篇)

【C++进阶】继承、多态的详解(多态篇)

目录

  • 【C++进阶】继承、多态的详解(多态篇)
      • 多态的概念
      • 多态的定义及实现
          • 多态的构成条件(重点)
          • 虚函数
          • 虚函数的重写(覆盖、一种接口继承)
          • C++11 override 和 final
          • 重载、覆盖(重写)、隐藏(重定义)的对比
      • 抽象类
          • 概念
          • 动态绑定与静态绑定
      • 单继承和多继承关系的虚函数表
          • 菱形继承、菱形虚拟继承
      • 常见的题目
      • 附加问题

作者:爱写代码的刚子
时间:2023.8.16
前言:本篇博客主要介绍C++中多态有关的知识,是C++中的一大难点,刚子将带你深入C++多态的知识。(该博客涉及到的代码是在x86的环境下,如果是在x86_64环境下指针的大小可能需要变成8bytes)

多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。

多态的定义及实现

多态的构成条件(重点)
    • 必须通过基类的指针或者引用调用虚函数
    • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(派生类的重写虚函数可以不加virtual)

示例:
在这里插入图片描述

  • 多态中不同对象传过去调用不同的函数,多态调用(指针或引用)看的指向的对象,普通调用看当前的类型

指定调用不满足多态:
在这里插入图片描述

虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数

虚函数的重写(覆盖、一种接口继承)

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

被重写的函数必须是虚函数

重写的条件:虚函数 + 三同,但是有些例外

虚函数重写的两个例外:

  1. 协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(不常用)
    协变,返回值可以不同,但是要求返回值必须是父子关系指针和引用(同时为指针或同时为引用)

在这里插入图片描述

  1. 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的 析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规 则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处 理成destructor。

【问题】:析构函数加virtual,是不是虚函数重写?
是,因为类析构函数都被处理成destructor这个统一的名字,因为要让他们构成重写。

在这里插入图片描述

【问题】:将析构函数变为虚函数和普通的析构函数有什么区别呢?
答:在特殊场景下会不同:
在这里插入图片描述
上面的场景很可能会造成内存泄漏,这里我们期待p->destructor()是一个多态调用,而不是普通调用。(多态调用看指向的内容,普通调用看对象的类型),上图中的p指针是Person类型且普通调用。要想实现多态调用需要加上virtual,构成重写

C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序 写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

    • final:修饰虚函数,表示该虚函数不能再被继承和重写
    • override:检查派生类虚函数是否重写了基类某个函数,如果没有重写编译报错。
      在这里插入图片描述
      在这里插入图片描述
      (final和override关键字顺序可以改变)

【附】:如果一个类不想被继承有几种方法?

  • C++98:基类构造函数私有化,通过public静态成员函数执行构造函数。(如果不是静态成员函数不创建类就不能调用函数,类似先有鸡还是先有蛋的问题)
  • 还可以将析构函数私有化,再创建一个静态的destructor成员函数
  • C++11:类名后面加final关键字
重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

抽象类

概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就 是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重 写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的 叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会 放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基 类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外 对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,Linux g++下呢?
动态绑定与静态绑定
  1. 静态绑定又称为前期绑定(早绑定,编译时),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定,运行时),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

  • 单继承中的虚函数表
  • 多继承中的虚函数表
class Base1 {
public:
    virtual void func1() {cout << "Base1::func1" << endl;}
    virtual void func2() {cout << "Base1::func2" << endl;}
private:
    int b1=3;
};
class Base2 {
public:
    virtual void func1() {cout << "Base2::func1" << endl;}
    virtual void func2() {cout << "Base2::func2" << endl;}
private:
    int b2=4;
};
class Derive : public Base1, public Base2 {
public:
    virtual void func1() {cout << "Derive::func1" << endl;}
    virtual void func3() {cout << "Derive::func3" << endl;}
private:
    int d1=5;
};

int main() {
    Derive d;
    printf("%d",sizeof(d));
    return 0;
}

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
在这里插入图片描述

这里有一个需要注意的地方:虽然重写的都是func1(),但是他们地址不一样而调用的是同一个函数,为什么?
可以查看汇编代码发现:第一个Base1中的func1()的地址就是正常的地址。第二个Base2中调用func1()时在ecx寄存器中发生了this指针减8,指向了Derive对象的首地址,保证this指针的正确(因为是派生类调用,而不是基类调用)。所以本质就是修正this指针,不同编译器处理方法不同,也可以在存相同的地址,在调用前让ecx中的this指针减8。

菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果要深入研究的话可以参考以下文章:

C++虚函数表解析
C++对象的内存分布

常见的题目

  1. 输出结果?
class A
{
public:
    virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
    virtual void test(){ func();}
};
class B : public A
{
public:
    void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
    B*p = new B;
    p->test();
    return 0;
}

答案是:B->1(本题可能误认为是B->0)

  1. 重要的是理解test()是由谁调用,这里p->test(),实际上是由p传给父类的A* this指针,由父类指针去调用func(),同时func()调用符合多态(1. 父类指针 2. 虚函数重写),注意:虽然this指针类型是A*,但是this指针是由B* p转化而来的,实际上指向的对象区域还是派生类,所以多态调用调用的是派生类的func();
  2. 还有很坑的一点:虚函数重写的是实现,缺省参数还是使用的是基类的!!!

附加问题

【问题1】:多态的条件中为什么不能子类指针或者引用,为什么不能是父类的对象?
答:1. 因为只有父类的指针才能既能指向父类又能指向子类 2. 对象的切片和指针或引用的切片是有一些不同的。子类赋值给父类对象切片,不会拷贝虚表,如果拷贝虚表,那么父类对象虚表中是父类虚函数还是子类就不确定了。所以对象的切片不拷贝虚表就已经不满足多态了。如果是指针或引用的切片,父类的切片对应父类的虚表,子类的切片对应子类的虚表。重写后子类会将从父类拷贝来的虚表进行修改(用重写的函数来覆盖)来实现多态。

【问题2】: inline函数可以是虚函数吗?
答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。

【问题3】:静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

【问题4】:构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

【问题5】:析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。

【问题6】:对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

【问题7】:虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

【问题8】:什么是抽象类?抽象类的作用?
答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
【附】:

  1. 不是虚函数不会进入虚表
  2. 通常虚表最后会加个0,但是不同的编译器处理不同,VS下会给,g++没给
  3. 派生类如果自己加虚函数,编译器会将该虚函数放在虚表后面,但监视窗口很可能不会显示。
  4. 同类型的对象共用虚表
  5. 虚表存在哪?打印地址进行比较可发现虚表存在代码段(常量区)

打印虚函数表:

typedef void(*FUNC_PTR)();
void PrintVFT(FUNC_PTR* table)
{
    for(size_t i=0;table[i]!=nullptr;i++)//vs下,linux下要写死范围
    {
        printf("[%d]:%p\n",i,table[i]);
    }
}
int main()
{
	Person ps;

	int vft = *((int*)&ps);
	PrintVFT((FUNC_PTR*)vft);

	return 0;
}

有虚函数表就可以拿到里面的函数,无论是不是私有还是公有,拿到地址就可以使用里面的函数

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

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

相关文章

常见指令以及权限理解

常见指令以及权限理解 命令格式&#xff1a; command [-options] parameter1 parameter1 命令 选项 参数1 参数2 1.command为命令名称&#xff0c;例如变化目录的cd等 2.中括号[ ]实际在命令中是不存在的&#xff0c;这个中括号代表可选&#xff0c;通常选项前面会添加一个符号…

K8S deployment挂载

挂载到emptyDir 挂载在如下目录&#xff0c;此目录是pod所在的node节点主机的目录&#xff0c;此目录下的data即对应容器里的/usr/share/nginx/html&#xff0c;实现目录挂载&#xff1b;图1红框里的号对应docker 的name中的编号&#xff0c;如下俩个图 apiVersion: apps/v1 k…

Sentinel规则持久化

首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中&#xff0c;接着注册的写数据源会将新的规则保存到本地的文件中。 示例代码&#xff1a; 1.编写处理类 //规则持久化 public class FilePersistence implements InitFunc {Value("spring.application:n…

Qt应用开发(基础篇)——高级纯文本窗口 QPlainTextEdit

一、前言 QPlainTextEdit类继承于QAbstractScrollArea&#xff0c;QAbstractScrollArea继承于QFrame&#xff0c;是Qt用来显示和编辑纯文本的窗口。 滚屏区域基类https://blog.csdn.net/u014491932/article/details/132245486?spm1001.2014.3001.5501框架类QFramehttps://blo…

服务器数据库中了360后缀勒索病毒怎么办?360后缀勒索病毒的加密形式

随着信息技术的发展&#xff0c;企业的计算机服务器数据库变得越来越重要。然而&#xff0c;在数字时代&#xff0c;网络上的威胁也日益增多。近期&#xff0c;我们收到很多企业的求助&#xff0c;企业的计算机服务器遭到了360后缀勒索病毒的攻击&#xff0c;导致服务器内的所有…

【计算机视觉】相机基本知识(还在更新)

1.面阵工业相机与线阵工业相机 1.1 基本概念区别 面阵相机则主要采用的连续的、面状扫描光线来实现产品的检测&#xff1b; 线阵相机即利用单束扫描光来进行物体扫描的工作的。 1.2 优缺点 &#xff08;1&#xff09;面阵CCD工业相机&#xff1a; 优点&#xff1a;应用面…

Maven - 统一构建规范:Maven 插件管理最佳实践

文章目录 Available Plugins开源项目中的使用插件介绍maven-jar-pluginmaven-assembly-pluginmaven-shade-pluginShade 插件 - 标签artifactSetrelocationsfilters 完整配置 Available Plugins https://maven.apache.org/plugins/index.html Maven 是一个开源的软件构建工具&…

图数据库_Neo4j和SpringBoot整合使用_创建节点_删除节点_创建关系_使用CQL操作图谱---Neo4j图数据库工作笔记0009

首先需要引入依赖 springboot提供了一个spring data neo4j来操作 neo4j 可以看到它的架构 这个是下载下来的jar包来看看 有很多cypher对吧 可以看到就是通过封装的驱动来操作graph database 然后开始弄一下 首先添加依赖

核能的发展与应用

目录 1.核能的概念 2.核能的实现原理 3.核能的利与弊 4.核能未来的发展趋势 1.核能的概念 核能是指利用核反应过程中释放出的能量来产生电力或其他形式能量的能源形式。核能主要通过核裂变和核聚变两种方式产生。 1. 核裂变&#xff1a;核裂变是指重核&#xff08;通常是铀、…

ElementUI 树形表格的使用以及表单嵌套树形表格的校验问题等汇总

目录 一、树形表格如何添加序号体现层级关系 二、树形表格展开收缩图标位置放置&#xff0c;设置指定列 三、表单嵌套树形表格的校验问题以及如何给校验rules传参 普通表格绑定如下&#xff1a;这种方法只能校验表格的第一层&#xff0c;树形需要递归设置子级节点prop。 树…

如何保证数据传输的安全?

要确保数据传输的安全&#xff0c;您可以采取以下措施&#xff1a; 使用加密协议&#xff1a;使用安全的传输协议&#xff0c;如HTTPS(HTTP over SSL/TLS)或其他安全协议&#xff0c;以保护数据在传输过程中的安全性。加密协议可以有效防止数据被窃听或篡改。 强化身份验证&…

计算机网络基础

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

【hive】hive中row_number() rank() dense_rank()的用法

hive中row_number() rank() dense_rank()的用法 一、函数说明 主要是配合over()窗口函数来使用的&#xff0c;通过over(partition by order by )来反映统计值的记录。 rank() over()是跳跃排序&#xff0c;有两个第二名时接下来就是第四名(同样是在各个分组内)dense_rank() …

设计模式——适配器模式

引入实例 说起适配器其实在我们的生活中是非常常见的&#xff0c;比如&#xff1a;学校的宿舍的电压都比较低&#xff0c;而有的学生想使用大功率电器&#xff0c;宿舍的就会跳闸&#xff0c;然而如果你使用一个适配器&#xff08;变压器&#xff09;就可以使用了&#xff08;…

搭建openGauss 5.0 一主一从复制集群

openGauss是一款支持SQL2003标准语法&#xff0c;支持主备部署的高可用关系型国产数据库。 多种存储模式支持复合业务场景&#xff0c;新引入提供原地更新存储引擎。NUMA化数据结构支持高性能。Paxos一致性日志复制协议&#xff0c;主备模式&#xff0c;CRC校验支持高可用。支…

golang操作excel的高性能库——excelize/v2

目录 介绍文档与源码安装快速开始创建 Excel 文档读取 Excel 文档打开数据流流式写入 [相关 Excel 开源类库性能对比](https://xuri.me/excelize/zh-hans/performance.html) 介绍 Excelize是一个纯Go编写的库&#xff0c;提供了一组功能&#xff0c;允许你向XLAM / XLSM / XLS…

【【STM32之GPIO】】

STM32之GPIO 学完了正点原子自带的视频课之后感觉仍然一知半解现在更新一下来自其他版本的STM32学习 GPIO 就是 General Purpose Input Output 中文名叫通用输入输出口 可配置8种输入输出模式 引脚电平 0V~3.3V 部分引脚可容忍5V 输出模式下可控制端口输出高低电平&#xff…

【Spring Cloud 二】——Spring Cloud基本介绍

Spring Cloud基本介绍 一、Spring Cloud简介二、Spring Cloud核心组件Spring Cloud Netflix组件Spring Cloud Alibaba组件Spring Cloud原生组件微服务架构图 三、Spring Cloud与Spirng Boot的关系四、Spring Cloud的版本选择Spring Cloud Alibaba的版本选择 一、Spring Cloud简…

从外部访问K8s中Pod的五种方式

hostNetwork、 hostPort、 NodePort、 LoadBalancer、 Ingress 暴露Pod与Service一样&#xff0c;因为Pod就是Service的backend 1、hostNetwork&#xff1a;true 这是一种直接定义 Pod 网络的方式。 如果在 Pod 中使用 hostNetwork:true 配置&#xff0c; pod 中运行的应用程序…

UG\NX 二次开发 相切面、相邻面的选择控件

文章作者:里海 来源网站:https://blog.csdn.net/WangPaiFeiXingYuan 简介: 有群友问“UFUN多选功能过滤面不能选择相切面或相邻面之类的吗?” 这个用Block UI的"面收集器"就可以,ufun函数是不行的。 效果: C++语言在UG二次开发中的应用及综合分析 C++ …