| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> 【C++】多态 -> 正文阅读 |
|
[C++知识库]【C++】多态 |
目录 一、多态的概念
二、多态的定义及实现2.1 多态的构成条件多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。Person 对象全价买票,Student 对象买票半价。 那么在继承中要构成多态还有两个条件:
?2.2 虚函数
2.3 虚函数的重写
?2.4 多态的使用关于多态的两个条件:
?刚刚我们完成了重写,接下来就是让父类指针去调用虚函数。 这里我们写一个 func 函数。
但是如果出现以下情况,多态的效果就无法实现。
?
?为解决这个问题,有两个解决方法。 1. 删除子类的中函数的 int,构成重写(即恢复原来的形式) 2. 将父类中虚函数也添加 int 参数,构成重写,传参要传入 int,如下图: ?
特例一:子类不加 virtual? 特例二:重写的返回值协变 ?一个返回 Person* ,一个返回 Student* ,也可以构成多态。 特例三:析构函数的重写,虽然函数名不同 2.5 析构函数重写如果基类的析构函数为虚函数,与此时派生类析构函数只要定义,无论是否加 virtual 关键字,都基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。 虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理称 destructor ,这就符合了函数名称相同构成函数重写的条件。 如果,我们将不将父类中的析构函数定义为虚函数。就会出现很大的问题: ?我们来分析一下原因: 此时不符合多态,编译期间就确定了函数的地址。所以父类指针没有去虚表中寻找虚析构函数的地址(也就是子类的析构函数),而是直接调用了自身的析构函数(父类的析构函数)。 如果符合了多态,那么在调用时,该指针指向父类部分,就会去调用虚表中重写的子类虚函数,然后调用子类的虚析构函数,调用完成了编译器自动调用父类的析构函数(这一步操作是自动完成)。 所以,如果没有实现多态,子类就无法调用自身的析构函数。 ?为了验证以上分析,这里演示一下编译器执行的步骤。 即。建议析构函数定义为虚函数。 2.6 C++11 override 和 final从上面可以看出,C++对函数重写的要求比较严格,但有些情况下由于疏忽,可能会导致函数名字字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug 会得不偿失,因此:C++11提供了 override 和 final 两个关键字,可以帮助用户检测是否重写。 1. final:修饰虚函数,表示该虚函数不能被重写。 2. final:修饰类,表示一个类不能被继承。 3.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。 三、抽象类3.1 抽象类的概念
抽象类强制了我们重写虚函数。是接口的体现。 3.2 接口继承和实现继承
四、多态的原理4.1 虚函数表这里有一道常见的笔试题,大家想到答案是 8 了吗 ?原因是,Base 类中不仅仅存放了一个 _b 成员变量,还有一个指向 Func1 的指针。 ? 通过观察我们发现除了 _b 成员,还多一个 __vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个与平台有关),对象中的这个指针我们呢加偶走虚函数表指针(virtual function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。 为了深层次的研究,针对上面的代码做出以下改造: 1.增加一个派生类Derive 去继承 Base 2. Derive 中重写 Func1 3.Base 再增加一个虚函数 Func2 和一个普通函数 Func3
观察上图,得出结论:
总结:派生类的虚表生成
4.2 多态的原理研究完了虚函数表,接下来就来剖析多态的原理。 代码如下: 我们打开调试,观察父类、子类中虚表指针以及虚函数表中的存储的地址。发现,重写了的函数(BuyTicket)在虚函数表中地址不同,而没重写的函数(Func)的地址相同。 ?其调用的逻辑图: 下图是其在监视窗口中函数调用的走向,跟随着箭头观察即可。
我们再来观察,存在多态和不存在多态时,底层的汇编代码: 其实编译器也是会检查当前符不符合多态,如果符合多态,就去调用子类中重写了的函数,如果没有重写,就直接去调用父类的函数。 构成多态的调用,运行时到指向对象虚表中找调用虚函数地址,所以 p 指向谁就调用谁的虚函数。 不构成多态则是普通调用,编译时确定函数地址,直接进行调用。 4.3 动态绑定和静态绑定
五、单继承和多继承关系中的虚函数表需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型。 5.1 单继承中的虚函数表?同一个类的对象会共用一个虚表 那子类的虚表是不是同一个呢?如下图:? 所以,不管是否完成重写,子类虚表和父类虚表都不是同一个。 那接下来还有一个问题,现在有一个只存在子类中的虚函数,那这个函数的地址会被存放到虚函数表中吗?那放在那个表中呢? 通过我们观察在内存中的观察,发现,这 0x010115aa 可能就是 func()的地址,只不过VS的调试窗口显示 vfptr?中没有func(),可能这就是 VS的一个BUG了。 接下来为了验证其到底是不是 BUG,我们可以通过打印地址来看看,虚函数表中到底有没有 func 函数的地址。代码如下,然后我们来解释代码: 我们写的这个函数能通过虚表指针正常调用,也就是说,子类增加的虚函数是会被添加到自身的虚函数表中的。然而 VS 调试中,会显示子类重写的虚函数和父类中没重写的虚函数,不显示是子类增加的虚函数,这算是 VS 调试的一个小 BUG 。 ?现在我们来剖析以上的关键代码。 ?函数指针只能这样 typedef ,这是语法要求,我们这样定义是为了后面调用函数时增强代码的可读性。 重点如下!!!(深度考验C语言指针) 因为对象前4个字节存放的是虚表指针(_vfptr),所以我们要取到 p1 的前4个字节。我们取出 p1 的整体地址,然后访问到前四个字节,即,将其转化为(int*),通过解引用取出虚表指针的地址(整形的形式),再将该整形转化为一个函数指针数组。 这样我们就可以直接通过一个类似遍历数组元素的函数来访问到虚函数表中存放的函数。 我们传入的实参是一个函数指针数组(虚表指针_vfptr),所以我们要使用函数指针数组接受,通过下标即可访问函数表中的函数。 ?这里注意,在虚表结束处放入 nullptr 只是 VS 编译器的做法,在 g++、clang等编译器下可能不适用,这时就要我们显示传入函数的个数了。 5.2 多继承中的虚函数表上面我们研究的情况都是单继承,那多继承下,虚表指针的情况是什么样的呢? ?关于为什么大小是20,其实非常简单 ?好的,这里我们再研究下一个问题: 上面说了 VS 调试不显示子类新增加的虚函数,那如上我们子类有一个新增的虚函数,此函数会被放在Base1的虚函数表中呢?还是Base2的虚函数表中呢? 接下来我们通过函数指针强行调用虚函数来看看是到底是存放在哪的。 ?发现,Base1中的虚函数表中存放着子类新增加的虚函数?func 。 ?再让我们看看 Base2 中的虚函数表中存放着的函数吧。 首先第一个问题就是我们如果取到 Base2中的虚函数表呢? ?这样必定的不行的。我们先来观察 对象d 其结构。 ? ?所以说,我们要让 ptr 指向 Base2 才行,即跳过 Base1 的空间。 这样行吗?我们看看结果。 ?程序调用一个函数就结束了,说明我们的代码必定是有问题的。 问题在于:此时 &d 是一个 int* 型指针,加上 sizeof(Base1)后,直接就跳了 4 * sizeof(Base1) 的空间。所以,在加上sizeof之前,我们要先将其转化为 char* 型指针,才能让其正确的指向 Base2 的起始位置。 上面这个方法就解决了这个问题,但是我们还有另外一个方式——切片。 ?现在我们来观察其中的现象,发现 Base2 的指针中并没有存放子类增加的 func3()。 所以,得出结论,子类新增加的虚函数会被存放到第一个父类的虚函数表中。 此时我们便可以画出其对应的关系图: 发现,Base1 和 Base2 中 存放 func1 的地址不同!但是明明是同一个函数,调用的地址不同,为什么最后结果调用的是相同的? 这就是编译器的一个精妙设计,如果想研究清楚需要去研究 VS 是如果调用函数的(汇编层次)。 首先我们重代码的层次来看一下,虚函数表中存放的地址和实际函数的地址是怎样的。 我们来打印一下子类中重写的虚函数地址。普通函数的地址我们直接输出函数名即可,但是类中的函数我们要指定类域。 并且,关于成员函数我们打印其地址要加上 & 地址操作符才能正常的输出(语法规定)。 这样就可以打印成功了。 然后我们将Base1、Base2 以及子类重写的虚函数地址都打印一下: 发现,其实不止两个虚函数表中同一个函数的地址不同,其实两个虚函数表中函数的地址与直接打印函数的地址也都不相同。 ?接下来我们通过调用来检查这三个地址最后调用的是否是同一个函数。希望大家耐心观察,其实并不复杂 即使多继承中两个父类虚函数表中存放重写的函数地址不同,但是仍可以调用到同一个函数。 都是重写的函数,但是两份虚函数表中存放的地址不同?。因为其存放的是jmp的地址。 5.3 菱形继承、菱形虚拟继承
现在我们粗略的看一下虚拟继承。因为其层次太深,过于复杂,实际中也很少使用。 如果我们在 A 中添加一个虚函数,并在 B、C 类中进行重写,Dmp中不重写。就会出现以下情况。 问题肯定在于我们没有将?Dmp 中的 func1 函数进行重写,现在我们对其进行重写,看还是否报错。 那此时我们使用 VS 开发者工具来看看类 Dmp 的结构。 ?观察 d 对象中的虚函数和虚基指针。 如果 B 、C 类对?func1 进行了重写,而 Dmp 中不进行重写,那 A 的虚函数表中就不知道应该存放B 的 func1 还是 C 的 func1,所以我们需要菱形继承的类主动重写 func1 函数,使继承下的?A 类中存放 Dmp 的 func1 ,这样才能通过编译。 六、多态知识点总结6.1 语法
6.2 原理
七、继承和多态常见的面试问题1.什么是多态?
2.什么是重载、重写(覆盖)、重定义(隐藏)?
3.多态的实现原理?
4.inline 函数可以是虚函数吗?
5.静态成员可以是虚函数吗?
? 6.构造函数可以是虚函数吗?
7.析构函数可以是虚函数吗?
8.拷贝构造函数可以是虚函数吗?
9.赋值运算符重载 operator= 可以是虚函数吗?
?更改如下: 10.对象访问普通函数快还是虚函数快?
11.虚函数表是什么阶段生成的,存在哪里?
?我们将得到的数据根据其类型进行划分: 12.C++菱形继承的问题?虚继承的原理?
13.什么是抽象类?抽象类的作用?
选择题一: ?答案: B 解析: 1. P 调用 A类中的 test(),此时 test() 调用 func() ,即 this -> func(),此时条件构成多态。因为传入的是子类的指针,即 p 是指向 B 的,所以去调用子类中的 func() 函数。 2. 因为普通函数继承是实现继承,而虚函数重写是接口继承,重写实现。所以 this -> func() 是仅仅是调用接口,val 仍然是 1。 为了验证第二点,即使子类中 val 不给缺省值,打印出的仍然为 1 。 变式: ? 题二: 求打印出的内容 答案: ?解析:
题目三: 求打印出的内容 答案: GIF 动画? 将 this 标出,便以理解。 这里的难点是当 new B时,B 构造函数中的调用 test() 语句时,test()函数回转调用子类的 func ()函数了。 原因: 因为B构造函数中语句为 this->test(), 调用时将 子类的 test 进行赋值给父类的 this ,并且父类指针 this 指向子类对象,形成多态的条件。 父类的 this 指针调用 func()?函数会去虚函数表中寻找 func 函数,发现子类进行了重写,于是就去调用了子类重写的 func() 函数。? ? |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 | -2025/1/11 14:02:04- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |