【C++】面向对象三大特性之—— 多态(从底层带你理解多态)

目录

前言

什么是多态

多态的定义及实现

虚函数

虚函数的重写

多态的构成条件

虚函数重写的两个例外

协变

析构函数的重写(重要!!!)

override 和 final(了解)

override

final

重载、重写、隐藏

抽象类

介绍

格式

抽象类实战

多态原理

虚函数表

多态原理

动态绑定 与 静态绑定

多态拓展

结语


前言

多态的学习,对继承的掌握有着一定要求,特别是切割赋值,继承后默认成员函数中的析构函数的相关问题

如果有想复习或者学习一下继承相关知识点的友友,可以看一看下面的这篇继承博客╰(*°▽°*)╯

【C++】面向对象三大特性之—— 继承 | 详解

什么是多态

现实世界中有很多例子都属于多态的范畴

比如,小猫叫是喵喵、小狗叫是汪汪;再比如备胎给女生发消息,和男神给女生发消息,结果是不一样的

所以我们由此可以引出多态的概念:多态就是不同的对象,做同一件事,结果不同

你看猫和狗,都是叫,一个是喵喵,一个是汪汪,结果不同

你看备胎和crush,都是发消息,一个是“哦”,一个是 ”你真贴心“,结果也不一样

综上两种,都可以称之为多态行为

多态的定义及实现

我们下文就以买票为例吧:

普通人买票是全票学生买票是打折军人买票是优先

虚函数

要实现多态,虚函数的了解是必须的,我们先来了解一下什么是虚函数

其实虚函数就是在普通的成员函数前面加一个virtual,如下:

class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};

我们可以看到,就是在成员函数前面加上一个virtual

当然这个virtual和继承中的菱形虚拟继承使用的virtual只是重复使用的关系而已,并无直接关联

就好像是C语言中的<<是左移,但是在C++中复用这个符号变成了流提取(cout)

虚函数的重写

虚函数的重写就是,我们这个Person类是父类,然后我们在其他类继承了这个类,那么其他类中就会有这个虚函数,然后我们在其他类中再次将这个虚函数的函数体的内容重新根据不同类实现一遍而已

举个例子:

class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:virtual void buyticket(){cout << "学生——打折" << endl;}
};class soldier : public Person
{
public:virtual void buyticket(){cout << "军人——优先" << endl;}
};

虚函数的重写的注意事项!!!!

返回值、函数名、参数不能修改!!

能修改的只有函数里面的内容,相当于是拿父类那个虚函数的壳套在了这个函数上面

另外还有一个点就是:子类中的virtual可以不写,只要父类中的virtual写了,那么子类中的函数只要满足重写的其他条件,就是重写,举个例子:

class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:在这里加不加virtual都是虚函数的重写只要父类写了virtual就行void buyticket(){cout << "学生——打折" << endl;}
};

多态的构成条件

如果要构成多态的话,需要满足以下两个条件:

  1. 虚函数的重写
  2. 必须是父类的指针或引用调用

上面这两个条件,缺一不可,我们来看一个例子:

class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:virtual void buyticket(){cout << "学生——打折" << endl;}
};class soldier : public Person
{
public:virtual void buyticket(){cout << "军人——优先" << endl;}
};void func(Person& p)
{p.buyticket();
}int main()
{Person per;Student stu;soldier sol;func(per);func(stu);func(sol);return 0;
}

解释一下这串代码:首先我们最上面写了三个类,一个Person做为父类,然后Student和soldier两个子类都继承了Person类

三个类里面都有virtual,两个子类都有重写虚函数

再往下看,我们写了一个函数func,这个函数的参数是一个父类的引用

最后我们在main函数里面实例化了三个类的三个对象,都用这个func

这时我们再来回头看看,都有虚函数的重写,也有父类的引用调用(func函数),所以我们可以断定,这串代码能够构成多态,结果如下:

再来检测一下是不是一定要这两个条件:

我们试一试去掉virtual,不让他形成虚函数重写:

class Person
{
public:void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:void buyticket(){cout << "学生——打折" << endl;}
};class soldier : public Person
{
public:void buyticket(){cout << "军人——优先" << endl;}
};

在其他地方没有改变的情况下,我们不让其满足虚函数重写的条件,我们能看到,全调用父类的那个函数去了

我们再试试不用父类的引用或指针:

void func(Person p)
{p.buyticket();
}

我们能看到,结果并没有改变,还是没有实现出多态这个行为

所以我们通过实践也得出了,如果想要实现多态行为,就必须满足两个条件:

  1. 虚函数的重写
  2. 必须是父类的指针或引用调用

虚函数重写的两个例外

协变

这个协变了解就好,现实中相当鸡肋,基本用不到,所以我们只需要了解有这么个东西,不要别人写的时候我们不知道就好

首先,协变是针对虚函数重写的

我们的虚函数要满足重写,就需要函数名、参数、返回值都不能改变

但是协变就是——返回值可以不同,但是必须是父类子类的引用或指针,我们来看一个例子:

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; }
};

上面代码的意思是说:A类是一个父类,B类是一个子类(继承A类),这时我们的Person和Student两个类里面的虚函数重写,可以返回值不同

父类中的是A*(父类指针),子类中的是B*(子类指针)

析构函数的重写(重要!!!)

一般情况下,析构函数并不会出什么问题,但是我们来看这样一种情况:

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}int _a;
};class B : public A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}int _b;
};int main()
{A* ptr = new B;delete ptr;return 0;
}

上面这一串代码的意思是:我们写了一个A类和一个B类,两个类之间是继承关系

但是我们再main函数里面,我们拿A* 的指针来指向B这个类,这时候就出问题了:

我们能看到,这个析构的调用是不是不太对啊,我们要两个类的析构都调用,但是他这就只调用了一个父类的析构,大大滴不对啊

而且根据我们在继承章节所学的知识,我们其实是应该调用子类的析构,因为我们在子类是不需要显示写析构的,编译器会在析构结束后自动调用父类的析构

如果对继承相关知识点有疑惑的话,可以看一下下面这篇文章,其中默认成员函数中的析构函数就有该知识点的讲解:

【C++】面向对象三大特性之—— 继承 | 详解

这么一听,感觉有点熟悉

这里是两个类,都要调用析构,我们要的是指向B类,就调用B类的析构,指向A类的,就调用A类的析构,这可不就是多态吗

如果这里能写成多态的话,我们的问题就迎刃而解了,因为他会自己调用B类的析构

这时我们回顾一下,多态需要的两个必须的条件:

  1. 虚函数的重写
  2. 必须是父类的指针或引用调用
  A是父类,B是子类A* ptr = new B;
delete ptr;

光看代码的话,父类的指针或引用的条件是达到了,但是虚函数重写怎么办啊

虚函数的重写需要满足很苛刻的条件,一个是虚函数,一个是函数名、参数、返回值相同

当年祖师爷发现这个问题之后也是很苦恼,最后C++就发明了这么一种东西出来:

在编译完代码之后,所有的析构函数的函数名都会被改成destructer

这样的话,如果要解决上面的问题,我们就只需要在父类的析构函数后面加上一个virtual即可

因为上文提到过了,只要父类中的虚函数写了virtual,那么子类写不写其实无伤大雅,只不过最好加上,养成好习惯

综上,代码如下:

class A
{
public:A(){cout << "A()" << endl;}///父类析构函数加上virtual,构成虚函数重写virtual ~A(){cout << "~A()" << endl;}int _a;
};class B : public A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}int _b;
};int main()
{main函数里面有父类的指针调用上面又有虚函数的重写,综上,构成多态指向子类,会调用子类的析构并且子类的析构会在调用完之后自动调用父类的析构(这是继承的知识点)A* ptr = new B;delete ptr;return 0;
}

override 和 final(了解)

override

这是C++11新添加的两个小语法

override是一个判断是否重写了的小语法,举个例子:

class A
{
public:virtual void Print(){cout << "支持博主" << endl;}
};class B : public A 
{
public:加上了override,判断是否重写virtual void Print()override{cout << "一键三连" << endl;}
};void func(A& ref)
{ref.Print();
}int main()
{A a;B b;func(a);func(b);return 0;
}

我们简单地实现了一个多态,但是我们在虚函数的后面加上了一个override判断

这时候就是正常调用

如果我们试着不满足虚函数重写的条件呢?

class A
{
public:void Print(){cout << "支持博主" << endl;}
};class B : public A 
{
public:void Print()override{cout << "一键三连" << endl;}
};

编译器就会报错,说没有完成重写

final

final这个语法就是:

  1. 修饰虚函数,表示这个虚函数不想被重写
  2. 修饰类,表示这个类不像被继承
class A
{
public://看这里virtual void Print()final{cout << "支持博主" << endl;}
};class B : public A 
{
public:virtual void Print(){cout << "一键三连" << endl;}
};void func(A& ref)
{ref.Print();
}int main()
{A a;B b;func(a);func(b);return 0;
}

编译器就会报错,这是虚函数不能重写的情况

///看这里
class A final
{
public:virtual void Print(){cout << "支持博主" << endl;}
};class B : public A 
{
public:virtual void Print(){cout << "一键三连" << endl;}
};void func(A& ref)
{ref.Print();
}int main()
{A a;B b;func(a);func(b);return 0;
}

这是不能被继承的情况

重载、重写、隐藏

这三个其实很好区分

首先是重载,重载的话有一个要求,就是两个函数想要重载就必须在同一个域里面,并且参数不相同,编译器会在后面根据参数的不同起不同的函数名,然后找地址调用

再来看看重写和隐藏

这两个其实是一个包含关系,隐藏里面包着重写

试想一下,隐藏,就只需要父类子类成员函数或成员变量名字相同,就会构成隐藏

但是重写却不仅仅要求名字相同,还有返回值,参数都相同才是,且必须是虚函数

所以我们可以这么界定这两个:

  1. 重写是一种特殊的隐藏
  2. 父类子类的同名函数,只要不构成重写的,就是隐藏

抽象类

介绍

抽象类是什么呢,试想一下:车,并不是一种具体的东西,这是一类东西

车里面有奔驰,宝马,法拉利等等,但是车,这是一个类型,是抽象出来的一个概念

所以,假设我们写了一个类,叫做car(车),但是车并不是一个具体的东西,只是一个类而已

所以我们就能将这个类设成一个抽象类,让其他的类来继承这个抽象类

格式

如果想要整出一个抽象类的话,我们只需要在虚函数后面加上一个  =0  即可

如下:

class car
{
public:virtual void comment() = 0;
};

注意,抽象类不能被实例化,所以我们继承了抽象类之后,必须要重写

而且不能直接将抽象类做为参数,只能是抽象类的引用或指针

抽象类实战

接下来我们简单拿抽象类实现一个多态,代码如下:

class car
{
public:virtual void comment() = 0;
};class benz : public car
{
public:virtual void comment(){cout << "奔驰" << endl;}
};class ferrari : public car
{
public:virtual void comment(){cout << "法拉利" << endl;}
};void func(car& c)
{c.comment();
}int main()
{benz be;ferrari fe;func(be);func(fe);return 0;
}

多态原理

虚函数表

在了解多态原理之前,虚函数表的了解是必不可少的

首先,我们写的所有虚函数,都会被放在虚函数表里面

可以这么理解,我们的对象里面有一个指针,这个指针指向一个函数指针数组,这个函数指针数组就是虚函数表

然后虚函数表里面全是虚函数的地址,这些虚函数和普通函数一样,都存在代码段那里

所以,如果没必要的话,不要什么函数都设成虚函数,因为这样子会增加编译器的负担,没有这个必要

重点来了!!

派生类继承了虚函数之后,会将虚函数的内容拷贝一份下来放进派生类的虚表里面

注意,不是同一个虚表,子类有自己的虚表

然后,当我们将虚函数重写了之后,重写的虚函数,就会将子类虚表中对应的虚函数给覆盖

多态原理

多态的原理就是,子类和父类的虚表是不一样的

当我们将虚函数重写了之后,重写后的虚函数会将原本子类中的那个虚函数给覆盖掉

而为什么要求要使用父类的指针呢?

回忆一下,我们在继承的时候提到过,当我们使用父类的指针或引用,指向或引用的对象是子类的时候,这个指针或引用,指向或引用的就是子类中父类的那一部分

这时我们再回头来看一下,虚表,也在引用的范围内(C++特殊规定)

而我们重写了之后的虚函数已经和原本父类的虚函数不一样了,这时再去拿父类指针或引用调用这个虚函数的时候,就能够实现多态的效果了

我们来画个图理解理解:

我们再来写一个例子:

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}virtual void func2(){cout << "A::func2()" << endl;}void func3(){cout << "A::func3()" << endl;}
};class B : public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}
};void show(A& _a)
{_a.func1();_a.func2();_a.func3();
}int main()
{A a;B b;show(a);cout << endl << endl;show(b);return 0;
}

看我们上面的代码,我们写了两个虚函数在父类里面

但是我们之将func1重写了,func2没有重写

这时,func2就不会被覆盖,func1就会被覆盖,所以当我们调用的时候,func2因为没有重写所以其实还是父类中的那个函数,func1因为被重写了,所以就实现了多态

动态绑定 与 静态绑定

首先我们来回想一下,我们的多态,是在程序运行了之后,根据不同对象的虚表去找的函数,所以这其实是运行时绑定,也叫动态绑定

与之相反的是静态绑定,我们的普通成员函数,因为没有存进虚表里面,就是一个普通的函数,所以就会在编译的时候,将这个函数编译成一个代码段,根据这个代码段我们就能找到函数,所以这是在编译的时候就绑定的,也叫静态绑定

多态拓展

其实,不止是我们今天学的这些知识叫做多态,这太局限了

只是相对于C++来说,有这么一个章节的内容,叫做多态

其实在C++中,在现实生活中,很多场景我们都能叫其为多态

比如函数重载:我们看起来是调用了同一个函数,传了不同的参数进去之后,给到了不同的结果

虽然底层是函数重载,但光从表象上看,这就是多态行为

再比如这个:

int main()
{int a = 0, b = 1;char x = 'x', y = 'y';swap(a, b);swap(x, y);return 0;
}

能看到,我们都是调用swap这个函数,但是我们传的不同的参数,给出了不同的结果,这就是一种多态行为

聊完了C++的,再来聊一聊现实世界的

就好比我们开头举的例子:

crush和备胎给女生发消息,同样是发消息,得到的结果不同

同样是第一次做错事,美女和普通人得到的结果也不同

多态多态,就是多种状态,同一种行为的不同状态,不同人的同一种行为的不同状态

当我们将思路打开,理解透了多态的本质之后,即使现在学的忘记了,以后回来看一眼也就秒懂了

结语

看到这里,这篇博客有关多态的相关内容就讲完啦~( ̄▽ ̄)~*

如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)

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

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

相关文章

linux 查看端口占用并处理

lsof 命令 lsof -i:端口注意pid netstat 命令 netstat -tnpla | grep 端口注意pid 查看详情 ps -ef | grep 3766607删除 kill -9 PIDkill -9 3766607

【整数规划】+【0—1规划】解决优化类问题(Matlab代码)

目录 文章目录 前言 一、整数规划 分类&#xff1a; 二、典例讲解 1.背包问题 2.指派问题 总结 前言 如果觉得本篇文章还不错的话&#xff0c;给作者点个赞鼓励一下吧&#x1f601;&#x1f601;&#x1f601; 在规划问题中&#xff0c;有些最优解可能是分数或小数&am…

SpringBoot+Vue3+SSE实现实时消息语音播报

目录 1、前言 2、什么是SSE 2.1、与WebSocket有什么异同&#xff1f; 3、代码实现 3.1、前置代码 3.2、SSE相关代码 3.3、消息类相关代码 3.4 、前端代码 4、实机演示 1、前言 有这样一个业务场景&#xff0c;比如有一个后台管理系统&#xff0c;用来监听订单的更新&…

【NUCLEO-G071RB】010——TIM6-基本定时器

NUCLEO-G071RB&#xff1a;010——TIM6-基本定时器 基本定时器设计目标芯片配置程序修改运行测试 基本定时器 基本定时器只能用于计时&#xff0c;可以配置有无上溢出中断&#xff0c;它基本到不支持下溢出中断。它的时钟源&#xff08;应该&#xff09;是TPCLK&#xff0c;内…

ChatGPT首次被植入人类大脑:帮助残障人士开启对话

马斯克在脑机接口中最强大的竞争对手Synchron有了新的技术进展&#xff0c;他们首次将ChatGPT整合到其脑机系统中&#xff0c;以使瘫痪患者更容易控制他们的数字设备。Synchron凭借其独特的脑机接口&#xff08;BCI&#xff09;技术脱颖而出&#xff0c;该技术巧妙地运用了成熟…

【npm】如何将自己的插件发布到npm上

前言 简单说下 npm 是什么&#xff1a; npm 是一个 node 模块管理工具&#xff0c;也是全球最大的共享源。 npm 工具与 nodejs 配套发布&#xff0c;便利开发人员共享代码。npm 主要包括 npm 官方网站、CLI&#xff08;控制台命令行工具&#xff09;、和 registry&#xff08;…

「Pytorch」BF16 Mixed Precision Training

在深度学习领域&#xff0c;神经网络的训练性能瓶颈常常出现在 GPU显存的使用上。主要表现为两方面&#xff1a; 单卡上可容纳的模型和数据量有限&#xff1b;显存与计算单元之间的带宽和延迟限制了运算速度&#xff1b; 为了解决显卡瓶颈的问题&#xff0c;涌现了不同的解决…

Arduino控制带编码器的直流电机速度

Arduino DC Motor Speed Control with Encoder, Arduino DC Motor Encoder 作者 How to control dc motor with encoder:DC Motor with Encoder Arduino, Circuit Diagram:Driving the Motor with Encoder and Arduino:Control DC motor using Encoder feedback loop: How …

深度学习碎碎念——碎片知识1

1、什么叫模型收敛&#xff1f;什么叫模型欠拟合和过拟合&#xff1f; 什么叫模型收敛&#xff1f;——模型收敛是指在训练过程中&#xff0c;模型的损失函数逐渐减小并且趋于稳定的状态。简而言之&#xff0c;当模型的训练过程达到一个稳定的点&#xff0c;使得进一步的训练不…

CV党福音:YOLOv8实现语义分割(一)

前面我们得知YOLOv8不但可以实现目标检测任务&#xff0c;还包揽了分类、分割、姿态估计等计算机视觉任务。在上一篇博文中&#xff0c;博主已经介绍了YOLOv8如何实现分类&#xff0c;在这篇博文里&#xff0c;博主将介绍其如何将语义分割给收入囊中。 YOLOv8语义分割架构图 …

【C++】特殊类的设计与类型转换

文章目录 1. 特殊类的设计1.1 不能被拷贝的类1.2 只能在堆上创建对象的类1.3 只能在栈上创建对象的类1.4 不能被继承的类1.5 只能创建一个对象的类&#xff08;单列模式&#xff09; 2. 类型转换2.1 C/C的类型转换2.2 C规定的四种类型转换2.2.1 static_cast2.2.2 reinterpret_c…

【吊打面试官系列-Elasticsearch面试题】对于 GC 方面,在使用 Elasticsearch 时要注意什么?

大家好&#xff0c;我是锋哥。今天分享关于 【对于 GC 方面&#xff0c;在使用 Elasticsearch 时要注意什么&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 对于 GC 方面&#xff0c;在使用 Elasticsearch 时要注意什么&#xff1f; 1、SEE 2、倒排词典的索引需…

vue3使用pnpm运行项目但是运行不起来

运行项目的时候发现根本运行不起来了 尝试过创建.npmr文件 删除node_modules重新下 但是都出现问题了 创建.npmr&#xff1a;不管用 删除node_modules重新下&#xff1a;文字编译乱码&#xff0c;utf-8可能解析处理问题 最后解决方法&#xff1a; 重新创建项目&#xff0…

网络科技公司官网电商软件开发小程序网站pbootcms模板带手机端

免费授权可商用网站模板 PC端移动端后台测试数据 所有页面均都能完全自定义标题/关键词/描述&#xff0c;PHP程序&#xff0c;安全、稳定、快速&#xff0c;响应式同一个后台&#xff0c;数据即时同步&#xff0c;简单适用&#xff0c;附带测试数据&#xff01;&#xff01;

物流仓库安全视频智能管理方案:构建全方位、高效能的防护体系

一、背景分析 随着物流行业的快速发展和仓储需求的日益增长&#xff0c;仓库安全成为企业运营中不可忽视的重要环节。传统的人工监控方式不仅效率低下&#xff0c;且难以做到全天候、无死角覆盖&#xff0c;给仓库资产和人员安全带来潜在风险。因此&#xff0c;引入仓库安全视…

了解细胞外基质:它是啥?有啥作用?

了解细胞外基质&#xff1a;它是啥&#xff1f;有啥作用&#xff1f; 大家好&#xff0c;今天我们来阅读这篇Biofabrication methods for reconstructing extracellular matrix mimetics发表于《Bioactive Materials》上的文章。细胞外基质在人体中起着至关重要的作用&#xff…

同城门户同城分类信息网站源码discuz插件+pc端+小程序端+49款插件

同城分类信息 同城好店 同城合伙人 同城招聘 同城卡 同城活动 同城优惠抢购 同城商城 同城头条 同城抽奖 同城拼团 同城砍价 同城电话本 同城认证 同城签到 同城拼车 同城红包 同城子站点 同城相亲 同城交友 同城小程序 比较流行的同城信息门户网站源码&#xff0c;基于dz&…

【计算机网络】网络基础概念

目录 计算机网络发展 协议 协议分层 OSI 七层模型 TCP/IP 五层&#xff08;四层&#xff09;模型 究竟什么是协议&#xff1f; 网络与操作系统的关系 网络传输基本流程 局域网网络传输流程 认识 MAC 地址 局域网&#xff08;以太网为例&#xff09;通信原理 数据包…

【前端设计方案】H5 图片懒加载 SDK

实现思路 定义<img srcloading.png data-srcxxx.png/>页面滚动&#xff0c;图片露出时&#xff0c;将 data-src 赋值给 src 注意事项&#xff1a;滚动要节流 技术要点 获取图片的位置 elem.getBoundingClientRect() 图片 top < window.innerHeight 时&#xff0c;图片…

Install pytorch 使用 torch 的例子

如果不知道怎么开始和安装软件 从这里开始 如果需要GPU版本&#xff0c;请选择CUDA&#xff0c;而不是CPU PyTorchhttps://pytorch.org/ Python 3.8.13 | packaged by conda-forge | (default, Mar 25 2022, 06:04:10) [GCC 10.3.0] on linux Type "help", &quo…