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,一经查实,立即删除!

相关文章

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 的区别是什么&…

GlusterFS 分布式文件系统

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

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

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

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…

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;无论在哪个站点哪个页面都是如此。 我知道…

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写中间层代码 方式一&…

获取参数

获取querystring参数 querystring 指的是URL中 ? 后面携带的参数&#xff0c;例如&#xff1a;http://127.0.0.1:9090/web?query杨超越。 获取请求的querystring参数的方法如下&#xff1a; 方法1&#xff1a; Query package main// querystringimport ("github.com/…

引领长期投资新篇章:价值增长与财务安全的双重保障

随着全球金融市场的不断演变&#xff0c;长期投资策略因其稳健性和对价值增长的显著推动作用而日益受到投资者的重视。在这一背景下&#xff0c;Zeal Digital Shares&#xff08;ZDS&#xff09;项目以其创新的数字股票产品&#xff0c;为全球投资者提供了一个全新的长期投资平…

最优化理论与自动驾驶(十一):基于iLQR的自动驾驶轨迹跟踪算法(c++和python版本)

最优化理论与自动驾驶&#xff08;四&#xff09;&#xff1a;iLQR原理、公式及代码演示 之前的章节我们介绍过&#xff0c;iLQR&#xff08;迭代线性二次调节器&#xff09;是一种用于求解非线性系统最优控制最优控制最优控制和规划问题的算法。本章节介绍采用iLQR算法对设定…

分析redis实现分布式锁的思路

文章目录 1、基于redis实现分布式锁&#xff1a;利用key的唯一性1.1、独占排他1.2、死锁问题1.2.1、redis客户端程序获取了锁之后&#xff0c;服务器立马宕机&#xff0c;就会导致死锁。1.2.2、不可重入&#xff1a;可重入 1.3、原子性&#xff1a;加锁和过期之间&#xff1a;s…

深入剖析Docker容器安全:挑战与应对策略

随着容器技术的广泛应用&#xff0c;Docker已成为现代应用开发和部署的核心工具。它通过轻量级虚拟化技术实现应用的隔离与封装&#xff0c;提高了资源利用率。然而&#xff0c;随着Docker的流行&#xff0c;其安全问题也成为关注焦点。容器化技术虽然提供了良好的资源隔离&…

4.C_数据结构_队列

概述 什么是队列&#xff1a; 队列是限定在两端进行插入操作和删除操作的线性表。具有先入先出(FIFO)的特点 相关名词&#xff1a; 队尾&#xff1a;写入数据的一段队头&#xff1a;读取数据的一段空队&#xff1a;队列中没有数据&#xff0c;队头指针 队尾指针满队&#…

FPGA与Matlab图像处理之直方图均衡化

文章目录 一、什么是直方图?二、什么是直方图均衡化&#xff1f;三、Matlab实现直方图均衡化的步骤第一步&#xff1a; 彩色图像转成灰度图像第二步&#xff1a;提取亮度通道的直方图第三步&#xff1a;累计亮度通道的像素值频率第四步&#xff1a; 映射到新的灰度值 四、Veri…

嵌入式 开发技巧和经验分享

文章目录 前言嵌入式 开发技巧和经验分享目录1.1嵌入式 系统的 定义1.2 嵌入式 操作系统的介绍1.3 嵌入式 开发环境1.4 编译工具链和优化1.5 嵌入式系统软件开发1.6 嵌入式SDK开发2.1选择移植的系统-FreeRtos2.2FreeRtos 移植步骤2.3 系统移植之中断处理2.4系统移植之内存管理2…

【java面经】Redis速记

目录 基本概念 string hash list set zset 常见问题及解决 缓存穿透 缓存击穿 缓存雪崩 Redis内存管理策略 noeviction allkeys-lru allkeys-random volatile-random volatile-ttl Redis持久化机制 RDB快照 AOF追加文件 Redis多线程特性 Redis应用场景 缓…