目录
一、什么是多态?
- 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态 ,
- 例如:买车票,有人买的是全票,而有的人是学生票
多态在代码中的体现
我们先来看下一个代码例子:
我们先定义一个动物基类 A
再来定义一个person类和dog类,继承A
再写一个接口函数
我们分别实例化A,person,dog类的对象,去调用方法
发现调用的都是父类的方法,没有调用自己类(子类)的方法,这显然和我们的初衷相违背了
我们试着在原来的函数前面加上virtual关键字
这时候再来调用一下方法:
这时候就发现不同的对象调用的不同的方法,这就是多态,通过相同的接口,实现不同的结果
二、C++中多态的实现条件
- 1.必须要处于继承体系下。
- 2.基类中必须要有虚函数(被virtual关键字修饰的成员函数称为虚函数),在子类中必须要对基类中的虚函数进行重写
- 3.虚函数调用必须要通过基类的指针或者引用来进行调用。
浅分析下多态的底层
我们先不写virtual来看下状态
这时候的对象成员只有一个m_a
我们再来看看加上virtual的情况
我们发现b对象里面多了个_vfptr
我们再来写一个虚的show函数,看有没有第二个虚表指针的出现
我们发现并没有
也就是说一个类正常情况下只有一个虚表指针
多态的实现
重写:要求满足三同,名字相同,返回值相同,参数列表相同
虚:一虚到底
只要父类写了virtual,那么不管子类写不写virtual,都会是虚函数,(当然为了可读性,子类写上是最好了)
纠正一下多态的思想
我们定义了一个父类的指针指向了子类,这时候调用pb->fun()调用的是子类的方法,有的人可能认为这是利索应当的,
认为pb本来就指向的是子类,调子类的方法也是正常,但其实是错误的
因为在继承的时候就学习到了,父类对象的指针之所以能指向子类对象,正是因为子类中有父类,这个指针只是想要父类的部分,子类的部分是不关注的,所以按道理其实是访问不了子类的内容的,调用子类的方法就是多态的原理
我们研究的正是这个本不合理现象的原因,这就是多态
虚表的特性
1.相同类型的不同对象会共用同一张虚表
2.子类当中只有一张虚表(单继承)
虽然在宏观意义上,子类对象当中确实是有着父类对象,但是我们看对象的时候是看整体的,
子类和父类各自有各自的虚表空间。不能理解为:子类中有两张虚表,父类一个,子类单独有一个
三,多态产生的底层分析
我们从构造对象的角度来进行分析
我们现在有一个父类Base和一个子类D,我们实例化一个子类对象d,观察一下在底层是怎们进行构造的
我们都知道,在构造子类之前会先去构造父类,
那么我们来看看在父类中,这个底层结构是怎样的
父类中有个_vfptr和m_a,相当于就是:
构造完成父类后,会转到子类中去进行构造,我们来看看在子类中是怎么样的
在子类中我们可以看到对象的地址(this)是没有改变的,
但是父类Base中的_vfptr的地址发生了改变,这个虚表里的函数地址也发生了改变,这是什么情况呢
我们来分析一下底层
子类拷贝之后还是要做变化的 ,要执行覆盖的操作
这个覆盖的过程父类是感受不到的
这步操作也解释了为什么重写的时候要“三同”,因为多态其实就是一个狸猫变太子的事,要做到用狸猫去换太子,最起码长相要一模一样,才能去换。只有三同了,才能“欺骗”到父类
这个过程同样也解释了父类的指针为什么能“错误的”调用子类的函数
总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基 类中某个虚函数,用派生的类自己虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在 派生类中的声明次序增加到派生类虚表的最后。
四、虚函数重写的两个例外
1、协变
2. 析构函数的重写(基类与派生类析构函数的名字不同)
当我们正常写的时候会发现,子类的析构函数不会被调用,
但当我们给父类的析构函数加上一个virtual的时候,就会发现子类的析构函数被成功调用了
final
- 我们在程序中有可能会将要重写函数的名字写错,或者参数或者返回值写的不一致,但是我们自己难以发现,编译器在编译阶段并不会报错。这样我们就无法构成重写。这里为了解决这个问题C++11中给出了关键字override。
- override:C++11中新增关键字,目的在编译阶段来检测被override修饰的函数是否对其基类的虚函数进行重写,如果重写成功编译通过,否则报错
注意:
- 1.这里只能修饰虚函数
- 2.只能修饰子类的虚函数,因为如果修饰基类的虚函数是没有意义的,基类他也不对别人进行重写。
-
override
-
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
重载,重写,重定义三个概念的区分
五、抽象类
1.抽象类的意义
我们思考这样一个问题,上面我们创建的基类和派生类,基类是队伍类,能够实例化出一个动物类的对象其实是不合理的,因为没有任何一种生物叫做"动物",且类中有eat的方法,只有具体的一种动物才能吃,那么我们创建一个动物类的对象是毫无意义的
因为没有实现具体的方式,那么此时我们就要将虚函数设置为纯虚函数。
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
-
2.纯虚函数要注意的点
- 1.纯虚函数可以有函数体,但是是没有意义的。一般直接将函数名后面加=0
- 2.抽象类是不可以实例化对象的,因为这个抽象类不是什么具体的类是无法实例化对象的,但是抽象类可以创建指针和引用。
- 3.抽象类是一定要被继承的,而且在子类中必须要对抽象类中所有的虚函数进行重写。
抽象类不能实例化出对象,可理解为他太抽象了,不能用具体的对象来实例出来
六、早期绑定&晚期绑定
早期绑定
往往针对的是函数重载
程序在编译期间就已经确定了函数的行为。
- 例如:函数重载:对一个函数重载之后将不同的参数传入函数,就会调用不同的函数重载,在编译期间就确定要调用的函数
- 模板:模板也同样是,当我们将参数传入模板函数,或者模板类,他会给我们实例化出对应的函数或者类。
用一个例子来理解早期绑定
晚期绑定
晚期绑定往往针对的是多态
程序运行时才可以确定函数的行为,即在编译期间无法确定到底要调用那个函数
在这个图种,这个框就相当于是fun函数,不同的对象相当于不同的女孩,是感知不到最终要和谁结婚的
七、真实的得到虚表中的函数
我们先来看一个现象
我们定义了一个子类对象,我们这时候想要对其进行初始化,我们使用memset函数进行初始化
我们这时候运行一下
但却发生了报错,这是为什么呢?
我们发现,这个函数在初始化的时候连对象的虚表指针都给“敢翻了”,这时候连虚表都没有了,更别提多态了
取到虚表的地址
我们再通过虚表,取到具体的每个函数
八、深度剖析一下多态的底层
在父类对象中看虚表
在子类对象中看虚表
我们能够看到,子类的虚表中多了两个地址,我们能够猜到这是子类自己的虚函数地址
我们把子类独有的虚函数改为一个,观察是否只是多了一个地址
这时候我们就能确定,多出的地址确实是存放子类自己的虚函数
我们也可以用取函数虚表函数的方式来再次确认一下
可以看到,能够完全确认多出来的地址存放的确实是子类的虚函数
九、对象虚表的内存模型是什么样的
1.单继承
在面试的时候,有时候会问到对象的内存模型是怎么样的,其实就是问虚表在对象中是怎么存储的
无覆盖现象
子类虚表的内存模型如下
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面
有虚函数覆盖
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
2.多继承
1.没有覆盖
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
2.有覆盖
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了