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;//定义基类对象A
	B 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;//定义基类对象A
	B 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而非C

	return 0;
}

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

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

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

相关文章

用ChatGPT和六顶帽思考法帮助自己更好地决策和解决问题

当我们在解决复杂问题时&#xff0c;我们常常陷入单一视角的状态。创造性思维领域的先驱爱德华德博诺&#xff0c;提出了六顶帽思考法[1]&#xff0c;这意味着我们可以从六个不同的视角来思考一个问题&#xff0c;以实现高水平决策和解决问题。 每一顶“帽子”代表不同的视角。…

【写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串】

写一个函数&#xff0c;判断一个字符串是否为另外一个字符串旋转之后的字符串 1.题目 写一个函数&#xff0c;判断一个字符串是否为另外一个字符串旋转之后的字符串。 例如&#xff1a;给定s1 AABCD和s2 BCDAA&#xff0c;返回1 给定s1abcd和s2ACBD&#xff0c;返回0. AABCD左…

当前服务器版本不支持该功能,请联系经销商升级服务器 - - 达梦数据库报错

当前服务器版本不支持该功能&#xff0c;请联系经销商升级服务器 - - 达梦数据库报错 环境介绍1 搭建测试环境2 报错内容3 标准版介绍 环境介绍 某项目使用标准版数据库中&#xff0c;使用insert into 正常操作表&#xff0c;插入数据时报错&#xff0c;表为普通表。 1 搭建测…

sharedPreferences的使用之按钮状态切换的保存

什么是sharedPreferences&#xff1f;有什么用 SharedPreference是Android开发中一个轻量级的数据存储的方式&#xff0c;除了它还有SQLite数据库。它可以将数据以键值对的形式存放到文件中&#xff0c;在需要的时候再取出来使用。相比于去操作数据库&#xff0c;对于一些简单…

3.1 Spring MVC概述

1. MVC概念 MVC是一种编程思想&#xff0c;它将应用分为模型&#xff08;Model&#xff09;、视图&#xff08;View&#xff09;、控制器&#xff08;Controller&#xff09;三个层次&#xff0c;这三部分以最低的耦合进行协同工作&#xff0c;从而提高应用的可扩展性及可维护…

基于Flask的模型部署

基于Flask的模型部署 一、背景 Flask&#xff1a;一个使用Python编写的轻量级Web应用程序框架&#xff1b; 首先需要明确模型部署的两种方式&#xff1a;在线和离线&#xff1b; 在线&#xff1a;就是将模型部署到类似于服务器上&#xff0c;调用需要通过网络传输数据&…

大模型的数据隐私问题有解了,浙江大学提出联邦大语言模型

作者 | 小戏、Python 理想化的 Learning 的理论方法作用于现实世界总会面临着诸多挑战&#xff0c;从模型部署到模型压缩&#xff0c;从数据的可获取性到数据的隐私问题。而面对着公共领域数据的稀缺性以及私有领域的数据隐私问题&#xff0c;联邦学习&#xff08;Federated Le…

AI:02-基于深度学习的动物图像检索算法的研究

文章目录 一、算法原理二、代码实现三、实验结果四、总结深度学习在计算机视觉领域中的应用越来越广泛,其中动物图像检索算法是一个重要的应用场景。本文将介绍一种基于深度学习的动物图像检索算法,并提供相应的代码实现。 一、算法原理 本算法采用卷积神经网络(Convolutio…

如何将maven部署在Idea的教学,和idea介绍

目录 一.idea介绍&#xff0c;以及一些基本特点&#xff01; 1.1idea介绍 1.2idea特点 1.3.idea和eclipse区别 1.4idea安装 4.1下载网址 4.2下载后安装&#xff08;见图片&#xff09; 1.5 Idea的一些操作更改 5.1主题颜色 5.2设置鼠标悬浮提示 5.3显示方法分隔符 5.4忽…

架构设计第八讲:架构 - 理解架构的模式2 (重点)

架构设计第八讲&#xff1a;架构 - 理解架构的模式2 (重点) 本文是架构设计第8讲&#xff1a;架构 - 理解架构的模式2&#xff0c;整理自朱晔的互联网架构实践心得, 他是结合了 微软给出的云架构的一些模式的基础上加入他自己的理解来总结互联网架构中具体的一些模式。我在此基…

Databend 开源周报第 105 期

Databend 是一款现代云数仓。专为弹性和高效设计&#xff0c;为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务&#xff1a;https://app.databend.cn 。 Whats On In Databend 探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。 Databend 轻量级…

netty面试题2

1、一次完整的HTTP请求的所经历的步骤 1、首先进行DNS域名解析&#xff08;本地浏览器缓存、操作系统缓存或者DNS服务器&#xff09;&#xff0c;首先会搜索浏览器自身的DNS缓存&#xff08;缓存时间比较短&#xff0c;大概只有1分钟&#xff0c;且只能容纳1000条缓存&#xff…

C语言案例 分数列求和-11

题目&#xff1a;有一分数列&#xff1a;2 / 1,3 / 2,5 / 3,8 / 5,13 / 8,21 / 13 …求出这个数列的前20项之和。 程序分析 这是一个典型的分数列数学逻辑题&#xff0c;考究这类题目是需要从已知的条件中找到它们的分布规律 我们把前6荐的分子与分母分别排列出来&#xff0c;…

知识图谱实战应用23-【知识图谱的高级用法】Neo4j图算法的Cypher查询语句实例

大家好,我是微学AI,今天给大家介绍一下知识图谱实战应用23-【知识图谱的高级用法】Neo4j图算法的Cypher查询语句实例,Neo4j图算法是一套在Neo4j图数据库上运行的算法集合。这些算法专门针对图数据结构进行设计,用于分析、查询和处理图数据。图算法可以帮助我们发现图中的模…

Anaconda Prompt使用pip安装PyQt5-tools后无法打开Spyder或闪退

艹&#xff01;MLGBZD! 真TMD折腾人&#xff01; 出现原因&#xff1a; 首次安装完Anaconda3-2023.07-1-Windows-x86_64.exe后首次打开Spyder&#xff0c;此时是没有问题的&#xff0c;然后打开Anaconda Prompt&#xff0c;查看有哪些包&#xff0c;pip list 这时候开始首次安…

C语言学习笔记 使用vscode外部console出现闪退-12

前言 在使用vscode的外部console时&#xff0c;会出现闪退现象&#xff0c;这是因为程序运行结束后&#xff0c;系统自动退出了终端&#xff08;终端机制决定的&#xff09;。我们可以在C程序结束后&#xff0c;使用system函数来暂停DOS终端系统&#xff0c;这样就可以完整地看…

6-Ngnix配置反向代理

1.前提 虚拟机能连接外网 仿真http应用需在本虚拟机启用(原因&#xff1a;只有一台虚拟机做测试) http_8080和http_8081要启用&#xff08;http测试应用&#xff09; [rootcent79-2 ~]# ls -l http_* -rwxr-xr-x 1 root root 6391676 Jul 19 13:39 http_8080 -rwxr-xr-x 1 …

监控Kubernetes 控制面组件的关键指标

控制面组件的监控&#xff0c;包括 APIServer、Controller-manager&#xff08;简称 CM&#xff09;、Scheduler、etcd 四个组件。 1、APIServer APIServer 的核心职能是 Kubernetes 集群的 API 总入口&#xff0c;Kube-Proxy、Kubelet、Controller-Manager、Scheduler 等都需…

系列六、Redis中的五大数据类型及相关操作

一、五大数据类型 String类型、List类型、Set类型、ZSet类型、hash类型。 二、String类型 2.1、内存储存模型 2.2、常用操作命令 三、List类型 3.1、概述 list列表&#xff0c;相当于Java中的list集合。特点&#xff1a;元素有序 且 可以重复。 3.2、内存存储模型 3.3、常用…

MySQL——Mysql安装教程- Windows

一、Mysql安装 1、下载mysql安装包 下载链接&#xff1a; 链接&#xff1a; https://pan.baidu.com/s/1rFpMqOCApiQQEwYSs9XSmg https://pan.baidu.com/s/1rFpMqOCApiQQEwYSs9XSmg 提取码&#xff1a;zt88 2、 安装 1&#xff09;选择电脑磁盘空闲的路径&#xff1a; 2&…