【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.结语

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

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

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

相关文章

浅谈React

forwardRef和useImperativeHandle的联动使用 import React, { useImperativeHandle, useRef } from "react" import { forwardRef } from "react"const CustomInput forwardRef((props, ref) > {const inputRef useRef<HTMLInputElement>(null…

java并发编程概述

java并发编程概述 一. 进程和线程的概念 进程是计算机中的程序关于某数据集合上的一次运行活动&#xff0c;是系统进行资源分配的基本单位。进程是程序运行的实例&#xff0c;每当操作系统在运行一个程序时&#xff0c;会为其创建一个进程。每个进程都拥有自己的一整套变量。…

同享人力资源管理系统-TXEHR V15 DownloadTemplate 文件读取漏洞复现

0x01 产品简介 同享人力资源管理系统(TXEHR V15)是一款专为现代企业设计的人力资源管理软件解决方案,旨在通过先进的信息化手段提升企业人力资源管理的效率与水平。该系统集成了组织人事、考勤管理、薪资核算、招聘配置、培训发展、绩效管理等核心模块,并提供了灵活的配置…

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

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

解决QT creator中文乱码问题

1.首先设置文本编辑器为UTF-8 先在工具-选项-文本编辑器-behavior部分选择文件编码为UTF-8&#xff0c;紧接着是选择“如果编码是UTF-8则添加”&#xff0c;如下图 2.设置ext code for tools 为system 具体解决办法是 工具-选项-环境-interfaces这一栏有一个“Text code for to…

网安防御保护-小实验

1、DMZ区内的服务器&#xff0c;办公区仅能在办公时间内(9:00-18:00)可以访问&#xff0c;生产区的设备全天可以访问 2、生产区不允许访问互联网&#xff0c;办公区和游客区允许访问互联网 3、办公区设备10.0.2.10不允许访问DMZ区的FTP服务器和HTTP服务器&#xff0c;仅能ping通…

前端如何取消接口调用

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

昇思25天学习打卡营第17天|应用实践之SSD目标检测

基本介绍 今天要学习的内容是计算机视觉领域中的目标检测任务。与图像分类相比&#xff0c;目标检测更难&#xff0c;因为目标检测不仅要检测出图片中的物体的类别&#xff0c;还要检测出该物体的位置。现主流的目标检测算法大致可分为两种&#xff0c;一种是基于CNN的&#xf…

MQTT是什么,物联网

写文思路&#xff1a; 以下从几个方面介绍MQTT&#xff0c;包括&#xff1a;MQTT是什么&#xff0c;MQTT和webSocket的结合&#xff0c;以及使用场景&#xff0c; 一、MQTT是什么 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的发布/订阅消息…

Java技术栈总结:JVM虚拟机篇

一、Java的四种引用类型 1、强引用 最常见的引用&#xff0c;类似Object obj new Object()、String str “hello”。 如果一个对象具有强引用&#xff0c;垃圾回收器绝对不会回收它。即使抛出“OutOfMemoryError”错误&#xff0c;程序终止&#xff0c;也不会随意回收具有强…

市场趋势的智能预测:Kompas.ai如何洞察未来市场动向

在商业领域&#xff0c;市场趋势预测是企业制定战略规划和做出明智决策的关键。准确把握市场动向能够帮助企业及时调整战略&#xff0c;抓住机遇&#xff0c;规避风险。Kompas.ai&#xff0c;一款先进的人工智能市场分析工具&#xff0c;正通过其深度学习和数据分析能力&#x…

重塑肌肤DNA!华贝甄选解锁生命活力密码

在探索生命奥秘与健康的征途中&#xff0c;华贝甄选携手前沿干细胞科技&#xff0c;为您开启一场前所未有的健康革命。我们深知&#xff0c;生命的活力源自细胞的不懈更新与修复&#xff0c;而干细胞&#xff0c;正是这场生命奇迹的钥匙。 【重塑内分泌平衡&#xff0c;焕发自…

Sentieon Arm版本:进一步降低基因组计算成本

前不久&#xff0c;Arm在其社区的HPC blog上发布了一篇Sentieon在低通量全基因组&#xff08;LP-WGS&#xff09;的应用案例。 图1 伴随着大规模基因组学的需求持续增长&#xff0c;基因测序成本的降低使得研究和分析更加广泛。而在基因组学的每一个应用背后,都有一系列计算密…

入驻长沙!全球数据资产理事会长江中游中心挂牌成立

在全球数据资产化浪潮的推动下&#xff0c;长江中游地区迎来了其在数字经济领域的重要里程碑。 近日&#xff0c;《数据资产长江中游生态论坛暨数据资产入表和价值转化研讨会》于长沙圆满落幕&#xff0c;会上各产业专家、企业高管&#xff0c;围绕数据产品开发、数据资产融资…

ICC2:如何检查input floating

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f; 拾陆楼知识星球入口 input floating分为两种情况&#xff1a; input没接net的情况: get_flat_pins -f "direction &#xff1d;&#xff1d;in && undefined(net)" input接net…

数据结构:链表相关题目

链表反转 LeetCode地址&#xff1a;LCR 024. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 头插法&#xff1a; class Solution {public ListNode reverseList(ListNode head) {ListNode h1 new ListNode(-1);while(head!null){ListNode index new ListNode(head.val…

SQL职场必备:掌握数据库技能提升职场竞争力

&#x1f482; 个人网站:【 摸鱼游戏】【网址导航】【神级代码资源网站】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

顺序结构 ( 五 ) —— 数据输入输出 【互三互三】

文章目录 &#x1f341;序 &#x1f341;一、字符输入函数getchar &#x1f341;二、字符输出函数putchar &#x1f341;三、通过cout流输出数据 &#x1f341;四、通过cin流读入数据 &#x1f341;五、格式化输入函数scanf &#x1f341;六、格式化输出函数printf &…

【每日一练】python面对对象的基本概念和用法(附实例)

面向对象编程&#xff08;OOP&#xff09;是一种程序设计方法&#xff0c;其基本概念包括对象、类、继承和封装。 对象&#xff1a;对象是系统中的基本单位&#xff0c;用于描述客观事物。每个对象包含一组属性和对这些属性进行操作的方法。对象是类的一个实例&#xff0c;具有…

高通开发系列 - LCD屏调试过程中的一些技巧

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 返回:专栏总目录 目录 打开开发者模式wifi adb shellmodetest测试使用异显CPU和GPU加速开性能模式进程绑定大核CPU设置和查询进程状态视频…