C++进阶-->多态(Polymorphism)

1. 多态的概念

多态,顾名思义多种形态;多态分为编译时多态静态多态)和运行时多态动态多态),静态多态就是就是我们前面讲的函数重载和函数模板,可以通过传不同类型,然后在编译期间就确定好使用哪个的称为静态多态动态多态就是一般在程序运行时确定使用指定函数的就称作动态多态。

动态多态,举个生活中的例子,例如铁路12306中买票的时候,当成年且没有特殊情况的人是全价买票,学生买票有特定的学生价买票,军人买票时可以优先买票,而这个过程抽象来说就是传入不同的对象就会完成不同的行为。


2. 多态的定义及使用

2.1 多态的构成条件

多态是一个继承关系下的类对象,去调用同一函数,产生了不同行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态的两个必要条件

1. 必须是基类的指针或引用调用函数。

2. 被调用的函数必须是虚函数(virtual修饰的)。

解释一下为什么必须是基类的指针或引用调用函数,因为只有是基类的指针或引用才能做到既接收基类的地址又接收派生类的地址;

第二派生类必须对基类的虚函数进行重写/覆盖,派生类对基类的虚函数重写了才能做到传入不同的类执行不同的操作的效果,才能达到多态。f9e5263f8b84403fbc9bb0754efb1f39.png


3. 虚函数

在继承那一章我们也讲到虚继承,虚继承是用来避免菱形继承带来代码的冗余和二义性的问题,关键字是virtual;虚函数的关键字也是virtual,只需要在类成员函数前面加上virtual修饰,那么这个成员函数就被称为虚函数。注意:如果不是成员函数是不能加virtual修饰的。如下代码所示:

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

3.1 虚函数的重写

派生类中有一个虚函数跟基类的虚函数完全一致的虚函数称为虚函数的重写。

完全一致指的是:函数名、参数列表、返回值类型。(注意!这里的参数列表指的是参数的类型,名字相同没有关系);如下代码所示:

class Person
{
public:virtual void BuyTicket(int x){cout << "买票--全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(int y){cout << "买票--半价" << endl;}
};

还有一点需要提一下的是:如果基类的虚函数加了virtual,那么派生类的虚函数可以不加virtual,这里我们可以直接按“在继承的时候就把基类的虚函数的属性继承下来了”理解,这个不太建议这样子用,但在笔试或者考试选择题可能会考。

下面是虚函数、虚函数的重写、实现多态的代码使用:

#include<iostream>
using namespace std;class Person
{
public:virtual void BuyTicket(int x){cout << "买票--全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(int y){cout << "买票--半价" << endl;}
};void BuyTicket(Person* ptr)
{ptr->BuyTicket(6);
}int main()
{Person p1;Student s1;BuyTicket(&p1);BuyTicket(&s1);return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:43498d0f6b634b0c906997d2785f7a99.png

最后再补充一个重重重重要的知识:虚函数的重写本质上是重写虚函数的实现!!!!下面就有一道相关的练习题。(毕竟人教人教不会,事教人一脚就懂)

3.2 多态场景下的选择题

f41ecc81bc1a48a4baec16641cf16cfd.png

#include<iostream>
using namespace std;class A
{
public:virtual void func(int val = 1){std::cout << "A->" << val << std::endl; }virtual void test() {func(); }
};class B : public A
{
public:void func(int val = 0) {std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

首先如果我们对重写的知识理解的不够全面的话,那么肯定会选择D, 但我们上面说了重写是重写函数的实现,函数的参数不会有变化,所以我们调用的func应该如下图所示:9aec85ea0f714c34bea7d1a5ac267686.png

所以正确的答案是B;37a6353d609e4e4687881f12f853fab7.png

最后我们再来细致分析一下p->test()究竟怎么构成多态,我们都知道在类里面的成员函数会有一个隐藏的参数就是this指针,而test是在A里面的,所以完整的函数应该为   virtual void test(A* this)这里就构成重载了,基类做this指针的返回值,完全符合上面所说的2个多态的构成条件。35fd32c94cc64bd388e7ac5a63ce8e6e.png


3.3 虚函数重写的一些问题

3.3.1 协变

协变:派生类重写基类的虚函数的时候返回值不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用,这个就成为协变。协变用处不多,仅需了解一下即可;如下代码所示:

#include<iostream>
using namespace std;
//基类
class A
{};
//派生类
class B :public A
{};class Person
{
public://基类做返回值virtual A* BuyTicket()//virtual Person* BuyTicket()也可以{cout << "买票-全价" << endl;return nullptr;}
};class Student :public Person
{
public://派生类做返回值virtual B* BuyTicket()//virtual Student* BuyTicket()也可以{cout << "买票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}

3.3.2 析构函数的重写

基类的析构函数只要定义为虚函数后,无论派生类的析构函数写不写virtual,都会与基类的析构函数构成重写。

前面继承有提到一嘴,就是析构函数最后都会被编译器转换为destructor(),那么这时候函数名(destructor)相同构成隐藏(这个是继承那边的)。

所以只要基类的析构加了virtual,派生类的就构成了重写;函数名一样都是destructor、参数一样都没有、返回值类型都没有。

接下来就谈谈为什么要实现析构函数的重写(重点!!!!!)

在思考为什么的时候我们就可以通过反推来证明即如果没有实现会发生什么情况;我们先看一下下面的代码:

#include<iostream>
using namespace std;class A
{
public:~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指针实例化了两个对象p1接收基类A的地址p2接收派生类B的地址,如果说我们没有实现析构函数的重写delete p1和delete p2 是直接走基类的析构函数,因为没有构成多态,也就不会产生不同的行为而只走基类A的析构函数,如果派生类B里面有动态申请空间,那么就会出现内存泄漏。如下图:

b738ad185b9145b29fe5b984044b249e.png

但如果我们让基类A的析构函数定义为虚函数,那么就构成多态,从而会根据传入不同的类型调用不同对象的析构函数。a43413a6925144f48d65349edfdb6835.png

这里为什么会还会调用一次基类A的析构函数呢?前面继承有提到,派生类的析构函数里面其实包含了基类的析构函数,主要是为了达到先子后父的析构顺序。

最后总结一下:C++的这些实现其实都是有关联的,他们为了处理上面的这种情况而实现了析构函数的重写,为了实现析构函数的重写而实现了让析构函数无论函数名为什么最后都会转换成destructor。真的很妙。


3.3.3 override和final关键字

override是用来检查你是否重写错误;d296969260e3403eaa4fcd1eafc3b2d3.png

final在继承那里也提到过,如果想实现一个类不被继承就用final,而这里的final是用来实现一个不能被重写的虚函数;981713e9edb04bc6beba32a0d8b57046.png


3.3.4 重载/重写/隐藏的对比032749fad3564b7eb4431220e422b9c3.png


3.3.5 纯虚函数和抽象类

纯虚函数只要在虚函数后面加个“=0”即可;纯虚函数不需要定义实现,没有很大作用,但是在语法上是支持实现的 。而包含纯虚函数的类称作抽象类,抽象类不能实例化出对象,但是派生类可以继承抽象类,如果派生类不重写纯虚函数,那么派生类也称为抽象类。纯虚函数其实就是间接强制了派生类要重写纯虚函数。

5e07f16386174a2fa66529fcfdc7041f.png

干说有点难理解,举个实际例子,狗和猫都是动物,狗的叫声是“旺旺”,猫的叫声是“喵喵”,狗和猫都是动物都有动物的特征所以可以继承动物的属性,但是动物是怎么叫的我们不知道,而动物就可以称作是抽象类,“叫”这一个动作可以用纯虚函数实现,猫和狗则是动物的派生类,所以需要重写这个纯虚函数,狗重写“叫”函数为“旺旺”,猫则是“喵喵”。如下代码所示:

#include<iostream>class Animal
{
public:virtual void talk() = 0;
};
class Dog : public Animal
{
public:virtual void talk() {std::cout << "汪汪" << std::endl;}
};class Cat : public Animal
{
public:virtual void talk() {std::cout << "(>^ω^<)喵" << std::endl;}
};
int main()
{Cat cat;Dog dog;cat.talk();dog.talk();return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:e0ee26f05fa64138975a82706ddfab98.png


4. 多态的原理

首先我们看一道题,用一道题引出下面的知识;67df6fb7104b4e8bb40023382fc2472b.png

#include<iostream>
using namespace std;class Base {
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}

我们知道,计算类的大小是使用对齐的方式计算的,那么计算出来的结果是8,可能我们的答案就选择了C,实则并不然,答案选的是D,首先我们通过调试看一下里面究竟多了什么玩意,如下图所示:

57f345d60a264bb895208fccb07578ff.png

我们发现里面除了成员变量_b和_char,还有一个指针_vfptr,这个指针我们称作虚函数表指针(virtual function pointer);

4.1 虚函数表指针

每个含有虚函数的类中都会有一个虚函数表指针,这个指针是指向一块存着虚函数地址的空间,虚函数表也称作虚表。所以我们传入不同类去调用该类的虚函数的时候就是通过这个指针去调用不同类中的虚函数的。

4.2 多态的实现

首先我们先来看一串代码

#include<iostream>
using namespace std;
class Person {
public:virtual void BuyTicket() {cout << "买票-全价" << endl;}
protected:string _name;
};class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl;}protected:int _id;
};class Soldier : public Person 
{
public:virtual void BuyTicket() { cout << "买票-优先" << endl;}protected:string _codename; // 代号
};void Func(Person* ptr)
{// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket// 但是跟ptr没关系,而是由ptr指向的对象决定的。ptr->BuyTicket();
}int main()
{// 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后// 多态也会发生在多个派生类之间。Student st;Soldier sr;Person pr;Func(&st);Func(&sr);Func(&pr);return 0;
}

我们可能会好奇,Func内的ptr是如何做到传入不同的类对象的地址然后走不同的虚函数执行不同操作的,下面就来讲解一下。

如下图

c80ba045e6684b748455df3ea1d7af28.png

3d5f3d6949ee41ee8ba6fe88b1fa0d49.png

通过上图我们看到,满足多态后,不再是编译时通过调用对象确定函数的地址,而是运行时到指定的对象内找到对象的__vfptr(虚表)然后确定对应的虚函数的地址,然后执行,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。


4.3 动态绑定和静态绑定

动态绑定则是满足多态条件的函数调用是在运行时绑定,说简单点就是在运行时去虚函数表内找到函数地址然后调用。

静态绑定则是在编译期间确定函数的地址然后调用。

这个可以通过汇编层看一下。

动态绑定👇看着挺复杂的,挺多操作,因为他是在运行时先去找到__vptr这个指针,然后找到虚函数表,再通过虚函数表内的地址找到虚函数,再调用该函数。

47759162ddf74cc9b7f69f69453ca4ae.png

静态绑定👇编译器直接确定函数地址然后直接调用。

2c61f3db7b3143b3b816dde418ee1fc8.png


4.4 虚函数表

1. 基类对象的虚函数表中存放基类所有虚函数的地址。如果同类型实例化对象的话则虚表共用,不同类型虚表各自独立。

2. 派生类由两部分构成,一部分是派生类自己的成员,还有一部分是基类的成员,还有基类的虚表指针,继承下来的基类中有虚表指针的话就不会自己再生成一个新的虚表指针,但继承下来的和虚表指针和基类的虚表指针不是同一个,他们的地址不同。d37324b29ea1407ab366b220ec5964b7.png

3. 如果派生类中有虚函数的重写,那派生类虚表中对应的虚函数就会被重写的虚函数覆盖。

4. 派生类的虚表中包含基类的虚函数地址,派生类重写的虚函数地址,还有派生类自己的虚函数地址。

5. 虚函数的本质是一个存虚函数指针的指针数组,一般情况下这个数组后面会放一个0x00000000的标记   (这个C++并没有进⾏规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000 标记,g++系列编译不会放)e1d4a3f479be41a6b0fb55b483fb441e.png

6. 虚函数和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址存在虚表中。

7. 虚函数表的存在位置是看编译器的,C++并没有规定,下面将来看一下vs的在哪里;如下代码所示:

#include<iostream>
using namespace std;class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};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);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:c838968a73ae41c4b2a62c55ed66fdc4.png

由此可见,虚表的地址与常量区的地址很接近,那就说明虚表是存在常量区的。


5. 额外补充的知识

首先我们先来看一个代码。

#include<iostream>
using namespace std;class A 
{ 
public: void test(float a) {cout << a; } 
}; class B :public A 
{
public: void test(int b) {cout << b; } 
}; void main() 
{A* a = new A;B* b = new B;a = b; a->test(1.1); 
}

按照我们的思路,a赋值给b之后,那么A的test的隐式指针this就应该转换成B类的this,应该调用的是cout出b的值为1。

但是编译器不是这么做的,编译器虽然通过指针的指向访问成员变量,但是不能通过指针的指向访问成员函数,而是通过指针的类型来访问成员函数。也就是说无论a的指针指向了谁,都只能访问到A类内的成员函数。(引用也是一样的!!!!!)


派生类有几个父类,且那几个父类都有虚函数的话,那么派生类就会有几张虚表。而如果在派生类中增加虚函数的话只会放在第一张虚表的最后。

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

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

相关文章

虚拟机桥接模式连不上,无法进行SSH等远程操作

说明&#xff1a;以下情况在window10上遇到&#xff0c;解决后顺便做了个笔记&#xff0c;以防后续再次用到&#xff0c;也给同道中人提供一个解决方案 一、首先按照以下步骤进行检查 1、是否连接了对应的wifi 2、是否设置了桥接模式 3、上述1、2确认无误的情况下请查看右上…

Flutter Image和Text图文组件实战案例

In this section, we’ll go through the process of building a user interface that showcases a product using the Text and Image widgets. We’ll follow Flutter’s best practices to ensure a clean and effective UI structure. 在本节中&#xff0c;我们将使用“Te…

使用Kubernetes管理容器化应用

使用Kubernetes管理容器化应用 Kubernetes简介 安装Kubernetes 安装Minikube 启动Minikube集群 创建一个简单的Web应用 创建项目目录 初始化项目 安装Node.js依赖 创建Docker镜像 编写Dockerfile 构建并推送Docker镜像 创建Kubernetes配置文件 创建Deployment 创建Service …

<十六>Ceph mon 运维

Ceph 集群有故障了&#xff0c;你执行的第一个运维命令是什么&#xff1f; 我猜测是ceph -s 。无论执行的第一个命令是什么&#xff0c;都肯定是先检查Mon。 在开始之前我们有必要介绍下Paxos协议&#xff0c;毕竟Mon就是靠它来实现数据唯一性。 一&#xff1a; Paxos 协议 1…

计算机网络-MSTP的基础概念

前面我们大致了解了MSTP的由来&#xff0c;是为了解决STP/RSTP只有一根生成树导致的VLAN流量负载分担与次优路径问题&#xff0c;了解MSTP采用实例映射VLAN的方式实现多实例生成树&#xff0c;MSTP有很多的理论概念需要知道&#xff0c;其实与其它的知识一样理论复杂配置还好的…

电源完整性

电源分配系统 电源分配系统:Power Distribution Network(简称 PDN) 真正用电节点在 Die&#xff0c;所以PDN系统包含 PCB 和 Package上的部分 PCB 上:VRM、大电容、小电容、电源平面、地平面 Package内:电容、电源平面、地平面 电源噪声的产生 稳压电源芯片本身的输出不恒定&a…

基于SpringBoot云养鸡互动平台的设计与实现

前言 对于当今社会的人们来说&#xff0c;互联网技术是必不可少的&#xff0c;随着经济和技术的不断发展&#xff0c;计算机已经深入到各个领域。云养鸡互动平台将人们的时间需求与计算机技术结合起来&#xff0c;架起一座桥梁&#xff0c;使云养鸡互动更加方便快捷。云养鸡互…

枫清科技仲光庆:AI+行业新范式,双轮驱动助力数智化升级

10月23日&#xff0c;由财视传媒主办&#xff0c;未来图灵、尚品新消费、未来企业家俱乐部、传播达人汇联合主办的“赢在大模型时代”2024未来发布论坛暨“向上拾年”财视十周年盛典在北京举行。枫清科技联合创始人兼 CPO 仲光庆应邀出席并分享了“AI行业落地新范式”&#xff…

谷歌云GCP基础概念讲解

概览 云的基础是虚拟化&#xff1a;服务器&#xff0c;存储&#xff0c;网络。服务器是远程计算机的逻辑分区。存储是物理硬盘的逻辑划分。网络则是虚拟私有云。 谷歌是唯一一个拥有全球私有基础设施的公司&#xff1b;他们的谷歌云基础设施没有任何一部分通过公共互联网。换句…

【ACM出版,EI稳定检索,九大高校联合举办, IEEE Fellow支持】2024年计算机视觉与艺术研讨会(CVA 2024)

在线投稿&#xff1a;学术会议-学术交流征稿-学术会议在线-艾思科蓝 2024年计算机视觉与艺术国际学术会议&#xff08;CVA 2024&#xff09;作为2024年人工智能、数字媒体技术与交互设计国际学术会议&#xff08;ICADI 2024)的分会。此次大会旨在汇聚全球在计算机视觉与艺术…

Java知识巩固(十二)

I/O JavaIO流了解吗&#xff1f; IO 即 Input/Output&#xff0c;输入和输出。数据输入到计算机内存的过程即输入&#xff0c;反之输出到外部存储&#xff08;比如数据库&#xff0c;文件&#xff0c;远程主机&#xff09;的过程即输出。数据传输过程类似于水流&#xff0c;因…

基于SSM+小程序的智慧旅游平台登录管理系统(旅游2)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 旅游平台开发微信小程序功能有管理员和用户。 1、管理员功能有个人中心&#xff0c;用户管理&#xff0c;景点分类管理&#xff0c;旅游景点管理&#xff0c;景点购票管理&#xff0c;景…

我谈Canny算子

在Canny算子的论文中&#xff0c;提出了好的边缘检测算子应满足三点&#xff1a;①检测错误率低——尽可能多地查找出图像中的实际边缘&#xff0c;边缘的误检率&#xff08;将边缘识别为非边缘&#xff09;低&#xff0c;且避免噪声产生虚假边缘&#xff08;将非边缘识别为边缘…

量子容错计算

基本思想 容错量子计算的基本想法是&#xff0c;在合理编码后的量子态上直接量子计算&#xff0c;以至于不完全需要解码操作。假设有一个简单的量子电路&#xff0c;但不幸的是噪声影响着这个电路的每一个元件&#xff0c;包括量子态的制备、量子逻辑门、对输出的测量&#x…

代码随想录算法训练营第十一天(补) 栈与队列| 后序表达式、滑动窗口、高频元素、链表总结

目录 一、150. 逆波兰表达式求值 二、239. 滑动窗口最大值 三、347.前 K 个高频元素 四、总结 一、150. 逆波兰表达式求值 力扣题目链接(opens new window) 根据 逆波兰表示法&#xff0c;求表达式的值。 有效的运算符包括 , - , * , / 。每个运算对象可以是整数&#x…

【论文阅读】SRGAN

学习资料 论文题目&#xff1a;基于生成对抗网络的照片级单幅图像超分辨率&#xff08;Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network&#xff09;论文地址&#xff1a;https://arxiv.org/abs/1609.04802 代码&#xff1a;GitHub - x…

芯片上音频相关的验证

通常芯片设计公司&#xff08;比如QUALCOMM&#xff09;把芯片设计好后交由芯片制造商&#xff08;比如台积电&#xff09;去生产&#xff0c;俗称流片。芯片设计公司由ASIC部门负责设计芯片。ASIC设计的芯片只有经过充分的验证&#xff08;这里说的验证是FPGA&#xff08;现场…

SpringMVC实战:构建高效表述层框架

文章目录 1. SpringMVC简介和体验1.1 介绍1.2 主要作用1.3 核心组件和调用流程1.4 快速体验 2. SpringMVC接收数据2.1 访问路径设置2.2 接收参数2.2.1 param和json参数比较2.2.2 param参数接收2.2.3 路径参数接收2.2.4 json参数接收 2.3 接收cookie数据2.4 接收请求头数据2.5 原…

python爬虫实战案例——抓取B站视频,不同清晰度抓取,实现音视频合并,超详细!(内含完整代码)

文章目录 1、任务目标2、网页分析3、代码编写 1、任务目标 目标网站&#xff1a;B站视频&#xff08;https://www.bilibili.com/video/BV1se41117WP/?vd_sourcee8e376ccbc5aa4cfd88e6a7917adfd1a&#xff09;&#xff0c;用于本文测验 要求&#xff1a;抓取该网址下的视频&…

华为ICT题库-云服务部分

1651、关于创建数据盘镜像的约束条件&#xff0c;以下说法错误的是&#xff1f;&#xff08;云服务考点&#xff09; (A)使用云服务器的数据盘创建数据盘镜像时&#xff0c;要确保该云服务器必须有系统盘 (B)通过外部文件创建数据盘镜像必须明确指定操作系统类型 (C)使用云服务…