C++之多态

目录

一、什么是多态?

多态在代码中的体现

二、C++中多态的实现条件

浅分析下多态的底层

 多态的实现

纠正一下多态的思想

虚表的特性

三,多态产生的底层分析

总结一下派生类的虚表生成:

 四、虚函数重写的两个例外

1、协变

2. 析构函数的重写(基类与派生类析构函数的名字不同)

 final

override

重载,重写,重定义三个概念的区分

 五、抽象类

 1.抽象类的意义

2.纯虚函数要注意的点

六、早期绑定&晚期绑定

早期绑定

晚期绑定

七、真实的得到虚表中的函数

取到虚表的地址

八、深度剖析一下多态的底层

在父类对象中看虚表

在子类对象中看虚表

九、对象虚表的内存模型是什么样的

1.单继承

 2.多继承


一、什么是多态?

  • 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态 ,
  • 例如:买车票,有人买的是全票,而有的人是学生票

多态在代码中的体现

我们先来看下一个代码例子:

我们先定义一个动物基类 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 关键字,都与基类的
析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规
则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处
理成 destructor

 当我们正常写的时候会发现,子类的析构函数不会被调用,

但当我们给父类的析构函数加上一个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()了

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值