-
目录
1.什么是多态?
2.多态的分类
2.1.静态多态(也可以称为:静态绑定||早绑定)
2.2.动态多态(也可以称为:动态绑定||晚绑定)
3.C++中动态多态的实现条件
4.多态的体现
5.什么是重写
测试各种情况下是否构成重写:
大总结一下构成重写的具体需求。
重载,重写,重定义三个概念的区分
6.override关键字:
7.final关键字
final使用需要注意的点:
8.抽象类
8.1.抽象类的意义
8.2.纯虚函数要注意的点
8.3.接口继承和实现继承
9.多态实现原理
9.1.首先我们来看包含虚函数类的大小和不包含虚函数的大小
9.2.包含有纯虚函数的类对象模型
9.2.1基类虚函数的构建规则:
9.2.2.子类虚表的构建规则
9.2.2.1.子类继承模型:
9.2.2.2.子类继承后的大小
9.2.2.3.如果子类重写了基类的虚函数,就用子类自己的虚函数替换虚表中相同偏移量位置的基类虚函数的入口地址。
9.3.虚表构建原理
9.4.虚函数调用原理
9.5.多继承中子类的对象模型和大小
1.什么是多态?
- 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。例如同样是动物都会叫,但是不同的动物叫的时候就会发出不同叫声。
-
2.多态的分类
-
2.1.静态多态(也可以称为:静态绑定||早绑定)
- 程序在编译期间就已经确定了函数的行为。
- 例如:函数重载:对一个函数重载之后将不同的参数传入函数,就会调用不同的函数重载,在编译期间就确定要调用的函数。
- 模板:模板也同样是,当我们将参数传入模板函数,或者模板类,他会给我们实例化出对应的函数或者类。
-
2.2.动态多态(也可以称为:动态绑定||晚绑定)
- 程序运行时才可以确定函数的行为,即在编译期间无法确定到底要调用那个函数。
-
3.C++中动态多态的实现条件
- 1.必须要处于继承体系下。
- 2.基类中必须要有虚函数(被virtual关键字修饰的成员函数称为虚函数),在子类中必须要对基类中的虚函数进行重写
- 3.虚函数调用必须要通过基类的指针或者引用来进行调用。
- 接下来我们来举一个多态的例子:
- 让代码运行起来我们看到我们只写了一份代码,但是在运行阶段却出现了三种结果。
-
4.多态的体现
- 在程序运行时,根据基类的指针或者引用指向不同类的对象,选择合适的虚函数进行调用。
-
5.什么是重写
- a.必须在继承的体系当中
- b.基类的成员函数必须是虚函数(子类重写的函数不是虚函数也可以与基类中的函数构成重写)
- c.子类和基类的虚函数的原型(返回值类型 函数名字 参数列表 必须一致)子类中的函数加不加virtual关键字都可以。注意:
- 例外:
- 1.返回值类型可以不同————基类虚函数必须要返回基类对象的指针或者引用:子类的虚函数必须返回子类对象的指针或者引用(这里的基类对象和子类对象可以是不同继承体系的对象,这里的不同继承体系也必须是同一继承体系下的基类和子类,不能说基类返回一个继承体系下的基类,而子类返回另一个进程体系下的子类)
- 2.函数名字也可以不同,但是这里的函数一定是析构函数
- d.与函数的访问权限没有关系,只要函数的原型一致即可。
-
测试各种情况下是否构成重写:
- 第一种情况:基类函数和子类函数都不是虚函数,但是函数原型一样:
- 运行结果:这里其实是构成基类函数同名隐藏
- 第二种情况:子类函数是虚函数,基类不是虚函数,函数原型相同:
- 运行结果:我们看到仍然没有构成重写
- 第三种情况:这次我们仍然还保持函数原型相同,但是这次让基类函数为虚函数,子类函数不是虚函数
- 运行结果:可以看到这里构成了重写
- 总结:那么从这三种情况我们就可以的出一个结论只要基类是虚函数,子类是不是虚函数都可以构成重写。
- 第四种情况:这里我们将基类保持虚函数,然后子类和基类的函数名不同
- 运行结果:这里毋庸置疑,肯定是无法构成重写的,因为函数名字都不一样
- 第五种情况:子类的函数参数列表和基类不同
- 运行结果:这里肯定也是毋庸置疑无法构成重载的
- 第六种情况:我们将子类的返回值和基类的返回值不同的情况看一下
- 可以看到这里直接报错了说:返回类型与重写虚函数的返回类型既不相同也不协变。
- 第七种情况:我们将子类中的函数权限设置为私有,基类中设为共有
- 运行结果:可以看到这里仍然构成重写
- 注意:但是这里注意,这里如果将基类的虚函数设置为pirvate就会导致无法访问,因为我们构成多态是用基类的指针或者引用去调用函数,而在运行阶段形成不同的效果,构成函数重载。
- 第八种情况:这里就是我们上面所说的例外,返回值类型不同————协变
- 运行结果:我们可以看到这里也构成了重写
- 第九种情况:我们返回不同继承体系的基类对象和子类对象
- 运行结果:可以看到这里也构成重写
- 第十种情况:函数名字也可以不同但是这里仅可以存在于析构函数,我们这里将基类的析构函数设置为虚函数,然后创建一个基类的指针,指向子类的指针,然后将其delete掉,然后创建一个子类的对象,然后再将其delete掉,
- 运行结果:我们发现这里同样也构成了重写。至于为什么下面的还会调用基类的析构函数,这是因为子类继承自基类,当子类在调用完自己的析构函数,执行完自己的代码之后就会自动调用基类的析构函数将基类的内容析构。
-
大总结一下构成重写的具体需求。
-
重载,重写,重定义三个概念的区分
-
6.override关键字:
- 我们在程序中有可能会将要重写函数的名字写错,或者参数或者返回值写的不一致,但是我们自己难以发现,编译器在编译阶段并不会报错。这样我们就无法构成重写。这里为了解决这个问题C++11中给出了关键字override。
- override:C++11中新增关键字,目的在编译阶段来检测被override修饰的函数是否对其基类的虚函数进行重写,如果重写成功编译通过,否则报错
- 注意:
- 1.这里只能修饰虚函数
- 2.只能修饰子类的虚函数,因为如果修饰基类的虚函数是没有意义的,基类他也不对别人进行重写。
- 这两修饰种方式都是可以的
-
7.final关键字
- 介绍final关键字之前我们先模拟一个场景
- 这里多层继承,假如我们想自A之后F1()就无法被继续重写了要怎么实现呢?
- 这里就需要掏出我们C++11中新增的final关键字了,我们可以看到在B类中将F1()函数用final修饰之后,C之后是无法再对F1()进行重写的,一重写,在编译阶段就会报错
-
final使用需要注意的点:
- 1.只能修饰虚函数,这里的虚函数是指重写基类的函数,只要子类函数和基类构成重写,就可以修饰。这里也可以修饰基类的虚函数,但是不推荐这样使用我们给出虚函数就是为了构成多态,修饰了基类的虚函数之后,那么下面的子类就无法对其重写了,那么给出虚函数的意义也就不大了。
- 我们来看反例这里用final修饰普通函数,可以看到这里是无法修饰普通函数的,修饰之后就会编译报错。
- 2.final也可以修饰类
- final修饰一个类之后这个类就不可以被其他类继承了。
- 我们在C++98当中其实也可以实现用final修饰类的同样的作用,我们将基类的对象的构造函数设置为私有的,这样做其他类就无法继承,因为构造函数是私有的,其他类的构造函数的参数列表就无法调用。但是这样做我们创建的类就无法创建对象了,就没有什么意义了。
-
-
8.1.抽象类的意义
- 我们思考这样一个问题,上面我们创建的基类和派生类,基类中提供的动物叫的方式,其实是不合理的,因为具体的动物才可以叫,而那么我们创建一个动物类的对象是毫无意义的,因为没有实现具体的方式,那么此时我们就要将虚函数设置为纯虚函数。在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
-
8.2.纯虚函数要注意的点
- 1.纯虚函数可以有函数体,但是是没有意义的。一般直接将函数名后面加=0,加;即可。
- 2.抽象类是不可以实例化对象的,因为这个抽象类不是什么具体的类是无法实例化对象的,但是抽象类可以创建指针和引用。
- 3.抽象类是一定要被继承的,而且在子类中必须要对抽象类中所有的虚函数进行重写。
-
8.3.接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口,在子类中要将虚函数具体实现出来。所以如果不实现多态,不要把函数定义成虚函数。
-
9.多态实现原理
-
9.1.首先我们来看包含虚函数类的大小和不包含虚函数的大小
- 可以看到不包含虚函数类的大小就是原类的大小:
- 我们包含虚函数之后:类大小多出四个字节。
- 包含两个虚函数之后我们发现类大小仍然是8,只多出四个字节。
- 结论:如果类中包含有虚函数(和个数无关),编程器会给对象多增加四个字节,而且多增加的四个字节在对象的4个字节在对象的起始位置-----内部存放的是虚表的地址。
-
9.2.包含有纯虚函数的类对象模型
-
9.2.1基类虚函数的构建规则:
- 在构造阶段编译器为类对象填充四个字节--------》虚表指针
- 结论:虚表中放的是虚函数的地址的顺序和虚函数在类中声明的顺序一致。而且是在构造对象时候填充的,也就是在构造函数中填充的,如果构造函数没有实现编译器会给用户生成一份默认的构造函数,在默认的构造函数中会给对象的前四个字节存放虚表地址。如果我们自己显示实现:编译器会对用户自己实现的构造函数进行修改----增加给对象的前四个字节存放虚表地址的语句。
-
9.2.2.子类虚表的构建规则
-
9.2.2.1.子类继承模型:
- 基类虚表中的内容拷贝一份到子类的虚表中(此时子类和基类的虚表不是同一张)
-
9.2.2.2.子类继承后的大小
-
9.2.2.3.如果子类重写了基类的虚函数,就用子类自己的虚函数替换虚表中相同偏移量位置的基类虚函数的入口地址。
- 我们看监视窗口:
- 我们在子类中新增虚函数,然后查看监视窗口发现监视窗口中并没有显示新增的的虚函数的入口地址。这是为什么呢?是不是子类新增一张虚表来记录新增的虚函数,但是我们调试发现子类的大小仍然是12,那么这个推测显然是不成立的。
- 我们再来查看内存
- 我们已经知道虚表指针指向的内容是一个存放虚函数地址的数组,那么我们可以将数组中的每一个地址获取到,然后调用。
- —————————————————————————————————C语言铺垫知识—————————————————————————————————————
- 这里我们先补充C语言中函数指针相关的知识以免一些读者不了解接下来要做的事情
- 首先我们定义一个函数指针:这个函数指针的类型是一个返回值为空形参为int的函数指针
- 既然是一个函数指针,其本质也是一个指针,我们用它来指向一个具体的对象,那么我们在使用的时候就可以直接向其传入参数即可调用相应的函数。
- 这里我们当然也可以将函数作为一种类型来使用,只要在函数指针前加上typedef就表示这中函数指针的类型,我们可以用来定义变量,这里我们用函数的形参类型来使用,在函数函数指针类型作为函数形参的类型的时候,我们在传参的时候需要将函数名传入,因为函数名就是一个函数指针。
- —————————————————————————————————————结束———————————————————————————————————————
- 好现在我们知道了这些铺垫知识之后,接下来我们将虚表指针指向的虚表中保存的函数指针依次调用,那么这里我们就可以知道虚表中虚拟函数的分布情况:
- 我们可以看到新增的两个虚函数在虚表的最下面
- 所以我们得出结论子类继承了基类的虚函数之后子类的虚表中基类的虚函数在最上面而且按照在基类中声明的次序依次存放,而子类如果新增虚函数就按照其在类中声明的先后次序依次放在虚表的最后面。
- 总结:
- 1.子类将基类中虚表中的内容拷贝一份到子类的虚表当中(这里其实子类在构造时候先将调用基类的构造函数,然后基类的构造函数中将虚函数初始化好了之后,子类将首地址中的虚表指针改为自己的虚表指针)
- 2.如果子类中重写了基类中那个虚函数就将子类中重写的虚函数的入口地址替换相同偏移量位置的基类的虚函数入口地址
- 3.如果子类中新增加了虚函数,那么按照其在子类中声明的先后顺序依次添加到虚表的最后面
-
9.3.虚表构建原理
- 虚表生成时机:编译阶段
- 虚表构成过程:按照虚函数在类中声明的先后次序依次加载到虚表中,同一个类的对象共享的是同一张虚表
-
9.4.虚函数调用原理
- 我们先实现多态
- 然后转到反汇编
- 我们可以看到只要是基类中的虚函数,就会放到虚表中,然后再运行的时候,获取虚表中的虚函数的地址,然后取出调用。调用不是虚函数的话那么就会直接调用。
-
9.5.多继承中子类的对象模型和大小
- 我们先写一个多继承的代码,让子类D继承B1和B2.
- 可以看到基类继承了B1和B2的虚表和成员变量。这里继承之后子类将那个基类中的虚函数重写之后就将那个基类中的虚函数表中虚函数相应的偏移量位置的虚函数入口地址替换为自己重写的虚函数入口地址。而子类新增的虚函数则放在第一张虚表的最后面。
- 可以看到子类中的虚表和基类中的虚表不是同一张虚表,子类将自己的虚表指针指向了基类的虚表。
-