C++多态

目录

1.多态的概念

1.1运行时多态

[1.2 编译时多态](#1.2 编译时多态)

2.多态的定义以及实现

[2.1 多态构成的条件](#2.1 多态构成的条件)

[2.2 虚函数](#2.2 虚函数)

[2.3 虚函数的重写/覆盖](#2.3 虚函数的重写/覆盖)

[2.3.1 虚函数重写的两个例外](#2.3.1 虚函数重写的两个例外)

1.协变

2.析构函数的重写

[2.4 override 和final关键字](#2.4 override 和final关键字)

[2.5 重载/重写/隐藏的对比](#2.5 重载/重写/隐藏的对比)

​编辑

[3. 抽象类 和纯虚函数](#3. 抽象类 和纯虚函数)

4.多态的原理

[4.1 动态绑定与静态绑定](#4.1 动态绑定与静态绑定)

[4.2 虚函数表](#4.2 虚函数表)

[4.3 虚函数表存放在了哪里?](#4.3 虚函数表存放在了哪里?)


1.多态的概念

1.1运行时多态

多态多态,顾名思义,多种状态,什么的多种状态?函数的多种状态。即当你传不同的对象时,虽然调用的是同一个函数,但其实这个函数执行的行为是不同的。举个例子:买票的时候,有学生票,成人票。票就是要传的对象,而你的买票的地方是函数,虽然都是买票的,但是对于学生,是学生票,对于成人,是成人票。(行为不同)以上说的是动态多态(运行时多态)。当然,多态还有静态多态(编译时多态)。

1.2 编译时多态

编译时 多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的 函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在 编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。

来看一个代码图片吧。

没学多态之前是不是认为,这个输出语句调用的是同一个函数,但其实不是的,其实是调用了两个函数(因为变量的类型不同),这就是多态。

2.多态的定义以及实现

2.1 多态构成的条件

定义:是⼀个继承关系的下的类对象,去调用同⼀函数,产生了不同的行为。

条件:

1.必须是基类的指针或者引用调用的虚函数。

2.被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

还有几点要注意一下:

要实现多态效果,第⼀,必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类 对象又指向派生类对象;第二,派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派 生类之间才能有不同的函数,多态的不同形态效果才能达到。

观看上面的代码图片,可知:ptr是基类的指针或者引用吧,并且调用的还是虚函数吧,并且子类的虚函数还完成了重写。

注意:调用虚函数的为父类的指针或者引用,你传递的实参是父类定义的对象就调用父类的函数,传递的实参是子类定义的对象就调用子类的函数,(这个很重要)

在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

2.2 虚函数

前面老说虚函数,这到底是什么呢?可以这么说,虚函数是为了多态才出现的,同样,重写也是为了多态才出现的。

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

2.3 虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有⼀个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值 类型、函数名字、参数列表完全相同(这个指的是参数的类型,参数有无缺省值不影响)),称派生类的虚函数重写了基类的虚函数。

OK,来看一个题:

OK,咱们来分析一下这道题,先看main函数,p是指向B类的,所以可以认为p是不是属于B的。那么p调用test函数。但是这个test函数是A类里的呀,好,B继承了A,所以B类定义的指针也可以调用test函数。test函数里有个func函数。

那么这个func函数到底是A类的this指针调用的,还是B类的this指针调用的呢?是A类this指针调用的,因为継承不能说把this指针都给继承过去吧,那就太扯淡了。正好,回顾一下多态的条件,这里是不是构成了多态?是的。但是,这里是B类的指针调用的test函数,所以应该打印B类的虚函数,但是呢,B类虚函数前面没有virtual呀?别着急,咱们前面说的,这样也可以,因为此时是继承了A类的函数名,返回类型,参数列表+B类的函数体。所以说,这里的val是1呀,不是0.所以说答案是B->1。

2.3.1 虚函数重写的两个例外

1.协变

咱们知道,只有返回类型,函数名,参数列表相同的才可以叫做重写,但是协变不同,协变可以让返回值类型不同,也可以构成重写。但是需要注意几点:。即基类虚函数返回基类对象的指针或者引 用,派生类虚函数返回派生类对象的指针或者引用。

当然,这里不仅仅可以返回当前基类和子类的类型,还可以返回其他有继承关系的类和类型。

2.析构函数的重写

析构函数的重写,析构函数名统一处理成destructor。

这里本人也分为几个部分,先来看第一个部分:

咱们先来看堆上申请的空间里存放资源的析构。这里delete会调用析构函数对吧。那么先来看第一个图,如果不加上virtual,那么这俩析构函数就不构成多态,由于他俩都是A类的指针,所以说只会析构A类中的资源。但是,假如B类中有资源,那么你只析构A类的,不管B类的,是不是会造成资源泄露的安全问题呀,所以,这种不构成多态的方法不可取。

那么第二张图就是构成了多态的情况,这种情况才是正确的情况。(即基类中的函数要设计成 虚函数,为了构成多态)。这种情况下,B类的析构函数是A类析构函数的重写(编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 virtual修饰,派⽣类的析构函数就构成重写。)那么就可以同一个函数实现两种不同的行为了。

这里的第二个new B前面写A*,单纯是为了演示第一种不是多态的情况。还有就是子类的析构函数,调用完子类的析构,会再自动的调用基类的析构函数。

第二个就是对于栈上开的空间,执行完直接析构了(需要遵守子类的先析构,父类的再析构)。并且,这里有人就会说了,这里没有virtual,析构函数的名字又相同,不是会构成成员函数隐藏吗?不是只会调用子类的析构函数吗?不不,在这里,析构函数不遵守什么隐藏规则,不然就资源泄露了,完蛋了就。

2.4 override 和final关键字

从上⾯可以看出,C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,比如函数名写错参数 写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结 果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让 派生类重写这个虚函数,那么可以用final去修饰。

这个没什么好说的,就是对于一些极端情况发生的检查。

2.5 重载/重写/隐藏的对比

3. 抽象类 和纯虚函数

在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被 派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例 化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类纯虚函数某种程度上强制了 派生类重写虚函数,因为不重写实例化不出对象。

可以看出来,派生类中对虚函数进行了重写,那么这个派生类实例化对象就不会报错了。

虚函数就是为了多态而出现的,所以不必要时没必须要写虚函数。

4.多态的原理

先来看一段代码,这段代码是在x86环境下(地址大小为4字节)。

这段代码的大小为12字节,不对呀,按理说,不应该是8字节吗?怎么哪多的4字节?这个4字节就是指向虚函数表的指针的大小 。你看,前面有个_vfptr,ptr咱知道,指针嘛,但是vf是啥呀,v是virtual,f是function。其实,按理说应该还有一个t的,代表table(表),即完整的应该是_vftptr。

这个东西就是虚函数表,一个含有虚函数的类中都至少要有一个虚函数表,并且这个类中的所有虚函数的地址都必须要放在虚函数表中。

所以上面咱们看到的那个买票的的原理,底层运行时到指向的对象的虚表中确定对应的虚函数的 地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函 数。

4.1 动态绑定与静态绑定

• 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用 函数的地址,叫做静态绑定。

• 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数 的地址,也就做动态绑定。

4.2 虚函数表

1.基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同⼀张虚表,不同类型的对 象各自有独立的虚表,所以基类和派生类有各自独立的虚表。

2.而这个派生类,派生类由两部分组成,一个是继承基类的成员,一个是自己的成员,那么指向基类的虚函数表的指针跟指向派生类中基类成员的虚函数表指针不是同一个!并且,派生类中重写的基类的虚函数成员与派生类中自己的虚函数成员共用同一张虚函数表,但是这个虚函数表不是基类的那个虚函数表!

来看这个代码图片,可知,基类的虚函数表跟派生类的虚函数表不是同一个,并且,看红色的2,他俩的地址是不是没啥变化,就是因为,这个fun函数在派生类中没有被重写,也就是覆盖,所以说,fun函数的地址没变,但是BuyTicket的函数地址就变了,也就验证了覆盖这个词,即地址都不一样了,肯定是覆盖了。

即:派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函 数地址。

3.派生类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,派生类 自己的虚函数地址三个部分。

4.虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函 数的地址又存到了虚表中。

4.3 虚函数表存放在了哪里?

先来看这段代码,注意:先说明一下,指向虚函数表的指针,咱们如何打印出他的具体的值呢?这个其实非常简单,直接用强转成int*的指针再打印就可以了(注意, 只有指针才可以在不同的类之间进行大跨度的强转,你要是将person类强转成int型的,铁铁的不可以!)

所以,可以看出,这个虚函数表的地址跟常量区的地址非常相似,也就是说虚函数表是存放在常量区的。并且虚函数的地址跟虚函数表的地址非常近,所以说虚函数的地址是存放在虚函数表里的。

OK,本篇完......................

相关推荐
阳光_你好4 分钟前
C++/Qt中QActionGroup类用法
c++·qt
T.Ree.12 分钟前
【数据结构】_树和二叉树
c语言·开发语言·数据结构
夜夜敲码23 分钟前
C语言教程(十五):C 语言函数指针与回调函数详解
c语言·开发语言
Cao12345678932125 分钟前
判断是否为闰年(C语言)
c语言·开发语言
菜鸟射手28 分钟前
QT creater和vs2017文件路径问题
linux·c++·windows·qt
wuqingshun31415935 分钟前
蓝桥杯17. 机器人塔
c++·算法·职场和发展·蓝桥杯·深度优先
是发财不是旺财36 分钟前
跟着deepseek学golang--认识golang
开发语言·后端·golang
simple_whu1 小时前
解决编译pcl时报错‘chrono_literals‘: is not a member of ‘std‘
c++·windows·visual studio
Bruce_Liuxiaowei1 小时前
基于Python+Flask的MCP SDK响应式文档展示系统设计与实现
开发语言·python·flask·mcp
chuxinweihui1 小时前
数据结构——栈与队列
c语言·开发语言·数据结构·学习·算法·链表