C++:多态(协变,override,final,纯虚函数抽象类,原理)

目录

编译时多态

函数重载

模板

运行时多态

多态的实现

实现多态的条件

协变

析构函数的重写

override 关键字

final 关键字

重载、重写、隐藏对比

纯虚函数和抽象类

多态的原理


多态是什么?

多态就是有多种形态

多态有两种,分别是编译时多态(静态多态)、运行时多态(动态多态)

编译时多态

函数重载

函数重载就是其中的一种多态

void print(int i) {  cout << "Printing int: " << i << endl;  
}  void print(double f) {  cout << "Printing float: " << f << endl;  
}  

我们用一个print函数就可以实现上面的两种状态,由于是编译时完成的多态所以叫做编译时多态 

模板

模板也是其中的一种多态

template<class T>  
void print(T value) {  cout << value << endl;  
} 

 这里我们可以给print函数传任意类型的参数,这里也可以体现出多态

在我们传参时在编译时期就会生成对应的函数,所以它也是编译时多态

运行时多态

运行时多态就是我们需要完成某一个任务(函数),那么当我们传不同的对象时所能产生的效果是不一样的

例如:

当我们买火车票的时候,普通人买票是全价,学生买票可以打折,如果是军人那甚至可以优先买票

当我们在完成买票这个过程的时候,不同的人来买票所产生的结果是不一样的,这就是多态

下面来具体解释运行时多态

多态的实现

多态的构成

首先多态是一个继承关系下的对象去调用同一个函数,从而产生了不同的行为

实现多态的条件

  • 被调用的函数必须是虚函数
  • 必须是指针或者引用调用虚函数
  • 派生类需要对基类的虚函数进行重写/覆盖(只有这样才会两个相同的函数有不同的行为)
class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
};class Student : public Person
{
public:void BuyTicket(){cout << "买票半价" << endl;}
};void func(Person* p)
{p->BuyTicket();
}int main()
{Person p;Student s;func(&p);func(&s);return 0;
}

首先Student和Person是继承关系

其次基类Person所需要实现多态的函数是虚函数

子类里面的函数可以写virtual也可以不写,因为Person继承下来后无论写不写本身都是虚函数

为了能有不同的行为,我们需要重写该函数

调用函数的时候使用的是指针或者引用

这样我们多态的条件就全部都有了,下面只需要传相应的对象就可以完成对应的行为了

协变

当派生类重写基类的虚函数时,派生类和基类的返回值不同

基类虚函数返回其它基类对象的指针或者引用,派生类虚函数返回其它派生类对象的指针或者引用,称为协变

class A {};
class B : public A {};class Person {
public:virtual A* BuyTicket(){cout << "买票-全价" << endl;return nullptr;}
};class Student : public Person {
public:virtual B* BuyTicket(){cout << "买票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}

析构函数的重写

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

如上代码A类的析构函数和B类的析构函数其实是构成重写的

看起来两个类的析构函数名字不相同,但是在编译器进行处理的时候会统一将析构函数的名字处理成destructor 

所以即使我们是两个A类型的指针,它们new出来的对象如果不构成多态是不会调用B的析构函数的,只有构成多态,A对象才会调用A的析构,B对象调用B的析构

这里已经满足了三个条件:

A和B的继承关系

析构函数为虚函数

指针或引用调用虚函数 

如果这里不构成多态那么就会发生内存泄漏的问题! 

override 关键字

由于C++对重写的要求较为严格,因此C++11提供了override关键字

它的作用是可以帮助我们检查是否重写

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A {
public:~B() override{cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};

若是把A的virtual给去掉,则构不成重写

final 关键字

如果我们不想让派生类重写这个虚函数,那么我们可以加上关键字final

一旦我们试图对基类某个虚函数带上了final关键字进行重写,那么则会报错

class A
{
public:virtual ~A() final{cout << "~A()" << endl;}
};class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};

重载、重写、隐藏对比

纯虚函数和抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数

class Person
{
public:virtual void BuyTicket() = 0;
};

纯虚函数不需要定义实现

因为实现没有意义,需要被派生类重写。只需要声明即可 

这个时候的这个Person类叫做抽象类

有纯虚函数的类就是抽象类

抽象类是不可以定义出对象的 

如果派生类继承后不重写虚函数,那么该派生类也是抽象类

纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就无法实例化出对象

多态的原理

当一个类中有虚函数时,这个类是会多出一个成员指针变量__vfptr,我们把它叫做虚函数表指针

v代表virtual,f代表function,ptr则是指针

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
private:int _age;char _name[20];
};int main()
{Person p;cout << sizeof(p) << endl;return 0;
}

从上面的实验可以看出,类中是会有一个虚函数表指针的,除了age和name占24个字节以外,还多了一个虚函数表指针占了4个字节,所以是28个字节

从底层的角度来想,为什么我们满足了多态的条件后,可以通过Person指针所指向的对象来精确的找到我们要使用的函数呢? 

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
private:int _age;char _name[20];
};class Student : public Person
{virtual void BuyTicket(){cout << "买票半价" << endl;}
private:int _id;
};int main()
{Person p;Student s;return 0;
}

通过上图得知

当我们满足多态的条件时,底层不再是编译时通过对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数地址,所以就能实现我们是哪个对象就调用哪个对象的类中的虚函数

这也是为什么它是运行时多态的原因

虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址
  • 派生类会继承基类的虚函数表指针,但两者并不是同一个虚函数表指针
  • 派生类的虚函数表中包含 
  1. 基类的虚函数地址
  2. 派生类重写的虚函数地址
  3. 派生类自己的虚函数地址
  • 派生类重写基类的虚函数后,派生类的虚函数表里对应的虚函数就会被覆盖成派生类重写的虚函数地址
  • 虚函数和普通函数一样,都是存在代码段中的,只是虚函数的地址又存在虚表中
  • 虚函数表存在哪个地方并没有严格的规定,由编译器自己决定,具体在哪里我们可以依靠代码验证

验证代码: 

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}void func(){cout << "void func()" << endl;}
private:int _age;char _name[20];
};class Student : public Person
{virtual void BuyTicket(){cout << "买票半价" << endl;}
private:int _id;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Person b;Student d;Person* p3 = &b;Student* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Person::BuyTicket);printf("普通函数地址:%p\n", &Person::func);return 0;
}

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

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

相关文章

使用Go语言的互斥锁(Mutex)解决并发问题

解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 在并发编程中,由于存在竞争条件和数据竞争,我们需要将某些代码片段设定为临界区,并使用互斥锁(Mutex)等同步原语来保护这些临界区。本文将详细介绍Go语言标准库中Mutex的使用方法,以及如何利用它来解决实际…

Linux驱动开发 ——架构体系

只读存储器&#xff08;ROM&#xff09; 1.作用 这是一种非易失性存储器&#xff0c;用于永久存储数据和程序。与随机存取存储器&#xff08;RAM&#xff09;不同&#xff0c;ROM中的数据在断电后不会丢失&#xff0c;通常用于存储固件和系统启动程序。它的内容在制造时或通过…

Java基础面试题——异常

目录 关系图 1. Throwable和Exception之间的关系 2.异常分为哪两大类 3.常见的 RuntimeException 4. 常见的 Error 5.什么是已检查异常和未检查异常&#xff1f;它们的区别是什么&#xff1f; 6.Java 中如何自定义异常&#xff1f; 7.throw 和 throws 的区别是什么&…

ubuntu18.04升级到20.04

要将 Ubuntu 18.04 升级到 Ubuntu 20.04&#xff0c;你可以按照以下步骤进行操作。升级操作会涉及到系统的核心部分&#xff0c;建议在升级之前备份重要数据。 1. 备份数据 在进行系统升级之前&#xff0c;请确保备份重要的数据。可以使用 rsync、tar、或者系统备份工具来创建…

GlusterFS 分布式文件系统

一、GlusterFS 概述 1.1 什么是GlusterFS GlusterFS 是一个开源的分布式文件系统&#xff0c;它可以将多个存储服务器结合在一起&#xff0c;创建一个大的存储池&#xff0c;供客户端使用。它不需要单独的元数据服务器&#xff0c;这样可以提高系统的性能和可靠性。由于没有…

视频转文字工具:开启视频内容深度挖掘的钥匙

图片里到文字要提取出来&#xff0c;现在有很多的工具&#xff0c;但是视频里的文字要提取出来&#xff0c;是不是就不那么好操作呢&#xff1f;并不是的&#xff0c;现在也有不少支持视频转文字的工具&#xff0c;这次我们就来介绍一些可以提高我们视频文字提取效率的工具吧。…

速盾:凡科建站开cdn了吗?

凡科建站是一家专业的建站平台&#xff0c;提供了多种功能和工具来帮助用户快速搭建自己的网站。随着互联网技术的不断发展&#xff0c;网站的访问速度和稳定性成为了越来越重要的考虑因素。为了优化用户体验&#xff0c;提高网站的加载速度&#xff0c;凡科建站已经开启了CDN&…

经典sql题(八)SQL 查询详细指南总结一

SQL 查询详细指南 SQL&#xff08;Structured Query Language&#xff09;是一种用于管理和操作关系数据库的标准语言。本文将详细介绍 SQL 中的一些常见操作及其用法&#xff0c;包括 DISTINCT 去重、LIMIT 限制、排序、开窗函数、NULL 值替换、JOIN 与 UNION 等。 1. DISTI…

大话Python|基础语法(上)

一、单行注释 以下代码输出一个Hello World&#xff01;字符串 在Python代码中&#xff0c;注释会自动被Python解析器忽略 print(Hello World) 二、多行注释 在Python代码中&#xff0c;注释一共有两种形式&#xff1b; 1、单行注释&#xff1a;注释的内容只有一行 2、多行…

计算机网络笔记001

讲义 1.计算机网络的定义  定义&#xff1a; 一批独立自治的计算机系统的互连集合体  说明&#xff1a; 独立自治的计算机系统&#xff0c; 互连的手段是各种各样的&#xff0c; 依据协议进行 工作  2.计算机网络和通信网络  通信网络&#xff1a; 重点研究通…

element plus 按需导入vue

步骤一&#xff1a; 下载element plus 在打开vue项目的编辑器中打开终端&#xff0c;或者在cmd窗口进入到项目目录下 用你的包管理器安装element plus,例如使用npm包管理器&#xff1a; npm install element-plus --save 查看是否安装成功&#xff1a;查看项目文件package…

PostgreSQL(PG)(二十二)

&#x1f33b;&#x1f33b; 目录 &#x1f33b;&#x1f33b; 一、PostgreSQL 简介1.1、PG 的历史1.2、PG的社区1.2.1 纯社区1.2.2 完善的组织结构1.2.3 开源许可独特性 1.3 、PostgreSQL与MySQL的比较 二、PostgresQL的下载安装2.1、Windows上安装 PostgreSQL2.2、远程 连接 …

RK3568部署DOCKER启动服务器失败解决办法

按照上文的方法部署完DOCKER之后&#xff0c;启动服务异常&#xff0c;查阅网络相关资源&#xff0c;解决方案如下&#xff1a; 修改/源码/kernel/arch/arm64/configs/OK3568-C-linux_defconfig&#xff0c;在最后添加 CONFIG_MEMCGy CONFIG_VETHy CONFIG_BRIDGEy CONFIG_BRID…

php怎么连接使用kafka

PHP 连接并使用 Kafka 需要借助 Kafka 的 PHP 客户端库&#xff0c;比如流行的 php-rdkafka 扩展。它是基于 C 语言的 librdkafka 库的 PHP 绑定&#xff0c;功能稳定且性能高。下面是如何使用 php-rdkafka 来连接和使用 Kafka 的步骤。 1. 安装 php-rdkafka 1.1 安装依赖 首…

GS-SLAM论文阅读笔记--TAMBRIDGE

前言 本文提出了一个自己的分类方法&#xff0c;传统的视觉SLAM通常使用以帧为中心的跟踪方法&#xff0c;但是3DGS作为一种高效的地图表达方法好像更侧重于地图的创建。这两种方法都有各自的优缺点&#xff0c;但是如果能取长补短&#xff0c;互相结合&#xff0c;那么就会是…

6.7泊松噪声

基础概念 在OpenCV联合C中给一张图片添加泊松噪声&#xff08;Poisson Noise&#xff09;可以通过生成随机数并在图像的每个像素上加上这些随机数来实现。泊松噪声是一种统计分布服从泊松分布的噪声&#xff0c;通常用于模拟光子计数等场景。 使用泊松噪声的场景 泊松噪声通…

【解决】chrome 谷歌浏览器,鼠标点击任何区域都是 Input 输入框的状态,能看到输入的光标

chrome 谷歌浏览器&#xff0c;鼠标点击任何区域都是 Input 输入框的状态&#xff0c;能看到输入的光标 今天打开电脑的时候&#xff0c;网页中任何文本的地方&#xff0c;只要鼠标点击&#xff0c;就会出现一个输入的光标&#xff0c;无论在哪个站点哪个页面都是如此。 我知道…

Pandas 数据分析入门详解

今日内容大纲介绍 DataFrame读写文件 DataFrame加载部分数据 DataFrame分组聚合计算 DataFrame常用排序方式 1.DataFrame-保存数据到文件 格式 df对象.to_数据格式(路径) ​ # 例如: df.to_csv(data/abc.csv) 代码演示 如要保存的对象是计算的中间结果&#xff0c;或者以…

CQRS模型解析

简介 CQRS中文意思为命令于查询职责分离&#xff0c;我们可以将其了解成读写分离的思想。分为两个部分 业务侧和数据侧&#xff0c;业务侧主要执行的就是数据的写操作&#xff0c;而数据侧主要执行的就是数据的读操作。当然两侧的数据库可以是不同的。目前最为常用的CQRS思想方…

C++调用C# DLL之踩坑记录

C是非托管代码&#xff0c;C#则是托管代码&#xff0c;无法直接调用 CLR的介绍见CLR简介 MSDN提到了两种非托管-托管的交互技术&#xff1a;CLR Interop和COM Interop 后者要将C# 类库注册为COM组件&#xff0c;本文只探讨CLR&#xff0c;要通过C CLR写中间层代码 方式一&…