[C++]多态与虚函数

一、多态的概念

        顾名思义,多态的意思就是一个事物有多种形态,在完成某个行为的时候,当不同的对象去完成时会产生不同的状态。在面向对象方法中一般是这样表示多态的:向不同的对象发送同一条消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个不同的对象可以用自己的方法来相应同一条消息。这里的消息指的是调用函数,这里的行为就是指不同的实现,即执行不同的函数。

        举个生活中的例子,就比如说视频账号分普通用户和会员用户,当普通用户看视频时会有广告弹出,而当会员用户看视频时不会有广告弹出,不同的用户在看视频时有不同的行为(即弹窗和不弹窗),这就是多态。

二、多态的定义和实现

        为了可以表现多态的好处,先来看一个没有使用多态的例子,即Person(人),Student(学生),Graduate(研究生)。

//基类:人
class Person
{
protected:string _name; //姓名int _age;	//年龄
public:Person(const string& name,int age):_name(name),_age(age){}//显示基类的成员信息void display(){cout << _name << endl;cout << _age << endl;}
};//派生类:学生
class Student : public Person 
{
protected:string _id;//学号
public:Student(const string& name, int age, const string& id):Person(name,age),_id(id){}//显示学生类的成员信息void display(){cout << _name << endl;cout << _age << endl;cout << _id << endl;}
};//派生类:研究生
class Graduate: public Student
{
protected:string _major;//主修专业
public:Graduate(const string& name, int age, const string& id, const string& major):Student(name,age,id),_major(major){}//显示研究生类的成员信息void display(){cout << _name << endl;cout << _age << endl;cout << _id << endl;cout << _major << endl;}
};int main()
{Student s1("张三",20,"202481");Graduate g1("李四", 22, "20xxxx", "计算机与科学");//定义基类的指针对象p1和p2Person* p1;Person* p2;p1 = &s1;p2 = &g1;p1->display();cout << endl;p2->display();cout << endl;s1.display();cout << endl;g1.display();return 0;
}

运行结果:

张三
20

李四
22

张三
20
202481

李四
22
20xxxx
计算机与科学

 

        这里定义了三个类,分别是基类Person,基类的直接派生类Student,Student的直接派生类Graduate,它们的关系是单继承。类里都定义了一个同名的display函数用于显示当前类成员的所有信息,当我们在主函数中声明两个基类Person的指针分别指向学生类和研究生类的对象,然后通过基类的指针调用display函数,企图显示学生类和研究生类对象的所有数据但结果只能显示其对象中基类那部分的数据,不难想象,这是因为通过基类指针指向其派生类时,调用的函数是基类中的display函数。而如果我们要调用其派生类中的display函数以打印学生类对象s1和研究生类对象g1的所有信息,此时需要通过对象s1和g1(或者通过派生类的指针)访问display函数。但是,如果当基类的指针指向不同的派生类对象时,能通过这种方式调用同一类族中不同类的所有同名函数(这里是所有的display函数),那就好了。意思是,当基类指针指向学生类对象时,基类指针调用display函数时,能够调用到学生类对象中的display函数,当指向的是研究生类对象时,能够调用到研究生类的display函数,这就是多态。用虚函数就能解决这个问题。下面对上面代码进行一下修改,只需要在Person类中声明display函数时在函数头加上一个virtual关键字将display函数声明为虚函数即可。

对上面代码做出修改:

运行结果:

张三
20
202481

李四
22
20xxxx
计算机与科学

张三
20
202481

李四
22
20xxxx
计算机与科学
 

        这就是多态的妙处,可以通过基类指针对同一消息做出不同的行为。我们通过基类指针指向不同的对象时,调用一个具有相同函数名的函数,产生了不同的行为,这就是运行时的多态。

三、虚函数

(一)、虚函数的概念

        现在对虚函数进行一下描述。虚函数是指被virtual修饰的类成员函数。其派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与其基类的虚函数返回值类型、函数名、参数列表完全相同),那么这个派生类中的虚函数完成了对基类虚函数重写。

注意:图中画红圈的两个virtual可写可不写,但是建议都加上,为了让代码清晰明了

综上,继承中构成多态的条件有 :

1、必须通过基类的指针或引用调用虚函数。

2、被调用的函数必须是虚函数,并且派生类中要完成对虚函数的重写。

 (二)、虚函数重写的特殊情况

         上面讲到虚函数的重写的条件之一是函数名要与基类中声明的虚函数相同,但也有例外,比如说析构函数的重写,析构函数之所以能够构成虚函数重写的原因是,编译器会在编译时把析构函数的名字统一处理成destructor,这是为了让析构函数能够进行虚函数重写而做的特殊处理。可能你会疑惑,为什么析构函数也需要构成虚函数重写呢,当派生类的生命周期结束时,难道不是自动调用自身派生类的析构函数再调用其基类的析构函数吗,那为什么还要写成虚函数重写呢?来看下面这个例子。

  类A和类B中各自维护着一段动态申请的内存空间:

//析构函数重写
//基类A
class A
{
protected:int* p1 = new int;
public://析构函数~A(){cout << "~A()" << endl;delete p1;}
};//派生类B
class B: public A
{
protected:int* p2 = new int;
public://析构函数~B(){cout << "~B()" << endl;delete p2;}
};//测试样例1
void test1()
{B b;
}
//测试样例2
void test2()
{A* p1 = new B;delete p1;
}

当我们调用测试样例函数test1时,运行结果:

~B()
~A()

        这很符合我们的预期,按照先子后父的顺序调用我们的析构函数来进行内存清理的工作。

而当我们调用测试样例函数test2时,运行结果:

~A()

        当我们new了一个派生类B的对象时,用基类的指针指向其派生类的内存空间,通过delete释放p1后发现只执行了基类的析构函数,而派生类的析构函数没被执行,这就导致了派生类对象中内存泄漏的情况发生。

        这是为什么呢?因为当基类指针指向派生类对象时,通过基类指针调用某个函数时,如果该函数在其派生类中没有构成虚函数的重写,那么调用到的函数是基类中的函数。上面的例子中,delete基类指针指向的派生类的对象时,由于派生类的析构函数没有构成虚函数重写,没有构成多态,所以delete时基类指针只能调用到基类的析构函数,这就造成了内存泄漏。

        所以我们在有多态的继承体系中,十分建议所有的析构函数构成虚函数重写,这样在动态申请对象和释放对象时不会出现上述内存泄露的情况。

        下面是对代码进行的修改,将基类中的析构函数声明为虚函数,派生类中的析构函数构成虚函数重写:

//析构函数重写
//基类A
class A
{
protected:int* p1 = new int;
public://析构函数virtual ~A() //声明基类的析构函数为虚函数{cout << "~A()" << endl;delete p1;}
};//派生类B
class B: public A
{
protected:int* p2 = new int;
public://析构函数virtual ~B(){cout << "~B()" << endl;delete p2;}
};

运行结果: 

~B()
~A()

 

        这样,析构函数之间也具有多态了,通过基类指针指向不同的派生类时,当delete释放派生类对象的内存空间时就能调用到不同派生类的析构函数来正确进行资源清理的工作了。

 (三)、override和final关键字

        这两个关键字是C++11中新增的关键字。

        override用于检测派生类中的虚函数是否完成了重写,如果没有完成重写会在编译过程中报错。使用方法就是在函数后面加上即可。

        final用于修饰某个虚函数,作用是使该虚函数在该类之后的派生类中都不能够被重写。使用方法和override相同。

四、抽象类

        如果一个类有一个纯虚函数那么这个类就不能够实例化出对象,包含虚函数的类叫做抽象类(也叫接口类)。当派生类继承了抽象类时,那么这个派生类必须重写纯虚函数,否则这个派生类仍然无法实例化出对象。有时基类中将某一成员函数定义为虚函数,并不是基类的需求,而是考虑到了派生类的需求,所以,将基类作为抽象类,在基类中预留一个或多个纯虚函数,具体功能留给派生类根据需求而去定义。

        下面举一个经典的例子来体现该过程,不知道大家有咩有听过一首歌,歌名为:The Fox (What Does The Fox Say?),歌手名:Ylvis。

//抽象基类:动物
class Animal
{
public:virtual void goes() = 0; //声明该函数为纯虚函数,只需在虚函数后面加上 = 0即可
};//狗
class Dog: public Animal
{//重写纯虚函数
public:virtual void goes(){cout << "dog goes woof" << endl;//狗汪汪叫}
};//猫
class Cat : public Animal
{//重写纯虚函数
public:virtual void goes(){cout << "cat goes meow" << endl;//猫喵喵叫}
};//鸟
class Bird : public Animal
{//重写纯虚函数
public:virtual void goes(){cout << "bird goes tweet" << endl;//鸟啾啾叫}
};//....其他具体的动物类//多态调用动物的叫声
void goes(Animal& a)
{a.goes();
}int main()
{Dog d;Cat c;Bird b;//各类动物的叫声goes(d);goes(c);goes(b);//...其他动物的叫声return 0;
}

运行结果:

dog goes woof
cat goes meow
bird goes tweet

 

        在抽象基类Animal类中,因为还不知道是什么动物,自然不知道是什么叫声,调用基类的goes函数是没有意义的,所以将其goes函数声明为纯虚函数派生类继承到虚函数的接口,目的是为了在派生类中完成重写,并达成多态继承的是接口,所以叫做接口继承

将某个虚函数声明为纯虚函数的方法:

五、虚函数的使用注意事项和使用条件

注意事项:

1、只能用virtual声明类的成员函数,使其成为虚函数,而不能将类外的普通函数声明为虚函数。

2、一个成员函数被声明为虚函数后,在同一个类族中就不能定义一个非virtual的但与该虚函数具有相同的参数列表和返回值的同名函数。

使用虚函数的条件:

一般情况下,如果类中不需要将某个函数声明为虚函数,不要声明为虚函数,因为使用虚函数时,系统会有一定的空间上的开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,它是一个指针数组,存放每个虚函数的入口地址。 

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

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

相关文章

jetbrain插件市场无法下载插件/idea插件install无效

最近把电脑重装了一次系统&#xff0c;发现idea插件市场可以搜到插件&#xff0c;但是不显示overview之类的信息&#xff0c;点install也没反应。 于是打算直接到插件市场的官网plugins.jetbrains.com下载插件安装。 结果发现同样可以搜索到插件&#xff0c;但是无法下载。 在…

中国工商银行长春分行开展“工驿幸福 健康财富”长辈客群康养活动

中国工商银行长春分行作为国有大行&#xff0c;持续完善有温度、专业化、安全稳健的养老场景服务&#xff0c;以工行驿站为依托、以长辈客群养老需求为中心&#xff0c;积极对接社区构建敬老、康养的“金融泛金融”工行驿站服务生态&#xff0c;进一步提升长辈客群的到店体验。…

第十九天培训笔记

上午 1 、构建 vue 发行版本 [rootserver eleme_web]# nohup npm run serve& // 运行 vue 项目 [rootserver eleme_web]# mkdir /eleme [rootserver eleme_web]# cp -r /root/eleme_web/dist/* /eleme/ // 将项目整体 移动到 /eleme 目录下 [rootserver eleme_web]# …

Opencv threshold函数、adaptiveThreshold函数详解和示例

1.threshold函数 double cv::threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type ) src&#xff1a;待二值化的图像&#xff0c;图像只能是 CV_8U 和 CV_32F 两种数据类型。对于图像通道数目的要求与选择的二值化方法相关。dst&#xff1a;…

TypeScript 定义不同的类型(详细示例)

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

Vulnhub - JANGOW: 1.0.1 靶标实战

靶场地址&#xff1a;https://www.vulnhub.com/entry/jangow-101,754/ 靶场IP&#xff1a;192.168.56.118 信息收集 使用御剑对目标进行扫描 该靶标开启了21、80两个端口&#xff0c;21端口运行服务为ftp&#xff0c;其版本为 vsftpd 3.0.3 &#xff0c;80端口运行服务为Apa…

vscode+platformio开发小技巧

使用vscodeplatformio开发&#xff0c;具体安装配置文章很多&#xff0c;这里分享一些方便使用的小技巧&#xff0c;让使用体验在不增加学习成本的情况下更加丝滑。 1、配置依赖库 在使用vscode开发前&#xff0c;arduino环境遗留了一些库文件&#xff0c;这些第三方库可以通…

arasan CAN2.0 CAN FD user guide详解

1. 引言 1.1 概览 Arasan 的 Controller Area Network - Flexible Data (CAN-FD) 控制器 IP 实现了 CAN 2.0A、CAN 2.0B 以及高性能 CAN-FD (Flexible Data Rate) 协议。它符合非 ISO CAN-FD 由 Bosch 提出的标准以及 ISO11898-1:2015 DIS 标准。它可以集成到需要 CAN 连接性…

2024年有哪些开放式耳机值得入手?精选五大高分品牌

近几年兴起的开放式蓝牙耳机&#xff0c;具有佩戴舒适稳固、不影响使用者判断外界环境等优点&#xff0c;十分适合在户外环境下使用&#xff0c;因此受到了众多健身人士的喜爱。那么该如何挑选到一款适合自己的开放式耳机呢&#xff1f;2024年有哪些开放式耳机值得入手&#xf…

SpringCloud Alibaba 微服务(四):Sentinel

目录 前言 一、什么是Sentinel&#xff1f; Sentinel 的主要特性 Sentinel 的开源生态 二、Sentinel的核心功能 三、Sentinel 的主要优势与特性 1、丰富的流控规则 2、完善的熔断降级机制 3、实时监控和控制台 4、多数据源支持 5、扩展性强 四、Sentinel 与 Hystrix …

Axure Web端元件库:构建高效互动网页的基石

在快速迭代的互联网时代&#xff0c;Web设计与开发不仅追求视觉上的美感&#xff0c;更注重用户体验的流畅与功能的强大。Axure RP&#xff0c;作为一款专业的原型设计工具&#xff0c;凭借其强大的交互设计能力和丰富的元件库&#xff0c;成为了众多UI/UX设计师、产品经理及前…

C语言 ——— 指针笔试题(中篇)

指针加减整数和解引用的笔试题 笔试题1&#xff1a; int a[5][5]; int(*p)[4]; p a;printf("%p %d", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);问&#xff1a;打印的结果为&#xff1f;&#xff08;分别以 %d 和 %p 的形式打印&#xff09; …

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第六十二章 定时器按键消抖实验

i.MX8MM处理器采用了先进的14LPCFinFET工艺&#xff0c;提供更快的速度和更高的电源效率;四核Cortex-A53&#xff0c;单核Cortex-M4&#xff0c;多达五个内核 &#xff0c;主频高达1.8GHz&#xff0c;2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…

linux系统ShellCheck检查shell脚步语法正确的工具

目录 ShellCheck 安装ShellCheck 、dnf、yum 源代码编译 步骤如下&#xff1a; 示例命令&#xff1a; 方法三&#xff1a;使用其他第三方仓库、COPR 仓库 假设 ShellCheck 输出如下&#xff1a; 分析输出 修改脚本 再次运行 ShellCheck 1. Shell 脚本最佳实践 主题…

环境如何搭建部署Nacos

这里我使用的是Centos7&#xff0c; Nacos 依赖 Java环境来运行。如果您是从代码开始构建并运行Nacos&#xff0c;还需要为此配置 Maven环境&#xff0c;请确保是在以下版本环境中安装使用 ## 1、下载安装JDK wget https://download.oracle.com/java/17/latest/jdk-17_linux-x6…

不同类型游戏安全风险对抗概览(下)| FPS以及小游戏等外挂问题,一文读懂!

FPS 游戏安全问题 由于射击类游戏本身需要大量数值计算&#xff0c;游戏方会将部分计算存放于本地客户端&#xff0c;而这为外挂攻击者提供了攻击的温床。可以说&#xff0c;射击类游戏是所有游戏中被外挂攻击最为频繁的游戏类型。 根据网易易盾游戏安全部门检测数据显示&#…

AWS-负载均衡-创建一个对外的HTTPS ALB

目录 介绍 功能差异 适用场景 性能比较 补充 基本组成部分 创建流程 介绍 Elastic Load Balancing 支持三种类型的负载均衡器&#xff1a;Application Load Balancer、Network Load Balancer 和 Classic Load Balancer。这里用ALB( Application Load Balancer)说明。 功…

聊聊ChatGLM-6B部署与微调的深入理解

前言 ChatGLM的部署&#xff0c;主要是两个步骤&#xff1a; 在Github上下载chatglm的库文件在Hugging Face上下载模型参数与配置文件 ChatGLM包 从Github上看ChatGLM项目文件的结构来看&#xff0c;仅仅是包含三种部署方式的py代码与微调的py代码 而相关的实现细节&#…

黑马JavaWeb后端案例开发(包含所有知识点!!!)

目录 1.准备工作 环境搭建 开发规范 REST&#xff08;REpresentation State Transfer&#xff09;,表述性状态转换&#xff0c;它是一种软件架构风格 注意事项 统一响应结果 2.部门管理功能 查询部门 删除部门 新增部门 RequestMapping 3.员工管理功能 分页查询 批…

Android 10.0 Launcher 启动流程

在前面SystemUI启动流程中说到&#xff0c;在SystemServer中会去启动各种系统服务&#xff0c;这里的launcher也是启动的其中一个服务ActivityManagerService去启动的。在android10之前&#xff0c;系统四大组件的启动都是在ActivityManagerService中&#xff0c;在android10中…