C++-多态

news2025/1/19 23:13:25

目录

一.多态的概念

二.多态的条件

三.对实现多态的条件进行解释

四.override和final

五.三重对比

六.虚函数表和虚函数表指针

七.静态的多态和动态的多态


一.多态的概念

        多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态
        举个栗子:比如买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人买票时是优先买票。再举个栗子: 最近为了 争夺在线支付市场 ,支付宝年底经常会做诱人的 扫红包 - 支付 - 给奖励金 的 活动。那么大家想想为什么有人扫的红包又大又新鲜8 块、 10 ... ,而有人扫的红包都是 1 毛, 5 毛.... 。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如 你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你 去使用支付宝,那么就你扫码金额 = random()%1 ;总结一下: 同样是扫码动作,不同的用户扫 得到的不一样的红包,这也是一种多态行为。 ps :支付宝红包问题纯属瞎编,大家仅供娱乐。
        总结一句话就是不同的人在调用相同的方法时得到结果并不相同。

二.多态的条件

        
        首先多态是发生在基类和派生类之间的一种调用函数的情况。
        多态的条件:1.调用发生函数重写的虚函数函数。
                              2.通过基类的引用或指针来调用相应的虚函数。

       

       什么是虚函数:

        

       在类的成员方法前加上 virtual就表明该函数是一个虚函数,而且只有成员函数才能是虚函数。 

       什么是虚函数的重写:

                在派生类中存在一个和基类中相同的函数名,相同的返回值,相同的参数的函数就被称为基类虚函数的重写(也叫覆盖)。      可以称为虚函数加三同。

                这里强调一下,重写是语法层的定义,覆盖是底层原理上的定义,这个概念在后面虚表中有介绍到。

        
        一个多态调用的样例:
#include <iostream>

using namespace std;

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

void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

函数的多态调用看的是那个对象来调用,而函数的普通调用看的是调用 这个函数的对象的类型。

 因为此时不满足调用重写函数的一定要是基类的引用或指针。

虚函数重写的一些特例:
        首先前面提到过,虚函数重写的条件是,虚函数 + 三同。

        1.在派生类中重写虚函数时可以不加virtual(但是在使用的时候建议都加上,因为这样会增加代码的可读性,即使不加virtual编译器在处理这块的时候还是会当作虚函数的重写来处理的),基类中的对应的虚函数一定要加上,才能保证多态。

    

问题:基类中的析构函数可以是虚函数吗?为什么?
        基类中的析构函数加virtual后也是虚函数,那么为什么要将基类的析构函数也弄成虚函数呢?

        下面看一个示例:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual ~Person() { cout << "~Person" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	virtual ~Student()
	{
		cout << "~Student()" << endl;
		delete[] ptr;
	}

protected:
	int* ptr =  new int[10];
};
int main()
{
	Person* p = new Person;
	delete p;

	p = new Student;
	delete p;
	return 0;
}

        此时如果不重写析构函数就会发生内存泄漏的问题。因为此时p是Person类的指针,所以如果没有虚函数的重写,那么此时就只会去调用基类的析构函数去清理Student类型中Person的部分,而不会去释放Student中自己的部分。 

                那么通过上面的定义也不满足多态的定义啊,为什么会实现多态呢?

                        因为类的析构函数会被处理成destructor这个统一的名称。

                        此时派生类和基类的析构函数就满足了虚函数加同了,然后又是基类指针调用,所以就可以实现多态的调用。

        2.协变(在实际开发中并不常用)                                    理解即可

               虚函数在重写时,函数的返回值可以不同,但是要求返回值必须是父子关系的指针和引用。 

                但是要保证基类中是指针派生类中就是指针,基类中是引用派生类就是引用。 

三.对实现多态的条件进行解释

    1.为什么一定要是基类的指针或引用?基类对象不可以吗,为什么?

  • 因为只有基类的指针或引用才可以做到即可以调用基类又可以调用子类。   
  • 基类的对象在用派生类赋值时,会发生赋值转换,此时是切片拷贝,即使此时有虚表,也是基类自己的虚表,调用当然还是基类自己的函数。 

    2. 虚函数重写的本质是,当函数从基类中继承过来时,如果此时要对该函数进行重写,那么此时函数的原型并不会发生改变,只是将该函数的函数体进行重写。

        所以也有,普通的函数继承是函数的实现继承,多态继承函数是函数的接口继承。

        eg:

此时我们对代码进行更改

        

运行结果说明此时在派生类中重写的虚函数的this指针依然是派生类类型的指针,不会因为上面的定义而发生改变。this指针是根据类型是什么来形成的。

 

         首先在main函数中定义了B类型的指针变量p,然后new一个B类型的空间让p这个指针变量来保存,然后通过p来调用test函数,而test函数是B类型从基类中继承过来的,B类型中并没有对该虚函数进行重写,所以该函数中this指针依然是基类类型的指针变量,所以此时也就满足了多态调用的第一个条件基类的指针或引用,而在test函数中调用func函数,此时派生类中是对该函数进行重写了的,那么此时两个多态调用的函数的都满足了,多态调用看的是调用该函数的是那个对象,是派生类对象调用那么就去调用派生类对象的的func函数,但是因为虚函数的多态继承是接口继承,所以此时的结果是:

        


 

四.override和final

        在基类的虚函数中用final修饰表明该函数不能被它的派生类重写,如果重写会报错。

        override帮助派生类检查是否完成了重写,如果没有会报错。

如果一个类不想被继承应该如何设计呢:

1.将基类的构造函数定义为私有就可以了。

2.使用final

         

五.三重对比

        重载中函数的参数不同。

        只要在继承的体系中派生类和基类的函数名相同就构成重定义,然后具体是否是重写要看基类中的相应函数是否是虚函数。所有会有重写的条件比重定义的条件更加苛刻的说法。

六.虚函数表和虚函数表指针

在64位平台下

 

         这里会产出一个疑问:

                为什么会是16字节啊,成员函数不是不在类型实例化时,每次都开辟新的空间吗?此时就引入了虚函数表的概念

        当我们将类中的函数声明为虚函数时,那么此时类中就会形成一个虚函数表指针,在这个虚函数表指针中保存的是虚函数表的地址,在虚函数表中保存的都是我们声明为虚函数的函数的地址。

        虚函数表其实本质上是一个函数指针数组,在这个数组中保存的是被我们声明为虚函数的函数的地址。

        虚函数指针是保证这个函数指针数组的数组名的指针变量。

         所以此时也就解释了为什么会是16个字节了,多余的字节用来保存虚表的地址了和内存对齐。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual ~Person() { cout << "~Person" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() override { cout << "买票-半价" << endl; }
	virtual ~Student()
	{
		cout << "~Student()" << endl;
		delete[] ptr;
	}

protected:
	int* ptr =  new int[10];
};
int main()
{
	Person p;
	Student s;
	return 0;
}

                

        所以此时就解释了为什么,在多态调用时,都是基类的引用或指针但是最后的结果却并不相同。

        当基类指针或引用指向基类对象时,回去基类对象中的找到虚函数表指针,通过虚函数表指针找到虚函数表,在虚函数表中找到相应的函数的地址完成调用,在通过基类的引用或指针调用派生类时同理。

         在虚函数表的内存空间中会在结尾用,nullptr也就是0来表示结尾:(64位平台下)
        

在不同的平台下可能结果并不相同 。在vs中是这样但是在g++中就会在结尾加0。

那么虚函数表是保存在内存中的什么位置呢:

        是保存在内存中的代码段(常量区)的。

        提示:这里没有找到一针见血的方式去验证虚表就是保存在常量区的,是通过内存地址的编号来大概确定的。

                

        虚函数表中的内容是不可被更改的。

 多继承中类的虚表

        派生类不会单独产生虚表,通常派生类的虚表都是通过基类继承下来的,然后派生类在对继承下来的虚表进行处理,但是需要提醒的是派生类的虚表和基类中的虚表不是同一张虚表。

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private: int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

int main(void)
{
	Derive d;
	cout << sizeof(d) << endl;

	return 0;
}

此时这个代码的输出结果是多少:20(32位)

        为什么会是20个字节呢?因为是多继承所以在Derive这个类型中存在Base1子成员又存在Base2子成员 还有它自己的成员,Base1是8个字节Base2同理,然后将Derive中自己的成员是4字节,最后在内存对齐,可得结果是20字节。

        因为派生类不会生成自己的虚表所以,在此时的多继承中存在两个虚表。然后在派生类中发生了对两个基类中函数的重写,然后将重写后的函数的地址分别填写到两个虚表中。

        

        但是派生类中新建立的虚函数func3是保存在两个虚表中那个表中呢? 

        通过测试可知保存在第一个虚表中。

测试代码

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private: int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

typedef void(*FUNC_PTR) ();
void PrintVTable(FUNC_PTR* vTable)
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		FUNC_PTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main(void)
{
	Derive d;
	cout << sizeof(d) << endl;

	PrintVTable((FUNC_PTR*)(*(int*)(&d)));

	return 0;
}

        但是通过观察我们发现,此时派生类中对Base1和Base2中的func1函数进行重写但是两个虚表中所保存的地址却不相同,但是多态调用后的结果却相同,这是为什么呢。 

七.静态的多态和动态的多态

        1.静态的多态

                静态的多态是指函数的重载,也称编译是的多态,被调用函数的地址在编译的时候就要被确定。

                模板属于编译时多态。

        2.动态的多态

                动态的多态是指通过继承,虚函数的重写,实现的多态,也称为运行时的多态,是程序运行起来后通过虚函数表中记录的函数的地址来找到被调用的函数的。

        

        

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

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

相关文章

《第一行代码:Android》第三版-如何为一个Activity添加layout文件

确切地说就是讲如何给一个不带view的Activity添加一个view&#xff0c;就是添加一个layout文件。 新建安卓项目&#xff0c;如果选择&#xff1a;就会给你创建一个没有view的Activity&#xff0c;如果后来你发现需要为这个Activity添加view&#xff0c;就是添加一个布局文件怎…

高速USB转以太网芯片CH397 UBOOT使用教程

简介 CH397 是一款高集成度、低功耗的 USB 网卡芯片&#xff0c;内置青稞 RISC-V 处理器、符合 USB2.1 协议规 范的高速 USB 控制器及收发器 PHY、以及符合 IEEE802.3 协议规范、支持 10M/100M 网络的以太网 MACPHY。已适配各类台式电脑、笔记本电脑、平板电脑、游戏机等的标准…

香港身份、香港永居身份、香港护照区别,三种证件之间是什么关系?

香港身份、香港永居身份、香港护照区别&#xff0c;三种证件之间是什么关系&#xff1f; 在港“通常性”住满7年之后&#xff0c;可以申请永居身份&#xff01; 香港身份&#xff1a;也可以称之为临时身份&#xff0c;无论通过香港优才计划、高才通计划、专才计划或者留学拿身份…

ASUS(华硕) B760M-AYW WIFI D4_解决wifi不能使用

1、最近新购买了一套 diy电脑主机&#xff0c;选用的是 ASUS B760M-AYW WIFI D4电脑主板 win10 系统&#xff0c;到货后 发现右下角电脑图标处及网络适配器中 没有wifi选项 首先 在官网和旗舰店客服处&#xff0c;确认了 该主板 有集成wifi模块&#xff0c;鲨鱼鳍天线未安装…

一篇让小白彻底搞懂性能调优

什么是性能调优&#xff1f;(what) 为什么需要性能调优&#xff1f;(why) 什么时候需要性能调优&#xff1f;(when) 什么地方需要性能调优&#xff1f;(where) 什么人来进行性能调优&#xff1f;(who) 怎么样进行性能调优&#xff1f;(How) 硬件配置&#xff1a;CUP Xeon…

【获奖作品公开】和鲸社区全方位协力师生备战中国大学生计算机设计大赛

11 月 23 日&#xff0c;2021 - 2023 年&#xff08;第 14 - 16 届&#xff09;中国大学生计算机设计大赛获奖作品正式上线和鲸社区&#xff01; 中国大学生计算机设计大赛&#xff08;下简称“大赛”或“4C”&#xff09;是由教育部认证、我国高校面向本科生最早的赛事之一&a…

一文读懂Asyncio

什么是Asyncio asyncio 是用来编写并发代码的库&#xff0c;使用async/await语法。 asyncio 被用作多个提供高性能 Python 异步框架的基础&#xff0c;包括网络和网站服务&#xff0c;数据库连接库&#xff0c;分布式任务队列等等。 asyncio 往往是构建 IO 密集型和高层级结构化…

无人机覆盖路径规划综述

摘要&#xff1a;覆盖路径规划包括找到覆盖某个目标区域的每个点的路线。近年来&#xff0c;无人机已被应用于涉及地形覆盖的多个应用领域&#xff0c;如监视、智能农业、摄影测量、灾害管理、民事安全和野火跟踪等。本文旨在探索和分析文献中与覆盖路径规划问题中使用的不同方…

二叉树:已知先序中序求后序或者其他(秒解)

看图找规律哈&#xff0c;不明白的在评论区找我哦

【精选】600兆 保研资料包 文书+面经+择校择导经验 等等 干货满满

推免上岸学长精选保研资料包 资源简介 保研上岸某中9计算机专业学长精心整理的600多兆保研资料包&#xff0c;内容覆盖全面&#xff0c;一次将保研涉及到的面试PPT、个人陈述、中英文自我介绍、英语问题、保研常识、专家推荐信、联系导师邮件、往年保研经验贴&面经、面试…

苹果TF签名全称TestFlight签名,需要怎么做才可以上架呢?

如果你正在开发一个iOS应用并准备进行内测&#xff0c;TestFlight是苹果提供的一个免费的解决方案&#xff0c;它使开发者可以邀请用户参加应用的测试。以下是一步步的指南&#xff0c;教你如何利用TestFlight进行内测以便于应用后续可以顺利上架App Store。 1: 准备工作 在测…

【Openstack Train安装】九、Nova安装

Nova是OpenStack中最核心的组件&#xff0c;它负责根据需求提供虚拟机服务并管理虚拟机生命周期&#xff0c;包括虚拟机创建、虚拟机调度和热迁移等。 Nova的子组件包括nova-api、nova-compute、nova-scheduler、nova-conductor、nova-db、nova-console等等。 本文介绍Nova安装…

Mysql 不执行索引问题与优化

难以查找的隐藏问题 及 解决办法&#xff1a; 问题总结&#xff1a;

基于SSM的仓库管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

11月29日作业

作业&#xff1a; 自己封装一个矩形类(Rect)&#xff0c;拥有私有属性:宽度(width)、高度(height)&#xff0c; 定义公有成员函数: 初始化函数:void init(int w, int h) 更改宽度的函数:set_w(int w) 更改高度的函数:set_h(int h) 输出该矩形的周长和面积函数:void show(…

Python语言学习笔记之二(基础语法)

本课程对于有其它语言基础的开发人员可以参考和学习&#xff0c;同时也是记录下来&#xff0c;为个人学习使用&#xff0c;文档中有此不当之处&#xff0c;请谅解。 Python几种字符串的表示&#xff1a; 在Python中&#xff0c;字符串是一种基本的数据类型&#xff0c;可以使…

高并发架构设计方法:面对高并发,怎么对症下药?

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 我们知道&#xff0c;“高并发”是现在系统架构设计的核心关键词。一个架构师如果设计、开发的系统不支持高并发&#xff0c;那简直不好意思跟同行讨论。但事实上&#xff0c;在架构设计领域&#xff0c;高并发的历史…

Linux:可视化管理工具Webmin的安装

一、下载 地址&#xff1a;Webmin官网 我这里下载的是1.700-1版本 二、安装 1、在虚拟机上新建目录并安装软件 mkdir /opt/webmin rpm -ivh webmin-1.700-1.noarch.rpm2、修改webmin的root密码 /usr/libexec/webmin/changepass.pl /etc/webmin root 1234563、修改端口(可…

macbook电脑运行缓慢和卡顿内存怎么清理了?

假如你还在为“你的系统内存不足”的提示所困扰&#xff0c;或者你的Mac电脑突然运行缓慢和卡顿&#xff0c;那么你一般需要认真了解一下macbook内存怎么清理了? MacBook是功能强大的电脑&#xff0c;这点毫无疑问&#xff0c;但是它仍旧会随着时间推移变得运行缓慢。值得庆幸…

瑞云科技参与《数字孪生世界白皮书》编写,实时云渲染助力数字孪生

为了促进数字孪生技术的发展和应用&#xff0c;易知微与数字孪生世界企业联盟联合众多行业专家以及多家业内企业共同编写了《数字孪生世界白皮书&#xff08;2023&#xff09;》。该白皮书从数字孪生的综述、应用架构、核心技术、新型技术成果和重点行业应用等方面&#xff0c;…