C++ 多态性——虚函数

news2024/11/25 15:47:38

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

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

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对象毫无关系,对象的类型很明确,因此无须多态绑定。

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

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

相关文章

mysql查询语句之实践篇

基础查询语句 完整语法格式如下&#xff1a; select 字段列表 from 表名列表 where 条件列表 group by 分组字段 having 分组之后的条件 order by 排序 limit 分页限定 -- 创建表 create table stu(id int,name varchar(20),chinese double,english double,math double ); --…

ATFX汇市:美联储年内或仍将加息依次,美指向下空间不大

环球汇市行情摘要—— 昨日&#xff0c;美元指数上涨0.08%&#xff0c;收盘在102.08点&#xff0c; 欧元贬值0.07%&#xff0c;收盘价1.1003点&#xff1b; 日元贬值0.51%&#xff0c;收盘价142.47点&#xff1b; 英镑升值0.28%&#xff0c;收盘价1.2784点&#xff1b; 瑞…

NPM包的安装、更新、卸载

目录 1、下载安装全局包 2、解决全局安装包时的EACCES权限错误 2.1 重新安装NPM 2.2 手动更改npm的默认目录 3、更新从注册表下载的包 3.1 更新本地包 3.2 更新全局安装的软件包 3.3 确定哪些全局包需要更新 3.4 更新单个全局包 3.5 更新所有全局安装的软件包 4、在项…

Zabbix监控系统详解及配置

前言 作为一个运维&#xff0c;需要会使用监控系统查看服务器状态以及网站流量指标&#xff0c;利用监控系统的数据去了解上线发布的结果&#xff0c;和网站的健康状态。利用一个优秀的监控软件&#xff0c;我们可以&#xff1a; 通过一个友好的界面进行浏览整个网站所有的服务…

实战项目ssm权限系统 2-自定义springsecurity组件实现认证授权

一 springsecurity 作用 1.1 springsecurity Spring security是spring家族的一个安全性框架&#xff0c;主要是用来进行用户认证(Authentication)和用户授权(Authorization)的框架。 用户认证&#xff1a;验证用户登录是否合法 用户授权&#xff1a;登录成功后用户具有哪些…

Linux ARM64架构 动态替换 altinstructions

文章目录 简介一、altinstructions节1.1 .altinstructions1.2 .rela.altinstructions 二、内核模块重定位源码分析参考资料 简介 在内核开发中&#xff0c;有时需要对内核代码进行修补&#xff0c;以解决bug、优化性能或引入新功能。替代指令&#xff08;altinstructions&…

云原生K8S------Yaml文件详解

目录 一&#xff1a;K8S支持的文件格式 1&#xff0c;yaml和json的主要区别 2&#xff0c;YAML语言格式 二&#xff1a;yuml 1、查看 api 资源版本标签 2、写一个yaml文件demo 3、创建service服务对外提供访问并测试 4、详解k8s中的port 三&#xff1a;文件生成 1、kubec…

如果你需要使用重试机制,请使用Spring官方的Spring Retry

Spring Retry 是 Spring Framework 中的一个模块&#xff0c;提供了一种简单的方式来在应用程序中实现重试机制。 在应用程序中&#xff0c;如果遇到了一些不可避免的错误&#xff0c;比如网络连接失败、数据库连接失败等&#xff0c;我们通常需要对这些错误进行重试&#xff…

搭建日志服务器Rsyslog

Rsyslog介绍 Rsyslog的全称是 rocket-fast system for log&#xff0c;它提供了高性能&#xff0c;高安全功能和模块化设计。rsyslog能够接受从各种各样的来源&#xff0c;将其输入&#xff0c;输出的结果到不同的目的地。rsyslog可以提供超过每秒一百万条消息给目标文件。 特…

Qt+C++实现灯带动画运动位置变换移动跑马灯图片轮播

程序示例精选 QtC实现灯带动画运动位置变换移动跑马灯图片轮播 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<QtC实现灯带动画运动位置变换移动跑马灯图片轮播>>编写代码&…

Vue项目npm run dev 启动报错TypeError: Cannot read property ‘upgrade‘ of undefined

vue项目启动报错 TypeError: Cannot read property upgrade of undefined 由于我的vue.config.js文件 里面的代理target为空导致的 修改&#xff1a; 结果就可以正常运行了 参考原文&#xff1a; vue项目运行时报Cannot read property ‘upgrade’ of undefined错误_cannot r…

【Docker】Docker的应用场景,Docker 的优点,Ubuntu Docker 安装,使用 Shell 脚本进行安装

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; 七七的闲谈 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f…

【福建事业单位-资料分析】01 速算技巧-基期与现期

【福建事业单位-资料分析】01 速算技巧-基期与现期 一、速算技巧&#xff08;基于选项的速算&#xff09;1.1 计算类别——截位直除练习总结 二、速算技巧-比较类-分数比较2.1 一大一小&#xff08;一大/一小&#xff09;2.2 同大同小①分子分母都变大&#xff0c;保留两位直接…

智橙PDM系统:图文档与物料数据同时管理编辑系统

在当今快速变化的商业环境中&#xff0c;数字化转型已经成为企业不可或缺的一部分。产品数据的高效管理和协作变得尤为重要&#xff0c;而智橙的PDM系统&#xff08;产品数据管理系统&#xff09;为企业提供了无限协作的全新维度。 无限协作&#xff0c;创新无限 智橙PDM系统瞄…

QListView的使用(正逆序插入)

首先在介绍QListView之前&#xff0c;先说一下QListView和QListWidget的区别&#xff1a; 1、QListView是model&#xff08;模型/视图&#xff09;表格类型&#xff0c;QListWidget它是Item表格类型。 2、QListView使用较复杂&#xff0c;一般需要配合数据模型QAbstractListMod…

【Linux】公网环境下Ubuntu系统SSH远程树莓派

前言 &#x1f4d5;作者简介&#xff1a;热爱跑步的恒川&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步&#xff0c;喜爱音乐的一位博主。 &#x1f4d7;本文收录于恒川的日常汇报系列&#xff0c;大家有兴趣的可以看一看 &#x1f4d8;相关专栏C语言初…

Nginx与Tomcat的区别,什么是HTTP服务器(处理静态资源的服务器),什么是处理动态资源的服务器

Nginx和Tomcat都是常用的Web服务器&#xff0c;但它们的主要作用不同。Nginx是一个HTTP服务器&#xff0c;反向代理服务器和通用TCP/UDP代理服务器。它通常用于静态内容、媒体流和负载均衡。在高流量和高并发负载下&#xff0c;Nginx表现更出色&#xff0c;并且能够轻松处理静态…

解决android studio妙明奇妙出现的模拟器

1&#xff0c;查看设备 adb devices 2&#xff0c; adb命令断开指定的设备 要断开ADB与特定设备的连接&#xff0c;可以使用以下命令&#xff1a; adb disconnect <设备ID> 将 <设备ID> 替换为您要断开连接的设备的实际ID。设备ID可以在运行 adb devices 命令…

C语言刷题------(2)

C语言刷题——————&#xff08;2&#xff09; 刷题网站&#xff1a;题库 - 蓝桥云课 (lanqiao.cn) First Question&#xff1a;时间显示 题目描述 小蓝要和朋友合作开发一个时间显示的网站。 在服务器上&#xff0c;朋友已经获取了当前的时间&#xff0c;用一个整数表…

Ansible的安装和配置

安装和配置 Ansible 安装所需的软件包 创建名为 /home/greg/ansible/inventory 的静态清单文件&#xff0c;以满足以下要求&#xff1a; 172.25.250.9 是 dev 主机组的成员 172.25.250.10 是 test 主机组的成员 172.25.250.11 和 172.25.250.12 是 prod 主机组的成员 172.2…