C++多态的概念与使用(详细介绍)

概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
下面以学生买票为例说明多态的特性。
先加上两个知识点

重载与模板都属于多态,为静态多态

在这里插入图片描述
在这里插入图片描述

1.多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。
Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用指向子类对象
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

1.1虚函数

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

1.2多态的实现

在这里插入图片描述
由图可知,当不使用虚函数时,分别创建Person类对象,和Student类对象调用func函数,由于函数的参数是Person类,Person类对象调用
func函数会正常调用Person的Buyticket成员函数;而在Student类对象调用参数为Person类的func函数时,会发生子类向父类的赋值转换,
形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
在这里插入图片描述
当在父类和子类buyticket函数前加virtual,满足了子类的虚函数重写了基类的虚函数的条件。
形成多态的两个条件缺一不可(见多态构成条件)
如果func参数更换为Student类就违背了多态构成条件第一条:必须通过基类的指针或者引用调用虚函数
如果Person和Student类的返回值类型、函数名字、参数列表有一个不相同则不满足多态构成条件第二条:被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
在这里插入图片描述
在这里提出一个问题:析构函数是虚函数,是否构成重写?
答:构成,因为C++为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,满足虚函数的条件
补充继承方面知识点:派生类(子类)的析构函数会在被调用完成后自动调用基类(父类)的析构函数清理基类成员。因此Person的析构会被调用两次。

派生类的默认成员函数

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。

在这里插入图片描述
在这里插入图片描述

解释析构为什么尽量设置为虚函数场景:为了调用子类的析构释放子类的资源

但是,当动态申请的对象,如果给了父类指针管理析构函数是否需要是虚函数呢?
下图注释了new 和 delete的调用原理:
在这里插入图片描述
由上图可知父类指针管理子类的情况下,析构函数调用的都是父类的析构函数。
如果想要调用子类的析构函数的话,该怎么实现呢?
参考多态的构成条件,第一条满足(通过基类的指针或者引用调用虚函数)
第二条未满足(函数并不是虚函数)
因此只需在析构函数前加virtual即满足了多态构成的所有条件。
在这里插入图片描述
给出结论:
在这里插入图片描述

1.3虚函数重写的例外(了解即可)

协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
在这里插入图片描述

1.4C++11 override 和 final

  1. final:修饰虚函数,表示该虚函数不能再被重写
    在这里插入图片描述
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
    在这里插入图片描述

1.5重载、覆盖(重写)、隐藏(重定义)的对比重点

在这里插入图片描述

2.抽象类(接口类)

概念:在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
在这里插入图片描述

抽象类为什么不能被实例化?

从设计层面来看:因为在设计的时候,为了实现多态,当某些类只希望作为父类使用,不希望被实例化。以水果和苹果为例
也就是我们从上层设计角度,就不希望有些类被实例化。

2.2接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

虚基类

当发生菱形继承,D中会有BC两个相同的对象,造成数据冗余,因此让BC虚拟继承A, 这时的A成为虚基类。
一定不要搞混抽象类和虚基类的概念!!
在这里插入图片描述

3.多态的原理

3.1虚函数表指针

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
在这里插入图片描述
在这里插入图片描述
观察得出结论:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:
    a.先将基类中的虚表内容拷贝一份到派生类虚表中
    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

注意!!!:注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
总结:对象中存的是虚表指针—>通过虚表指针找到虚表----->虚表中存的是虚函数的指针!!
因为虚表里面有指针,所以类的大小要多出指针的大小
在这里插入图片描述

补充一个重要的知识点,是通过观察反汇编看出来的:
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

3.2动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

4.多态的常见问题

1.析构函数可以是虚函数吗?为什么
答:前面有解释,只要使用的场景想到了就会解释的清楚。

2.构造函数可以是虚函数吗?
答:不可以,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

3.静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,因为我要去头上四个字节是找到虚表,没有this指针找不到

4.inline函数可以是虚函数吗?
内联函数定义
答:严格来说不可以,因为内联函数是没有地址的,会再调用它的地方展开,虚函数是需要地址才能找到的,因此编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。

5.虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,虚表指针在构造初始化列表阶段初始化,一般情况下存在代码段(常量区)的。

6.什么是抽象类?
答:上面有解释

7.为什么只有多态只能使用引用和指针?
答:如果仅仅只是赋值传参,会发生继承的切片,子类会将属于父类的那部分拷贝回去,而子类的虚表是属于子类的,因此父类对象看不到子类的虚表,因此就无法形成多态。
而指针和引用直接指向的是子类的地址,因此可以访问到子类的虚函数表指针形成多态。

8.我的两个类AB没有继承,通过指针强转也能形成多态呀?
在这里插入图片描述
答:首先,如果你把A的print换个名字就会编译报错,这只是完全的巧合。哪怕在这种巧合的情况下,强转确实能访问到该到B的虚表,多态也确实是通过访问虚表实现的,但我们说的多态是建立在继承的基础上,希望的是某个行为,当不同的对象去完成时会产生出不同的状态,既然你不想形成多态为什么还要写虚函数让他生产虚表呢?对吧。

9.为什么多态只能父亲指向子类而不能子类指向父类?
答:通常来说,子类总是含有一些父类没有的成员变量,或者方法函数。而子类肯定含有父类所有的成员变量和方法函数。所以用父类指针指向子类时,没有问题,因为父类有的,子类都有,不会出现非法访问问题。
但是如果用子类指针指向父类的话,一旦访问子类特有的方法函数或者成员变量,就会出现非法访问。
非法访问后果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值