【C++深度探索】全面解析多态性机制(一)

hello hello~ ,这里是大耳朵土土垚~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹
在这里插入图片描述

💥个人主页:大耳朵土土垚的博客
💥 所属专栏:C++入门至进阶
这里将会不定期更新有关C++的内容,欢迎大家多多点赞关注收藏💖💖

目录

  • 1.什么是多态
  • 2.虚函数的定义与重写
  • 3.多态的实现
  • 4.虚函数重写的两个例外
  • 5.C++11 override 和 final
  • 6.重载、覆盖(重写)、隐藏(重定义)的对比
  • 7.抽象类
  • 8.结语

1.什么是多态

在C++中,多态(Polymorphism)是指通过基类指针引用来访问派生类对象的一种机制。简单来说,它允许我们在基类类型的指针或引用上调用派生类对象的成员函数。

通俗来说,就是多种形态,或者说完成某个行为时,当不同的对象去完成会产生出不同的状态。

例如:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。不同的对象去完成同一个行为——买票,会产生不同的状态。

在代码中的具体体现则依赖于虚函数(Virtual Function)。在基类中,可以将某个成员函数声明为虚函数,而在派生类中重写该函数。通过使用基类指针或引用指向派生类对象,并调用该虚函数,实际上在运行时会根据对象的实际类型调用合适的函数

2.虚函数的定义与重写

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

class Person {
public://虚函数virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

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

class Person {
public:
//虚函数virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {
public:
//虚函数重写virtual void BuyTicket() { cout << "买票-半价" << endl; }};

在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,所以不建议这样使用

注意这里与继承中的隐藏区分一下,隐藏是只要在派生类中有与基类函数名相同的函数就构成,而重写则需要返回值、函数名和参数列表完全相同才构成

3.多态的实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上述代码Student类继承了Person。而Person对象买票全价,Student对象买票半价。

在继承中要构成多态有两个条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

例如:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
//条件二:调用的是虚函数并且派生类对虚函数进行重写virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{
//条件一:基类的引用或指针调用虚函数p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

多态的实现结果如下:

在这里插入图片描述

通过使用基类引用p指向派生类对象,并调用虚函数BuyTicket(),实际上在运行时会根据对象的实际类型调用合适的函数来实现多态,也就是不同的对象完成同一个行为会有不同的状态——普通人买票全价,学生买票半价

这种通过基类指针或引用调用派生类对象的成员函数的行为称为多态。它使得我们可以在不同类型的对象上使用相同的接口,提供了更高的灵活性、可扩展性和代码复用性。

4.虚函数重写的两个例外

  • 协变(基类与派生类虚函数返回值类型不同)
    基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

前面我们学习过虚函数重写必须要求基类与派生类除了函数体以外其它完全相同,但是对于协变,基类与派生类的返回值类型可以不同,但基类与派生类的函数的返回类型必须是继承关系

//协变
class A {};
class B : public A {};class Person {
public:virtual A* f() { return new A; }//基类虚函数返回值类型为基类指针或引用
};
class Student : public Person {
public:virtual B* f() { return new B; }//派生类虚函数返回值类型为派生类指针或引用
};

当然基类虚函数返回值类型可以是别的基类也可以是自己,派生类虚函数返回值类型可以是别的派生类也可以是自己,与基类相呼应:

class Person {
public:virtual Person* f() { return new Person; }//基类虚函数返回值类型为基类指针或引用
};
class Student : public Person {
public:virtual Student* f() { return new Student; }//派生类虚函数返回值类型为派生类指针或引用
};
  • 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。

这是因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

例如:

//析构函数重写
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:
//构成多态条件二:基类虚函数并且派生类虚函数重写virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{
//构成多态条件一:使用基类指针或引用调用虚函数(这里是析构函数)Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

只有派生类Student的析构函数重写了Person的析构函数,delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

结果如下:
在这里插入图片描述

这里要注意派生类的析构调用完之后会自动调用基类对象的析构函数,所以这里基类的析构函数调用了两次

我们可以对比一下,当没有实现多态时,对于delete对象调用析构函数是不会根据所指向的对象调用相应的析构函数,而是直接调用Person类的析构函数:

//没有实现多态
class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

结果如下:

在这里插入图片描述

如果是这样,那么如果子类Student类中动态开辟了一块空间而没有调用合适的析构函数就会造成内存泄漏:

//内存泄漏
class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:int* ptr = new int[10];//动态开辟空间~Student() { delete[] ptr;//释放空间cout << "~Student()" << endl;}
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

在这里插入图片描述

在类的继承关系中,派生类的析构函数会自动调用基类的析构函数。因此,可以重写(覆盖)基类的析构函数,以处理派生类特有的资源清理需求

尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态。

5.C++11 override 和 final

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

  • final:修饰虚函数,表示该虚函数不能再被重写
    例如:
class Person {
public:virtual void BuyTicket() final { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

结果如下:

在这里插入图片描述

此外final也可以修饰类表明该类不可被继承。

  • override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Person {
public:virtual void BuyTicket(){ cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() override{ cout << "买票-半价" << endl; }
};

重写了BuyTicket(),编译不报错

如下图所示,屏蔽了基类的虚函数,派生类的函数没有重写,编译报错:

在这里插入图片描述

6.重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

重载与重定义主要区别在于作用域,而重定义与重写主要区别在于函数返回值与函数参数列表是否相同。

重写是重定义的一种特殊形式,重定义中包括重写

7.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

纯虚函数是在基类中声明的虚函数,但没有给出具体的实现,也就是没有函数体。抽象类只能用作其他类的基类,不能被直接实例化。

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

派生类必须实现基类中的所有纯虚函数,否则派生类也会成为抽象类。

例如:

//抽象类
class Person {
public:virtual void BuyTicket() = 0;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

在上面的示例中,Person是一个抽象类,定义了一个纯虚函数:BuyTicket()。Student是Person的派生类,必须实现基类中的纯虚函数

在这里插入图片描述

注意:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

8.结语

对于多态,我们可以将其理解为同一件事,不同的对象去做会有不同的状态,这就构成了多态,这与我们的现实生活息息相关,比如上文中举例的买票行为,再比如价格歧视等等。而我们在编程中要实现多态就必须要满足两个条件:一个是基类的指针或引用来调用虚函数,另一个则是基类中定义虚函数并且在派生类中对该虚函数进行重写;这两个条件缺一不可,这与多态实现的底层原理有关,我们后续再了解。对于虚函数重写的两个例外中析构函数的重写要掌握清楚,此外对于重载、重写与重定义的区别我们也要弄明白。以上就是今天的所有内容啦~ 完结撒花~ 🥳🎉🎉

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

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

相关文章

万物皆可嵌入--embedding在GPU中的实现

摘要 Embedding技术自从谷歌推出word2vec的工作后得到迅速的应用&#xff0c;典型应用之一是在广告推荐场景中&#xff0c;从word2vec演进到item2vec&#xff0c;embedding技术的出现也使深度学习进入广告推荐的场景成为可能。广告推荐模型动辄几十GB甚至TB的模型大小&#xf…

数据结构(Java):集合类LinkedList集合类Stack

1、集合类LinkedList 1.1 什么是LinkedList LinkedList的底层是一个双向链表的结构&#xff08;故不支持随机访问&#xff09;&#xff1a; 在LinkedList中&#xff0c;定义了first和last&#xff0c;分别指向链表的首节点和尾结点。 每个节点中有一个成员用来存储数据&…

京准电钟|ptp精密时钟服务器在网络内作用是什么?

京准电钟&#xff5c;ptp精密时钟服务器在网络内作用是什么&#xff1f; 京准电钟&#xff5c;ptp精密时钟服务器在网络内作用是什么&#xff1f; PTP精密时钟服务器在计算机网络中的作用非常重要&#xff0c;特别是在需要高精度时间同步的场景中。 PTP能够提供纳秒级的时间同…

TikTok短视频矩阵管理系统源码

在数字化浪潮汹涌的今天&#xff0c;短视频已成为人们生活中不可或缺的一部分。TikTok作为短视频领域的佼佼者&#xff0c;其用户基数庞大&#xff0c;影响力深远。然而&#xff0c;对于众多内容创作者和营销人员来说&#xff0c;如何高效管理多个TikTok账号&#xff0c;实现批…

one-hot-zhu案例

# 导入用于对象保存与加载的joblib # from sklearn.externals import joblib import joblib # 导入keras中的词汇映射器Tokenizer from keras.preprocessing.text import Tokenizer def one_hot_01(): # 1 准备语料 vocabs # vocabs {“周杰伦”, “陈奕迅”, “王力宏”, “…

分布式训练

一、分布式计算 跟多GPU不同是&#xff1a;数据不是从主存拿的&#xff0c;是在分布式文件系统拿的&#xff0c;有多个工作站&#xff0c;工作站中有多个GPU&#xff0c;通过网络读取数据到GPU中&#xff0c;GPU通过网络接收到来自参数服务器的参数进行运算计算梯度&#xff0c…

Biotinylated L-Thyroxine (T4) ;生物素 L-甲状腺素(T4)

一、基本信息 常用名&#xff1a;Biotinylated L-Thyroxine (T4) 生物素 L-甲状腺素(T4) 英文名称&#xff1a;Biotinylated L-Thyroxine (T4) 中文名称&#xff1a;生物素 L-甲状腺素(T4) 二、组成与性质 生物素&#xff1a;一种水溶性维生素&#xff0c;也称为维生素B7&#…

Photoshop批量处理图片分辨率

整理一些文件的时候&#xff0c;发现需要处理大量图片的尺寸和分辨率。如果一张一张的处理就会很慢&#xff0c;搜了下&#xff0c;Photoshop提供自动批量处理的方法。在此记录一下。 一、说说批量处理图片 1.打开PS软件并导入图片&#xff0c;我用的是比较老的版本cs4&#…

在创建jsp项目中解决无法连接数据库以及junit问题

&#x1f4bb;博主现有专栏&#xff1a; C51单片机&#xff08;STC89C516&#xff09;&#xff0c;c语言&#xff0c;c&#xff0c;离散数学&#xff0c;算法设计与分析&#xff0c;数据结构&#xff0c;Python&#xff0c;Java基础&#xff0c;MySQL&#xff0c;linux&#xf…

使用semgrep做代码规范扫描

semgrep 简介 semgrep 是一种静态代码分析工具&#xff0c;用于发现和修复软件代码中的安全漏洞、Bug 和编码风格问题。它可以帮助开发人员和安全团队在早期发现和解决潜在的代码问题&#xff0c;提高软件质量和安全性。 以下是 semgrep 的一些主要功能和特点&#xff1a; 静…

将 Vision Transformer 用于医学图像的语义分割

关于ViT的关键点如下&#xff1a; ViT架构基于将图像表示为一组补丁。图像补丁是图像的非重叠块。每个块最初都有一个由该块中的图像像素形成的嵌入向量。Transformer编码器是ViT的主要部分&#xff0c;它根据它们的类别归属来训练补丁之间的相似度。它包含一系列线性、归一化…

拥抱UniHttp,规范Http接口对接之旅

前言 如果你项目里还在用传统的编程式Http客户端比如HttpClient、Okhttp去直接对接第三方Http接口&#xff0c; 那么你项目一定充斥着大量的对接逻辑和代码&#xff0c; 并且针对不同的对接渠道方需要每次封装一次调用的简化&#xff0c; 一旦封装不好系统将会变得难以维护&am…

SprintBoot创建遇到的问题

最近使用IDEA版本为2022.3.1&#xff0c;java版本为21.0.3&#xff0c;现在做一个创建SprintBoot3的一个大体流程 1.先下载Maven&#xff0c;解压到一个位置 maven下载 2.配置setting.xml文件 这路径自己配置&#xff0c;这里不多演示 代码如下&#xff1a; <mirror>&…

基于物联网设计的人体健康监护系统(STM32+ESP8266+华为云IOT)(182)

基于物联网设计的人体健康监护系统(STM32+ESP8266+华为云IOT)(182) 一、前言 1.1 项目介绍 【1】项目功能介绍 随着人们生活水平的提高和健康意识的增强,对个人健康状况的关注也越来越高。传统的健康监测方式往往需要到医院或诊所进行体检,费时费力且不够便捷。本项目设计…

0124__Linux和Unix的Access.conf安全配置

Linux和Unix的Access.conf安全配置-CSDN博客

iptables 详解

在 Buildroot 系统中&#xff0c;iptables 是一个用户空间实用程序程序&#xff0c;它允许管理员配置 Linux 内核中的 IP 包过滤规则。以下是一些关于 iptables 的基本用法和常见示例&#xff1a; 基本用法 iptables 使用规则链来处理数据包。主要的链包括&#xff1a; INPUT…

前端如何取消接口调用

&#x1f9d1;‍&#x1f4bb; 写在开头 点赞 收藏 学会&#x1f923;&#x1f923;&#x1f923; 1. xmlHttpRequest是如何取消请求的&#xff1f; 实例化的XMLHttpRequest对象上也有abort方法 const xhr new XMLHttpRequest(); xhr.addEventListener(load, function(e)…

开关电源——15种控制模式(1)

关于开关电源的控制模式&#xff0c;TI官网的控制模式快速参考指南有相对全面的归纳和描述&#xff0c;提供了15种不同的控制架构&#xff0c;这些架构涵盖了从基础到高级的多种控制模式&#xff0c;以适应不同的应用需求&#xff0c;如下表所示&#xff1a; 以下是对控制模式相…

大话C语言:第28篇 内存分配与释放

1 malloc函数 函数说明&#xff1a; #include <stdlib.h>void *malloc(size_t size); 功能&#xff1a;在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域&#xff0c;用来存放类型说明符指定的类型。分配的内存空间内容不确定。 参数&#xff1a;size&…

记一次Ueditor上传Bypss

前言 前一段时间和小伙伴在某内网进行渗透测试&#xff0c;目标不给加白&#xff0c;只能进行硬刚了&#xff0c;队友fscan一把梭发现某资产疑似存在Ueditor组件&#xff0c;但初步测试是存在waf和杀软的&#xff0c;无法进行getshell&#xff0c;经过一番折腾最终getshell&am…