【学习篇】第20期 超详解 C++ 多态:从语法规则到底层原理

目录

开头

ok了,这一期我们继续学习 C++ 三大特性的最后一个------多态,废话不多说,我们直接开始

一.多态的概念

多态( Polymorphism)通俗来说就是 "多种形态"。

多态的本质 是:同一个接口(函数调用),作用于不同的对象,会产生不同的行为

举一个具体的例子:对于买火车票这个行为,普通人买是全价买票;学生买的就是半价票;而军人可能就是优先买票,可以看到,买票的用一个行为,对于不同的人会产生不同的 "行为",而多态就是为了优化这种场景

二.多态的定义及实现

(1)多态的构成条件

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了

Person。Person对象买票全价,Student对象优惠买票。

要实现运行时多态,必须同时满足以下两个条件:

• 必须是用基类的指针或者引⽤来调⽤虚函数

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

问题:为什么必须使用基类的指针或引用来调用虚函数,不能用基类对象调用?

因为基类对象只能容纳基类的成员,无法容纳派生类的成员。当你用派生类对象给基类对象赋值时,会发生切片(Slice),派生类的部分会被切掉,只剩下基类的部分。

对于重写:

重写(Override)也叫覆盖,是指派生类中有一个与基类完全相同的虚函数。"完全相同" 指的是:

• 函数名相同

• 参数列表完全相同(参数类型、个数、顺序都相同

• 返回值类型相同(协变例外)

• 两个函数都必须是虚函数(派生类可以不加 virtual,但基类必须加)

(2)虚函数详解: 多态的基石

在类的非静态成员函数前面加上virtual关键字,这个函数就成为了虚函数

问题:为什么静态成员函数不能是虚函数?

因为静态成员函数不属于任何对象,它没有this指针。而虚函数的调用是基于对象的(通过this指针找到虚函数表指针),所以静态成员函数不能是虚函数

虚函数的重写/覆盖 :派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。(满足上面的重写规则)

注意:派⽣类的虚函数在不加virtual关键字时,也可以构成重写写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),不建议这样使用,但是在考试中这是个容易考的易错点

问题:以下程序输出结果是什么()

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

答案是:B

是不是出乎你的意料,别急我来好好给你捋一捋!

如上图,首先 B 继承了 A,p 是 B (派生类) 的指针,调用了 A 的函数 text ,而text函数是通过 A* (基类的指针类型)接收的,满足了多态的第一个条件(必须是用基类的指针或者引⽤来调⽤虚函数),接着 text 调用 func 函数,而func 函数根据题目,是一个虚函数(缺省值不一样无所谓)

到这一步,有的同学会说,那不是调用 B (派生类) 中的 func, 那不应该选D吗????

这里,我们要再来说说重写了,其实 B 对 A 中 func 函数进行了重写,实则B 中func函数变成了:

重写进行了 A 和 B 的"组合" ,B 中 func 函数重写了 函数的内容 ,如上图,所以答案是 B

(3)虚函数重写的特殊情况

1.协变

协变是虚函数重写的一个例外情况:当基类虚函数返回基类对象的指针或引用 时,派生类虚函数返回派生类对象的指针或引用,仍然构成重写。

协变在实际开发中很少使用,了解即可

2.析构函数的重写

析构函数的重写是一个非常特殊的情况:

• 如果基类的析构函数是虚函数,那么派生类的析构函数无论是否加 virtual,都与基类析构函数构成重写

• 这是因为编译器会对析构函数的名称做特殊处理,编译后所有析构函数的名称都会被统一处理成destructor()

为什么基类析构函数必须设为虚函数?

这是面试中最常问的问题之一,答案是:防止内存泄漏

让我们通过一个例子来理解:

如上图,如果基类 A 中的析构函数不加 virtual 运行结果是什么??

可以看到,只会调用 A 的析构函数,没有调用派生类 B 的析构函数,这样会导致派生类 B 中申请的空间不会被释放,造成了内存泄漏

那为什么将基类中的析构设置为虚函数就可以解决问题???

将基类中的析构设置为虚函数后,就和派生类的析构函数构成了多态关系,在析构 p2 的时候,就会自动去调用 B中的析构函数,调用派生类的析构函数后,编译器会自动调用基类的析构,就解决了内存泄漏的问题

结论:只要一个类可能被继承,它的析构函数就应该设为虚函数

3.C++11:override 和 final 关键字

从上⾯可以看出,C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数

写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的

C++11 新增了 override 和 final 两个关键字,专门用于虚函数重写的检查和控制,极大地提高了代码的健壮性

override : 强制检查重写

override关键字加在派生类虚函数的后面,强制编译器检查该函数是否真的重写了基类的虚函数。如果没有重写(比如函数名写错、参数写错),编译器会直接报错

建议:所有重写的虚函数都应该加上 override 关键字

final : 禁止重写和继承

final关键字有两个用法:

• 加在虚函数后面:禁止派生类重写该虚函数

• 加在类后面:禁止该类被继承

4. 重载、重写、隐藏的终极对比


一句话总结:

• 重载:同一作用域,函数名相同参数不同

• 重写:父子类,函数名参数返回值都相同,都是虚函数

• 隐藏:父子类,函数名相同,且不构成重写

三.纯虚函数和抽象类:定义接口规范

纯虚函数的定义: 在虚函数的声明后面加上=0,这个函数就成为了纯虚函数, 包含纯虚函数的类叫做抽象类


纯虚函数的特点:

  1. 纯虚函数不需要实现(语法上可以实现,但没有实际意义)
  2. 包含纯虚函数的类称为抽象类(也叫接口类)
  3. 抽象类不能实例化对象
  4. 如果派生类继承抽象类后不重写所有纯虚函数,那么派生类也是抽象类,同样不能实例化

作用

注意:纯虚析构函数必须提供实现,因为派生类的析构函数会调用基类的析构函数。

四.多态的底层原理:虚函数表与动态绑定

(1)虚函数指针(vfptr)

我们先来看看下面这个题:

上⾯编译为32位程序的运⾏结果是什么()

A. 编译报错 B. 运⾏报错 C. 8 D. 12

答案是 D ,,除了 _b 和 _ch 成员,还多⼀个__vfptr 放在对象的前⾯(注意有些平台可能会放到对象的最后⾯,这个跟平台有关)

对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
(__vfptr 指向的是函数指针数组)

(2)多态的原理

1.多态是如何实现的

回到文章最开始的买票,都是 ptr -> buyticket, ptr 是 person* ,编译器是如何知道我是要调用 person 中的虚函数,还是 student 中的虚函数呢??

每一个具有虚函数的类中都有自己独立的虚函数表,这个表里存放的是该类中所有虚函数的地址,通过父类的指针来接收目标子类,拿到对应的 __vfptr,通过相对位置就可以找到目标子类的目标虚函数,然后调用

2.动态绑定和静态绑定

这就是动态绑定的完整过程:函数地址不是在编译时确定的,而是在运行时,根据指针指向的对象的实际类型,到对应的虚函数表中查找得到的。

3.虚函数表

虚函数和虚表存在哪里?

• 虚函数:和普通函数一样,存在代码段 (也叫文本段)

• 虚函数表:存在常量区(也叫只读数据段),是只读的

基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表 ,不同类型的对

象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。

问题(1):构造函数可以是虚函数吗?为什么?

不可以,原因;

  1. 虚函数的调用需要虚函数表指针,而虚函数表指针是在构造函数执行过程中才初始化的。如果构造函数是虚函数,那么调用构造函数时需要虚函数表指针,但此时虚函数表指针还没有初始化,矛盾。
  2. 构造函数的作用是创建对象,对象的类型在编译时就已经确定了,不需要动态绑定

问题(2):inline 函数可以是虚函数吗?

可以,原因:

inline 函数是在编译时展开的,而虚函数的调用是在运行时动态绑定的。当虚函数被 inline 修饰时,只有在用对象直接调用时才会 inline 展开;如果用指针或引用调用(多态场景),inline 会失效,因为编译器无法在编译时确定调用哪个函数

结尾:

这一期对C++多态知识的学习就结束了,希望通过本文的学习,你能够彻底理解C++多态。如果本文对你有帮助,欢迎点赞、收藏、关注!如果有任何问题,也欢迎在评论区留言交流。我会持续更新更多的C++技术文章,敬请期待!我主页里有更好康的呦!

往期回顾

  1. 一文彻底搞懂Linux权限知识
  2. C++模板
  3. 教你白嫖6个月的阿里云服务器 + Linux 常用指令学习
相关推荐
cheems95272 小时前
[Spring MVC] 统一功能与拦截器实践总结
java·spring·mvc
不吃土豆的马铃薯2 小时前
4.SGI STL 二级空间配置器 allocate 与_S_refill 源码解析
c语言·开发语言·c++·dreamweaver·内存池
Full Stack Developme2 小时前
Spring Boot 事务管理完整教程
java·数据库·spring boot
码界筑梦坊2 小时前
120-基于Python的食品营养特征数据可视化分析系统
开发语言·python·信息可视化·数据分析·毕业设计·echarts·fastapi
lsx2024063 小时前
《Foundation 模态框》
开发语言
城管不管3 小时前
前后端远程协作
java
青云计划3 小时前
Feed流
java·后端·spring
小许同学记录成长3 小时前
三维重建技术文档
算法·无人机