多态原理解析

news2025/1/20 17:06:35

一  多态应用

    首先,什么是多态呢?很多概念起初我们都是不理解的,就像我们刚接触继承一样,当学完后发现其实也没那么难,也挺容易理解的。

多态详细点就是多种状态,例如游戏中的抽宝箱,每个人难道都是一样的概率吗,如果你是游戏的老板,游戏玩家有三种,氪金玩家,回归老玩家,和零充平民,你会让这三种人做同样事的时候概率一样吗?不会的,要想让不同的人做同样的事呈现的结果不同,就是我们多态要实现的。

二 多态条件

1 子类的虚函数和父类的虚函数构成重写

2 调用的是重写虚函数

3 是用父类的指针或者引用调用的这个重写虚函数

这些条件后面会在原理部分一个个讲解,在讲原理前我们还要补充一些概念。

1.什么是重写

子类和父类的虚函数函数名相同,参数类型相同,返回值类型相同,三同后当子类继承了父类的虚函数,会对父类的虚函数进行重写。还有值得一提的是重写的仅仅是实现(这里后面会举例提及)。

   下面代码中子类的Print函数没有加virtual,那是不是说明Print不是虚函数,然后就不会进入虚函数表,更不会发生多态了呢?实际上还是会发生多态,其实这是一种特殊情况,语法规定当子类有个函数和父类构成三同后,并且父类的函数加了virtual关键字,即便子类的函数像下面Print函数一样不加virtual关键字,编译器也会将这两个函数推入重写流程,使得子类的函数的框架用的是父类的,也就继承了虚函数属性,进入了虚函数表,所以可以实现多态。

class A
{
public:
	 virtual void Print()
	{
		cout << "A::Print()" << endl;
	}
	int _a = 1;
};
class B:public A
{
public:
	void Print()
	{
		cout << "A::Print()" << endl;
	}
	int _b = 2;
};

还有两个也是构成重写的例子

(1)协变(基类和派生类的返回值类型可不同)

但是这个类型也有些限制,基类必须返回基类指针或引用,派生类必须返回派生类指针或引用,而且必须同为指针或引用。

(2)析构函数的重写

我在继承中曾提到,父子类的析构函数会触发隐藏,因为编译器将所有的析构函数名重命名了,那为什么要重命名呢?就是为了使父子类析构函数涉及重写,那为什么要重写,因为要触发多态,那为什么我们要触发多态,看下面代码。

class A
{
public:
    ~A()
   {
      cout<<"~A()"<<endl; 
   }
};
class B:public A
{
public:
     ~B()
   {
      cout<<"~B()"<<endl; 
   }
};
void test1()
{
     A&a1=new b1;//切片符合语法,但是如果没有多态,a1会调用A类的析构函数,想想这合理吗
     所以要想让a1调用根据指向类型来调用,而不是看类型,那就是要实现多态。  
}

2 什么是虚函数

就是在原来的成员函数前加一个virtual关键字,我们知道先前的虚继承会在子类中增加一个虚基表指针,但那是用来找父类成员的,而现在子类有了虚函数,对子类对象会有什么变化呢?当我们的子类对象还没继承父类对象时,看看我们子类对象内部的变化。

class B
{
public:

	virtual void Print()
	{
		cout << "A::Print()" << endl;
	}
	int _b = 2;
};

可以看到的除了一个成员变量_b,貌似还多一个地址存在B类对象中,这个地址其实就是虚函数表的地址,也就是说类中如果有虚函数,就会有一张虚函数表,该表存的是能找到函数的方法(可以简单的理解为函数地址,但实际上存的只是一句jump指令的地址,该地址可以帮助我们找到函数)。

 在这里,我们就可以再进一步理解子类的虚函数被父类重写究竟意味着什么。

class A
{
public:
	virtual void Print()
	{
		cout << "A::Print()" << endl;
	}
	int _a = 1;
};
class B:public A
{
public:
	virtual void Print()
	{
		cout << "A::Print()" << endl;
	}
	int _b = 2;
};
void test1()
{
	B b1;
}

B类公有继承A类时,我们发现子类对象b1虚函数表指针也还在,就是增加了一个父类成员变量,那重写究竟发生在哪呢?目前只剩下虚函数表还未查看。

父类对象内存图以及虚函数表

子类对象内存图以及虚函数表

  我们知道有虚函数就会有虚表,那虚表应该存的不仅仅是子类的虚函数,也应该有父类的虚函数,可是我们看见子类对象的虚函数表却只有一个地址。明明自己也有一个虚函数,加上继承而来的虚函数,按理说应该是有两个的。其实重写就发生在这里。父类和子类各有一张虚表,当子类继承了父类时,子类会拷贝一份父类的虚表加入到自己的虚表中,但是当子类的虚函数和父类的虚函数构成重写,那体现在虚函数表就是用子类的虚函数地址替换父类的。

   要注意的是是否是虚拟继承不影响子类对象是否有虚函数表,虚函数表和虚基表是两套概念,不要混为一谈。

三 多态原理

其实在讲虚函数的时候我就已经把多态原理讲得七七八八了,现在结合多态例子再来体会体会。

class A
{
public:
	virtual void Print()
	{
		cout << "A::Print()" << endl;
	}
	int _a = 1;
};
class B:public A
{
public:
	virtual void Print()
	{
		cout << "A::Print()" << endl;
	}
	int _b = 2;
};
void Print(A& a2)  a2是父类的引用,既可以接收父类对象,也可以接受子类对象(继承知识)
{
	a2.Print();
}
void test1()
{
	B b1;
	A a1;
	Print(b1);
	Print(a1);
}

    这就是多态的体现。实现步骤如下: 现在子类b1,父类对象a1,都传对象给父类的引用a2,让父类的引用去调用函数,以前调用普通函数是在编译的时候确定地址,而现在调用虚函数是运行阶段去虚函数表找调用方法,找虚函数表时:如果a2这个父类的引用指向子类对象,那用的就是子类对象的虚函数指针,用这个指针找的虚函数表,所以调用的是子类的虚函数,而当它指向父类对象时,用的是父类对象的虚函数指针,所以也就找到了父类的虚函数表,所以调用的是父类的虚函数。

所以才会有如下的结果,虽然都是a1去调用的Print函数,但是却能根据指向对象去调用不同的函数,妙啊,这就是多态啊。

    这么说来,我们重新定义了虚函数的调用方法——是用虚函数表,这样父类引用调用子类还是父类的虚函数关键看引用指向的对象是谁。那这时候我们再回去理解理解多态条件为什么是那几个?

1 为什么是虚函数:  我认为是要和普通函数区分开,要用虚函数表这种方法才能做到指向父类调用父类的虚函数,指向子类调用子类的虚函数。

2 为什么要调用的虚函数要构成重写,第一 因为我们想让调用父类的虚函数和子类的虚函数用相同的格式,也就是函数名,参数,返回值都一样,如果子类虚函数不要参数,父类虚函数要参数,那我们想一想,给不给函数传参是写代码时就决定的,而这个虚函数要不要参数却要运行才知道,这不就出问题了吗,而且虚函数构成重写后,子类的虚函数地址覆盖父类的虚函数地址,不然两个地址,用谁的。

3 我本来好奇为什么要统一传参给Print,然后在这个函数里调用Print成员函数。

现在我才发现这种方式有多妙,它提供了一种统一的调用方法,而且参数是父类的指针或引用,既可以接收子类对象传参,还可以接收父类对象传参。

void Print(A& a2)
{
	a2.Print();
}

但如果参数a2是父类对象:即便是子类对象赋值给父类对象a2,父类对象a2的虚函数指针仍然指向父类的虚函数表,虚函数表指针不参与复制的过程,所以父类对象也就无法找到子类的虚函数表了。

子类对象就更不行了。如果用子类的引用或者指针,那它就只能接收子类对象,父类对象怎么办,只有父类指针和引用才能既可以指向子类对象又可以指向父类对象。

四 经典例题解析

class A
{
public:

	virtual void fun1(int a=1)
	{
		cout << a << "A::fun1()" << endl;
	}
	int _a = 1;
};

class B:public A
{
public:

	virtual void fun1(int a=2)
	{
		cout <<a << "B::fun1()" << endl;
	}
	int _b = 2;

};
void test1()
{
	B b1;
	b1.fun1();
	A& a1 = b1;
	a1.fun1();
}

结果如下:a1是子类对象的引用,按理说是指向子类的,为什么用的缺省参数却是父类的呢?当然,后面打印的B::fun1()可以证实调用的是子类的函数,a1和b1都是调用子类函数,参数为什么不一样呢?这就牵扯到一个冷门知识了,多态中子类的虚函数会被重写,之前是说重写子类虚函数地址会覆盖父类的虚函数表中对应的虚函数地址,但是实际上父类的函数结构也会对子类的函数结构进行覆盖,但不是说把函数改了(因为当我们用子类对象普通调用的时候,用的缺省参数还是子类的),而是说a1通过虚函数表找到的子类虚函数中如果要用缺省参数,编译器会去找父类的。

   所以我认为重写的仅仅是实现这句话应该只是方便理解的,本来没有多态的时候,那a1调用fun1肯定是父类的函数,但是当有了多态,上面a1.fun1()就变成调用子类的了,就可以简单地理解为把父类的函数体覆盖了,重写了,但函数结构,例如缺省参数用的是父类的。实际上成员函数就一份,怎么会被改呢,只是我们把调用的函数地址改成了子类的,当要用缺省参数时,编译器从父类那里拿呗。

五 多继承下的虚函数表

之前子类虚函数表都只是单继承下的,如果是多继承,那情况则会更复杂。

   我们已经知道如果一个类有虚函数,那该类的对象就会有一张虚函数指针,继承给子类的时候虚函数指针也会被当成父类成员被继承,所以多继承中如果A,C父类都有虚函数,那在子类B中就一定会有两张虚表。

多继承随之而来还有个问题,如果B类有一个虚函数fun1,两个父类都有个虚函数fun1,重写的时候会不会把子类的fun1的地址在两张虚表各放一份呢?虽然虚表里显示的地址不一样,但其实都是调用一个子类的函数(这个要看汇编,并且一步步调试,真加入到博客进来阅读量会很大,有兴趣的可以私信)。

​
class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1()" << endl;
	}
	int _a = 1;
};
class C
{
public:
	virtual void fun1()
	{
	cout << "C::fun1()" << endl;
	}
	int _c = 3;
};
class B:public A,public C
{
public:
	virtual void fun1()
	{
		cout << "B::fun1()" << endl;
	}
	int _b = 2;
};

​

 再提一下,如果子类还有多的虚函数也会放入第一个虚表,当然如果子类有和C中的虚函数构成重写,就放到第二张表格去了。(vs下浅浅测试过)。

六 额外知识补充

到这就轻松多了,也就一些概念要补充。

1 静态多态和动态多态

   其实我们早就接触了多态,函数重载就是静态多态的形式之一,静态多态是指函数在编译时确定地址,而动态多态是在运行时才到虚函数表找的地址。

2 抽象类和纯虚函数

纯虚函数就是在虚函数括号后面加个=0,如果纯虚函数没有函数体,那就得再加个分号,有就不用了。而包含了纯虚函数的就叫抽象类,抽象类是无法实例化出对象的。


class A
{
public:
	virtual void fun1()=0
	{
		cout << "A::fun1()" << endl;
	}
	int _a = 1;
};

class B:public A,  B类继承了A类的纯虚函数,且没有对其重写,那B类也变成抽象类
{
  public:
};
                 C类再继承也得重写了纯虚函数才可以实例化出对象,这是一种强制重写父类虚函数的方式
class C:public B 
{
public:
	virtual void fun1()
	{
	cout << "C::fun1()" << endl;
	}
	int _c = 3;
};

间接强制重写父类虚函数用的是override关键字。

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

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

相关文章

6.文件实现

第四章 文件管理 6.文件实现 ​   连续分配方式&#xff1a;逻辑上相邻的块在物理上也必须相邻&#xff0c;也必须是占有一组连续的块并且依然需要保持这些块之间的相对顺序。 在连续分配方式下为了实现逻辑块号到物理块号之间的映射关系&#xff0c;在文件的目录表中必须记…

JAVA语言:什么是懒加载机制?

JVM没有规定什么时候加载,一般是什么时候使用这个class才会什么时候加载,但是JVM规定了什么时候必须初始化(初始化是第三步、装载、连接、初始化),只要加载之后,那么肯定是要进行初始化的,所以我们就可以通过查看这个类有没有进行初始化,从而判断这个类有没有被加载。 …

文件批量重命名怎么去括号?

文件批量重命名怎么去括号&#xff1f;平时我们一个一个修改文件名称的时候&#xff0c;是不会有括号的。但如果你使用传统的方法来进行文件批量重命名&#xff0c;那么最后得到的文件名是这样的“音频 (数字编号)”&#xff0c;这些文件的名称中会包含一个中文括号。这这个括号…

8.15起 webserver笔记

XShell 远程连接 XFTP 文件传输 VSC远程连接虚拟机&#xff0c;vim编辑器用起来不方便&#xff1a; 查看虚拟机IP地址&#xff1a; MY&#xff1a; 192.168.42.138 VSC每次都要密码&#xff0c;配置免密登录&#xff1a; 在本机命令行生成用户私钥&#xff1a;

JDBC连接数据库(mysql)

准备jar包 官网下载即可&#xff0c;这里提供两个我下载过的jar包&#xff0c;供使用 链接&#xff1a;https://pan.baidu.com/s/1snikBD1kEBaaJnVktLvMdQ?pwdrwwq 提取码&#xff1a;rwwq eclipse导 jar包: 导入成功会有如下所示&#xff1a; ---------------------------…

LeetCode ACM模式——二叉树篇(二)

刷题顺序及思路来源于代码随想录&#xff0c;网站地址&#xff1a;https://programmercarl.com 二叉树的定义及创建见&#xff1a; LeetCode ACM模式——二叉树篇&#xff08;一&#xff09;_要向着光的博客-CSDN博客 目录 102. 二叉树的层序遍历 利用队列 利用递归 10…

sql类型-用户定义表类型

一、创建用户定义表类型String_Table_Type CREATE TYPE String_Table_Type AS TABLE ( Id nvarchar(200) NOT NULL ) GO DECLARE test String_Table_Type INSERT INTO test VALUES(a),(b),(c) SELECT * FROM test 二、SqlSugar中使用

VBA manual

VBA MACRO 修复乱码打开VBAAlt F11File/Options/Customize Ribbon 修复乱码 Tools / Options Control Pannel / Region 打开VBA Alt F11 快速打开VBA File/Options/Customize Ribbon

融云:以对话为场景本质,AIGC 将如何改变游戏规则

8 月 17 日&#xff08;本周四&#xff09;&#xff0c;融云直播课从排查问题到预警风险&#xff0c;社交产品如何更好保障体验、留住用户&#xff1f;欢迎点击报名~ 生成式 AI 公司 MosaicML 以约 13 亿美元的价格被大数据巨头 Databricks 收购&#xff0c;这个发生于 6 月底的…

了解51单片机

目录 51单片机名字的由来 主要功能 1.控制处理 2.数据处理 3.通信 4.定时计数 51单片机的组成 1.中央处理器CPU 2.存储器RAM、只读存储器ROM 3.I/O口和中断系统 4.显示驱动电路、A/D转换器 5.定时器/计数器、脉宽调制电路、模拟多路转换器等电路 单片机的应用领域(…

“探索超前的Pinia:解密Vue.js最新热门状态管理库“

在Vue.js开发者的世界中&#xff0c;一个令人兴奋的新宠儿已经崭露头角&#xff0c;它就是Pinia。对于那些在状态管理方面追求卓越的人来说&#xff0c;Pinia是一片沃土&#xff0c;可以帮助你构建出令人叹为观止的应用程序。无论你是一名有经验的开发者&#xff0c;还是刚入门…

《开放加速规范AI服务器设计指南》发布,应对生成式AI爆发算力挑战

8月10日&#xff0c;在2023年开放计算社区中国峰会(OCP China Day 2023)上&#xff0c;《开放加速规范AI服务器设计指南》&#xff08;以下简称《指南》&#xff09;发布。《指南》面向生成式AI应用场景&#xff0c;进一步发展和完善了开放加速规范AI服务器的设计理论和设计方法…

小白到运维工程师自学之路 第七十五集 (Kubernetes 企业级高可用部署)2

8、添加master节点 在k8s-master2和k8s-master3节点创建文件夹 mkdir -p /etc/kubernetes/pki/etcd在k8s-master1节点执行 从k8s-master1复制密钥和相关文件到k8s-master2和k8s-master3 scp /etc/kubernetes/admin.conf root192.168.77.15:/etc/kubernetes scp /etc/kubernet…

TPAMI, 2023 | 用压缩隐逆向神经网络进行高精度稀疏雷达成像

CoIR: Compressive Implicit Radar | IEEE TPAMI, 2023 | 用压缩隐逆向神经网络进行高精度稀疏雷达成像 注1:本文系“无线感知论文速递”系列之一,致力于简洁清晰完整地介绍、解读无线感知领域最新的顶会/顶刊论文(包括但不限于Nature/Science及其子刊;MobiCom, Sigcom, MobiSy…

〔011〕Stable Diffusion 之 解决绘制多人或面部很小的人物时面部崩坏问题 篇

✨ 目录 &#x1f388; 脸部崩坏&#x1f388; 下载脸部修复插件&#x1f388; 启用脸部修复插件&#x1f388; 插件生成效果&#x1f388; 插件功能详解 &#x1f388; 脸部崩坏 相信很多人在画图时候&#xff0c;特别是画 有多个人物 图片或者 人物在图片中很小 的时候&…

【编织时空二:探究顺序表与链表的数据之旅】

本章重点 链表 链表的结合实现 顺序表和链表的区别和联系 1.链表 顺序表的问题及思考 顺序表的优点&#xff1a; 顺序表中的元素在内存中是连续存储的&#xff0c;因此可以通过索引直接访问任意位置的元素。顺序表尾插尾删操作实现简单。 问题&#xff1a; 中间/头部的插入…

我的创作纪念日+【MySQL】- 08 影响MySQL性能的配置参数

我的创作纪念日【MySQL】- 08 影响MySQL性能的配置参数 写在前面我的创作纪念日 mysql 优化服务器设置1.创建MySQL配置文件2.InnoDB缓冲池&#xff08;Buffer Pool&#xff09;3.线程缓存4.表缓存5.InnoDB I/O配置&#xff08;事务日志&#xff09;6.InnoDB并发配置7.优化排序&…

《电路》基础知识入门学习笔记

文章目录&#xff1a; 一&#xff1a;电路模型和电路规律 1.电路概述 2.电路模型 3.基本电路物理量&#xff1a;电流、电压、电功率和能量 4.电流和电压的参考方向 5.电路元件—电阻 6. 电路元件—电压源和电流源 7.受控电源 8.基尔霍夫&#xff08;后面都要用这个方法…

G1的原理整理

有道云笔记 G1垃圾收集器是JDK7 update 4&#xff08;2011年7月7日&#xff09;引入的一款垃圾收集器&#xff0c;全称Garbage-First Garbage Collector&#xff0c;G1是一个分代的&#xff0c;增量的&#xff0c;并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在…

一篇讲明白,配电柜如何精准监测

当今社会&#xff0c;电力作为现代生活和工业生产中不可或缺的重要能源&#xff0c;扮演着关键的角色。为了确保电力系统的可靠供应和高效运行&#xff0c;配电柜作为电力系统的核心组件之一&#xff0c;具有着重要的地位。 因此&#xff0c;配电柜监控系统在确保稳定的电力供应…