C++ 多态性——虚函数

虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程的多态。

根据类型兼容规则,可以使用派生类的对象代替基类的对象。如果基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,但是访问到的只是从基类继承来的同名的函数成员。如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,首先在基类中将这个同名函数声明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同行为,从而实现运行过程的多态。

1.一般虚函数成员

(1)一般虚函数成员的声明语法是:
virtual 函数类型 函数名(参数表);

这实际上就是在类的定义中使用virtual关键字来限定成员函数,虚函数声明只能出现在类定义中的函数原型声明中,不能出现在成员函数实现的时候。

运行过程中的多态需要满足3个条件:
(1)类之间满足类型兼容规则
(2)要声明虚函数
(3)要由成员函数来调用或者通过指针、引用来访问虚函数

【注意】虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数来处理。但将虚函数声明为内联函数也不会引起错误,因为编译器会自动忽略。

(2)普通函数成员与虚函数成员的比较

①普通函数成员

#include<iostream>
using namespace std;class A//基类A定义
{
public:void display()const//声明基类A中的成员函数为普通函数{cout << "显示类A" << endl;}
};class B :public A//公有派生类B定义
{
public:void display()const{cout << "显示类B" << endl;}
};class C :public B//公有派生类C定义
{
public:void display()const{cout << "显示类C" << endl;}
};void fun(A* p)//参数为指向基类A的对象的指针
{p->display();//"对象指针->成员名"
}int main()
{A a;//定义基类对象AB b;//定义直接基类为A类的派生类B的对象C c;//定义直接基类为B类的派生类C的对象fun(&a);//用基类A的对象的指针调用fun函数fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数return 0;
}

运行结果:
在这里插入图片描述
分析:
上述程序中,虽然基类A的指针指向了派生类B,C的对象,但是fun函数运行时,通过这个指针只能访问到派生类B和C中从基类A继承下来的成员函数display,而不是派生类B和C中自身的的同名函数display。

②虚函数成员

class A//基类A定义
{
public:virtual void display()const//声明基类A中的成员函数为虚函数{cout << "显示类A" << endl;}
};class B :public A//公有派生类B定义
{
public:void display()const//覆盖基类的虚函数{cout << "显示类B" << endl;}
};class C :public B//公有派生类C定义
{
public:void display()const//覆盖基类的虚函数{cout << "显示类C" << endl;}
};void fun(A* p)//参数为指向基类A的对象的指针
{p->display();//"对象指针->成员名"
}int main()
{A a;//定义基类对象AB b;//定义直接基类为A类的派生类B的对象C c;//定义直接基类为B类的派生类C的对象fun(&a);//用基类A的对象的指针调用fun函数fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数return 0;
}

运行结果:
在这里插入图片描述
分析:
程序中的A,B和C属于同一个类族,而且是通过公有派生而来的,因此满足类型兼容规则。同时基类A的函数成员声明为虚函数,程序中使用对象指针来访问函数成员。这样绑定过程就在运行过程中完成,实现了运行中的多态。通过基类类型的指针就可以访问到正在指向的对象的成员,这样就能够对同一类族中的对象进行统一处理,抽象程序更高,程序更加简洁、高效。

在本程序中派生类并没有显式给出虚函数的声明,这时系统就会遵循以下规则来判断一个函数成员是不是虚函数:
(1)该函数是否与基类的虚函数具有相同的名称
(2)该函数是否与基类的虚函数具有相同的参数个数及相同的对应参数类型
(3)该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值

如果从名称、参数、返回值3个方面检查之后,派生类的函数满足以上条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的其他所有重载形式。

【注意】
①用指向派生类对象的指针仍然可以调用基类中被派生类覆盖的成员函数,方法是使用“::”进行限定。例如如果把上例中的fun函数改为以下形式,其他部分不改动:

void fun(A* p)//参数为指向基类A的对象的指针
{p->A::display();//"对象指针->成员名"
}

运行结果:
在这里插入图片描述
可以看出,使用“::”进行限定之后,无论p所指向的对象的多态类型是什么,最终被调用的总是A类的display函数。在派生类的函数中,有时需要先调用基类被覆盖的函数,再执行派生类特有的操作,这时就可以使用“基类名::函数名(…)”来调用基类中被覆盖的函数。

②派生类覆盖基类成员函数时,既可以用virtual关键字,也可以不使用,二者没有差别。只要在基类中声明某成员函数是虚函数即可,派生类中的同名成员函数可以不声明为虚函数。有时候习惯于在派生类函数中也使用virtual关键字,因为这样可以清楚提示这是一个虚函数。

(3)基类的构造函数和析构函数对虚函数的调用

①当基类的构造函数调用虚函数时,不会调用派生类的虚函数。

假设有基类A和派生类B,两个类中有虚成员函数fun(),在执行派生类B的构造函数时,需要首先调用基类A的构造函数。如果A::A()调用了虚函数fun(),则被调用的是A::fun(),而不是B::fun()。这是因为当基类被构造时,对象还不是一个派生类对象。

同样,当基类被析构时,对象以及不再是一个派生类对象了,所以如果A::~A()调用了fun(),则被调用的时A::fun(),而不是B::fun()。

class A//基类A定义
{
public:A(){fun();cout << "调用基类A的默认构造函数" << endl;}A(int a):x(a){fun();cout << "调用基类A的构造函数" << endl;}virtual void fun()const//声明基类A中的成员函数为虚函数{cout << "显示类A" << endl;}~A(){cout << endl;fun();cout << "调用基类A的析构函数" << endl;}
private:int  x;
};class B :public A//公有派生类B定义
{
public:B(){}B(int b) :y(b){cout << "调用派生类B的构造函数" << endl;}virtual void fun()const//覆盖基类的虚函数{cout << "显示类B" << endl;}~B(){cout << endl;fun();cout << "调用派生类B的析构函数" << endl;}
private:int y;
};int main()
{A a(5);cout << endl;B b(3);return 0;
}

运行结果:
在这里插入图片描述
分析:

在主函数中,定义了一个基类A的对象a并进行初始化,初始化时调用基类A的构造函数,基类A的构造函数中调用虚函数fun(),虽然在基类A和派生类B中都有虚函数fun(),但是在基类A的构造函数中调用的fun()函数是基类A中的fun函数,而不是派生类B中的fun()函数。又定义了一个派生类对象b并进行初始化,初始化派生类对象b时先调用基类A的默认构造函数,再调用B类的构造函数进行初始化b对象,在调用A类默认构造函数时,A类默认构造函数中调用了虚函数fun,这里调用的虚函数fun仍然不是B类中的虚函数fun,而是A类中的虚函数fun。
这是因为当基类A被被构造时,对象还不是一个派生类对象。

②只有虚函数是多态绑定的,如果派生类需要修改基类的行为(即重写与基类函数同名的函数),就应该在基类中将相应的函数声明为虚函数。而基类中声明的非虚函数,通常代表那些不希望被派生类改变的功能,也就是不能实现多态的。一般不要重写继承而来的非虚函数,因为会导致通过基类的指针和派生类的指针会对象调用同名函数时,会产生不同的结果而引起混乱。

【注意】在重写继承来的虚函数时,如果函数有默认值形参值,不要重新定义不同的值。因为,虽然虚函数是多态绑定的,但是默认形参是静态绑定的。也就是说,通过一个指向派生类对象的基类指针,可以访问到派生类的虚函数,但是默认形参值却只能来自基类定义。例如:

class A//基类A定义
{
public:virtual void display()const//声明基类A中的成员函数为虚函数{cout << "显示类A" << endl;}
};class B :public A//公有派生类B定义
{
public:virtual void display()const//覆盖基类的虚函数{cout << "显示类B" << endl;}
};class C :public B//公有派生类C定义
{
public:virtual void display()const//覆盖基类的虚函数{cout << "显示类C" << endl;}
};void fun(A* p)//参数为指向基类A的对象的指针
{p->A::display();//"对象指针->成员名"
}int main()
{C c;//定义派生类对象A* p = &c;//基类指针p可以指向派生类对象A& r = c;//基类引用r可以作为派生类对象的别名A a = c;//调用基类A的拷贝构造函数用c构造a,a的类型是A而非Creturn 0;
}

这里,A a = c;会用C类型的对象c为A类型的对象a初始化,初始化时使用的是A类的拷贝构造函数。由于拷贝构造函数接收的是A类型的常引用,C类型的c符合类型兼容规则,可以作为参数传递给它。由于执行的是A类的拷贝构造函数,只有A类型的成员会被拷贝,C类中新增的数据成员不会被拷贝,也没有空间去存储,因此生成的对象是基类A的对象。这种用派生类对象拷贝构造基类对象的行为叫做对象切片。这时,如果用a去调用基类A的虚函数,调用的目的对象是对象切片后得到的A类对象,与C类型的c对象毫无关系,对象的类型很明确,因此无须多态绑定。

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

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

相关文章

中级课程——XSS

文章目录 介绍挖掘思路分类反射型存储型dom类型 介绍 挖掘思路 注入点&#xff1a;各种输入框 测试代码&#xff08;poc&#xff09;&#xff1a;js语句 分类 反射型 存储型 dom类型

《合成孔径雷达成像算法与实现》Figure3.5

clc clear all close all%参数设置 TBP 100; %时间带宽积 T 10e-6; %脉冲持续时间%参数计算 B TBP/T; %信号带宽 K B/T; …

python --windows获取启动文件夹路径/获取当前用户名/添加自启动文件

如何使用Python获取计算机用户名 一、Python自带的getpass模块可以用于获取用户输入的密码&#xff0c;但是它同样可以用来获取计算机用户名。 import getpassuser getpass.getuser() print("计算机用户名为&#xff1a;", user)二、使用os模块获取用户名 Python的…

[ubuntu]创建root权限的用户

一、创建新用户 1、创建新用户 sudo useradd -r -m -s /bin/bash 用户名 # -r&#xff1a;建立系统账号 -m&#xff1a;自动建立用户的登入目录 -s&#xff1a;指定用户登入后所使用的shell2、手动为用户设置密码 passwd 用户名 二、为用户增加root权限 1、添加写权限 ch…

【MySQL】sql字段约束

在MySQL中&#xff0c;我们需要存储的数据在特定的场景中需要不同的约束。当新插入的数据违背了该字段的约束字段&#xff0c;MySQL会直接禁止插入。 数据类型也是一种约束&#xff0c;但数据类型这个约束太过单一&#xff1b;比如我需要存储的是一个序号&#xff0c;那就不可…

【JavaEE进阶】Spring创建与使用

文章目录 一. 创建 Spring 项目1.1 创建一个Maven项目1.2 添加Spring依赖1.4. 创建一个启动类 二. 将 Bean 对象存放至 Spring 容器中三. 从 Spring 容器中读取到 Bean1. 得到Spring对象2. 通过Spring 对象getBean方法获取到 Bean对象【DI操作】 一. 创建 Spring 项目 接下来使…

【Fegin技术专题】「原生态」打开Fegin之RPC技术的开端,你会使用原生态的Fegin吗?(中)

你可以使用 Jersey 和 CXF 这些来写一个 Rest 或 SOAP 服务的java客服端。 你也可以直接使用 Apache HttpClient 来实现。但是 Feign 的目的是尽量的减少资源和代码来实现和 HTTP API 的连接。 *通过自定义的编码解码器以及错误处理&#xff0c;你可以编写任何基于文本的 HTT…

我开源的 c#+wpf 模仿网易云音乐播放器

MusicApp 介绍 gitee地址&#xff1a;https://gitee.com/liu_guo_feng/music-app 我开源的 c#wpf 模仿网易云音乐播放器 项目页面功能完成列表 首页(待完善) 每日推荐音乐 歌单详情 带播放列表 歌词页(待完善) 换肤功能(待完善) 系统托盘 … 预览 仅供学习使用 不作任何商业用…

【数据分享】2000-2022年我国乡镇人口数量数据(免费获取/Shp/Excel格式)

在之前的文章中我们分享了基于LandScan数据集的2000-2022年的1km精度的全球、全国、分省、分市的人口空间分布栅格数据&#xff08;可查看之前的文章获悉详情&#xff09;。以及基于栅格数据处理出的Shp和Excel两种格式的我国省市县三级的2000-2022年度的人口数量数据&#xff…

【Rust日报】2023-08-07 自动生成字节级的 SIMD 查找表

自动生成字节级的 SIMD 查找表 本文介绍了如何使用 Rust 编写 absolut 库&#xff0c;该库可以自动生成字节级的 SIMD 查找表。 SIMD 查找表可以用于高效地扫描字节数组&#xff0c;并找到其中特定字节的索引。absolut 库使用 SMT 求解器来自动生成 SIMD 查找表。absolut 库还支…

【硬件设计】模拟电子基础三--集成运算放大电路

模拟电子基础三--集成运算放大电路 一、集成运算放大器1.1 定义、组成与性能1.2 电流源电路1.3 差动放大电路1.4 理想运算放大器 二、集成运算放大器的应用2.1 反向比例运算电路2.2 同向比例运算电路2.3 反向加法运算电路2.4 反向减法运算电路2.5 积分运算电路2.6 微分运算电路…

如何使用Word转PDF转换器在线工具?在线Word转PDF使用方法

Word转PDF转换器在线&#xff0c;是一种方便快捷的工具&#xff0c;可帮助您在不需要下载任何软件的情况下完成此任务。无论您是需要在工作中共享文档&#xff0c;还是将文件以PDF格式保存以确保格式不变&#xff0c;都可以依靠这款在线工具轻松完成转换。那么如何使用Word转PD…

Azure通过自动化账户实现对资源变更

Azure通过自动化账户实现对资源变更 创建一个自动化账户第一种方式 添加凭据&#xff08;有更改资源权限的账户&#xff0c;没有auth认证情况&#xff09;创建一个Runbook&#xff0c;测试修改 AnalysisServices 定价层设置定时任务&#xff1a;开始定时任务&#xff1a; 第二种…

Nginx负载均衡搭建

目录 1、准备一台装有nginx服务的主机 2、所需模块说明&#xff1a; 3、两台Web服务器主机 4、 修改nginx的配置文件 5、查看结果&#xff1a; 1、准备一台装有nginx服务的主机 LVS—DR集群的搭建_.98℃的博客-CSDN博客 2、所需模块说明&#xff1a; Nginx http 功能模…

Android数据存储选项:SQLite、Room等

Android数据存储选项&#xff1a;SQLite、Room等 1. 引言 在移动应用的开发过程中&#xff0c;数据存储是至关重要的一环。无论是用户的个人信息、设置配置还是应用产生的临时数据&#xff0c;都需要在设备上进行存储以便随时访问。随着移动应用的日益发展&#xff0c;数据存…

20230808在WIN10下使用python3将TXT文件转换为DOCX

20230808在WIN10下使用python3将TXT文件转换为DOCX 2023/8/8 19:30 缘起&#xff0c;由于google的文档翻译不支持SRT/TXT格式的字幕&#xff0c;因此需要将SRT格式的字幕转为DOCX。 Ch4.Unreported.World.2022.Mexicos.Psychedelic.Toads.1080p.HDTV.x265.AAC.MVGroup.org.mkv …

python3学习--使用pandas 数据透视表分析数据--入门示例

什么是透视表&#xff1f; 透视表是一种可以对数据动态排布并且分类汇总的表格格式&#xff0c;可以以多种方式和视角查看数据特征 Pandas库提供了一个名为pivot_table的函数&#xff0c;它将一个特性的值汇总在一个整洁的二维表中。 使用示例 pivot_table函数说明 pandas.…

爬虫010_列表高级_添加_append_extend_修改_查询_in_not int_删除_del_pop_remove---python工作笔记029

然后再来看列表操作 首先添加append方法 然后插入,坐标是要插入的下标,右边是插入的内容 看结果 1,2,3,4,5,6 然后这个extend,是逐个插入,放到后边 然后是修改,直接对下标赋值 看结果</

从安装 Seata 开始的分布式事务之旅 springboot集成seata

从安装 Seata 开始的分布式事务之旅 介绍什么是 Seata&#xff1f; 安装 Seata Server下载 Seata Server 发行版配置Seata解压文件配置Seata的yml文件把配置文件config.txt加载到nacos上修改config.txt文件加载到nacos上 启动Seata服务正常启动查看启动日志打开控制台页面 启动…

pytest常用执行参数详解

1. 查看pytest所有可用参数 我们可以通过pytest -h来查看所有可用参数。 从图中可以看出&#xff0c;pytest的参数有很多&#xff0c;下面是归纳一些常用的参数&#xff1a; -s&#xff1a;输出调试信息&#xff0c;包括print打印的信息。 -v&#xff1a;显示更详细的信息。 …