多态(难的起飞)

注意   virtual关键字:

    1、可以修饰原函数,为了完成虚函数的重写,满足多态的条件之一

   2、可以菱形继承中,去完成虚继承,解决数据冗余和二义性

两个地方使用了同一个关键字,但是它们互相一点关系都没有

虚函数重写:

 

多态的条件:

1、虚函数的重写

2、父类对象的指针或者引用去调用虚函数

必须是父类指针或者引用

不可以是子类因为父类不可以传给子类

class Person
{
public:virtual void BuyTicket() { cout << "Person全票" << endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "Student半票" << endl; }
};
void func(Person& p1)
{p1.BuyTicket();
}
int main()
{Person p1;Student s1;func(p1);func(s1);return 0;
}

协变(是多态的一种特殊情况):

多态:

1、虚函数的重写(必须要函数名、返回值、参数要相同)

2、父类对象的指针或者引用去调用虚函数

但是协变可以返回值可以不同

但是返回值必须是基类的指针或引用和子类的指针或引用

//class A
//{
//};
//class B :public A
//{
//}
//其他类的基类和派生类也可以
//class Person
//{
//public:
//	virtual A* BuyTicket() { cout << "Person全票" << endl; return nullptr; }
//};
//class Student : public Person
//{
//public:
//	virtual B* BuyTicket() { cout << "Student半票" << endl;  return nullptr; }
//};
//void func(Person& p1)
//{
//	p1.BuyTicket();
//}
//class Person
{
public:virtual Person* BuyTicket() { cout << "Person全票" << endl; return nullptr; }
};
class Student : public Person
{
public:virtual Student* BuyTicket() { cout << "Student半票" << endl;  return nullptr;}
};
void func(Person& p1)
{p1.BuyTicket();
}
int main()
{Person p1;Student s1;func(p1);func(s1);return 0;
}

析构函数:

面试题:析构函数需不需要加vitrual?

class Person
{
public:~Person() { cout << "~Person()" << endl; }};
class Student : public Person
{
public:~Student() { cout << "~Student()" << endl;}
};int main()
{Person* p1= new Student;delete p1;return 0;
}

这种情况下父类的指针指向了new Student 但是使用完会造成内存泄漏,父类的指针只会调用父类的析构函数去清理该指向部分的空间,但是我们需要清理子类的空间就要调用子类的析构函数,所以需要加virtual 构成虚函数的重写,让父类的指针调用构成多态,就可以调用子类的析构函数。

 

看下一道面试题:

在做面试题之前先看下面代码

在继承关系中,

如何理解上述话呢?

看下面代码

在满足多态的条件下,虚函数的继承是继承了接口,所以缺省值继承了,但是子类要自己重写实现

所以当父类中的有虚函数,子类的就可以不用加virtual,但是不规范

答案:是B

为什么多态就要继承父类的接口?突然感悟

比喻:子类中的函数 drive(Banz* const this),父类也有(Car* const this)
  //子类这个this是接收不了父类的指针,只有父类的指针或引用才可以指向子类
   //所以这个继承接口才需要继承父类的接口----突然感悟

============下面代码=============== 

//作者:蚂蚁捉虫虫
//链接:https ://www.zhihu.com/question/517444641/answer/2390138862
//来源:知乎
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#include <iostream>       // std::cout
class Base {public:Base() {};virtual void func_a(int a = 0) {}; //这个是虚函数,子类只继承接口,具体的实现,由子类去实现void func_b(int b) { std::cout << b + 10 << "\n"; }; //这个是实函数,其接口和实现,都会被子类继承
};class Base_A : public Base {
public:void func_a(int a=15) { std::cout << a << "\n"; };
};class Base_B : public Base {
public:void func_a(int a) { std::cout << a + 15 << "\n"; };
};int main()
{Base_A a;Base_B b;a.func_a(); //仅仅继承了基类的接口,但没有继承实现a.func_b(10); //继承了基类的接口及实现std::cout << std::endl;b.func_a(10); //仅仅继承了基类的接口,但没有继承实现b.func_b(10); //继承了基类的接口及实现return 0;
}

 

只有在满足多态的情况下,虚函数的继承才是父类的虚函数继承对于子类来说继承的是父类的接口(包括缺省值),子类函数的实现需要子类来写

上述代码只是完成了重写,并没有满足多态,所以并没有继承接口

关键字final和override

1、final修饰虚函数,表示该虚函数不能再被继承

也可以修饰class叫最终类不能被继承

override关键字:检查子类的虚函数是否完成重写

构成虚函数重写吗?

没有,认真看,但是不会报错,所以,加上override就可以自动检测检查子类的虚函数是否完成重写

重载、重写、重定义

抽象类

可以看下列代码:

//作者:蚂蚁捉虫虫
//链接:https://www.zhihu.com/question/517444641/answer/2390138862
//来源:知乎
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。class Base {public:Base(){};virtual void func_a(int a) = 0; //这个是纯虚函数,子类只继承接口,具体的实现,由子类去实现void func_b(int b) {std::cout << b+10 << "\n";}; //这个是实函数,其接口和实现,都会被子类继承
};class Base_A: public Base{
public:void func_a(int a){std::cout << a << "\n";};
};class Base_B: public Base{
public:void func_a(int a){std::cout << a + 15 << "\n";};
};int main ()
{Base_A a;Base_B b;a.func_a(10); //仅仅继承了基类的接口,但没有继承实现a.func_b(10); //继承了基类的接口及实现std::cout << std::endl;b.func_a(10); //仅仅继承了基类的接口,但没有继承实现b.func_b(10); //继承了基类的接口及实现return 0;
}

上述代码里,定一个基类,里面有两个成员函数,一个是虚函数,一个是实际函数;然后又定义了两个子类,Base_A和Base_B,两个子类对基类中的func_b函数有不一样的实现

纯虚函数的作用强制子类完成重写

表示抽象的类型。抽象就是在现实中没有对应的实体的

接口继承和实现继承

多态的原理:

测试我们发现b对象是8个字节,除了_b成员,还多了一个指针_vfptr放在对象对面,我们叫做虚函数指针我们叫做虚函数表指针。一个含有虚函数表的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表称虚表

注意:虚函数存放在哪里? 虚表存在哪里

虚表存的是虚函数指针,不是虚函数,虚函数也是函数所以也是存在代码区,只是它的地址被存进虚函数指针中,这个指针被虚表记录着

重写:接口继承,实现重写,在原理上是覆盖将父类继承下来的vfptr的父类虚函数的地址覆盖成子类的虚函数地址

从反汇编看原理:

普通类函数:

在编译的过程中就已经确定了调用函数的地址

现在我们加上virtual虚函数

进入汇编,当形成多态时是如何调用的 

00B021E1 8B 45 08             mov         eax,dword ptr [A]  //将A指向空间地址给eax
00B021E4 8B 10                mov         edx,dword ptr [eax]  //将eax空间中的前四个字节地址给edx就是虚函数表指针
00B021E6 8B F4                mov         esi,esp//这个是维护函数栈帧的寄存器,不用管  
00B021E8 8B 4D 08             mov         ecx,dword ptr [A]  //将A指向空间地址给ecx
00B021EB 8B 42 04             mov         eax,dword ptr [edx+4]  //因为edx保存的是前四个字节空间的地址就是虚函数表指针+4就是run()的地址,将run()地址给eax,前4个是speak()的地址
00B021EE FF D0                call        eax //调用run()
00B021F0 3B F4                cmp         esi,esp 
00B021F2 E8 1A F1 FF FF       call        __RTC_CheckEsp (0B01311h) 

 多态就是有virtual函数是用虚函数表指针去存放虚函数的地址,在由虚函数表指针调用对应的函数

面试题:

虚函数存在哪里?代码段,虚函数和普通函数一样都是函数所以都是编译成指令存进代码段中

虚函数表存在哪里?

存在代码段中,不是存在栈区,因为栈区是由一个个栈帧堆建的所以每调用创建一个对象就要建立一个虚表是很消耗内存的

证明一下:

虚表存放在代码区中的代码段最合适,堆区是动态开辟的数据区分为bss区(存放未初始化的static和未初始化的全局变量)数据区存放(存放初始化的static和初始化的全局变量),所以代码段是最合适的

反向验证:

发现很接近代码区

总结:

多态的本质原理,符合多态的两个条件。那么在父类的指针或引用调用时,会到指向对象的虚表找到对应的虚函数地址,进行调用

多态(程序运行时去指向对象的虚表中找到函数地址,进行调用,所以p指向谁就调用谁的虚函数)

普通函数的调用,编译链接时确定函数的地址,运行时直接调用。类型时谁就是谁调用

动态绑定和静态绑定:

编译:就是代码和语法检查其实就是预处理、编译、汇编、链接

运行:就是将可执行文件加载到内存中进行对数据区的数据替换

静态绑定:更具调的类型就确定了调用的函数

动态绑定:运行时具体拿到类型确定程序的具体行为,就是在编译时无法确定函数的行为

运行时根据寄存器去拿到函数的地址

单继承和多继承的虚表(不是虚基表)

单继承:

void(*p)();  //函数指针

补充:

函数名就是函数的地址

那我们手动打印虚函数表

class base
{
public:virtual void func1() { cout << "base::func1()" << endl; }virtual void func2() { cout << "base::func2()" << endl; }};
class derive :public base
{
public:virtual void func1() { cout << "derive::func1()" << endl; }virtual void func3() { cout << "derive::func3()" << endl; }virtual void func4() { cout << "derive::func4()" << endl; }};
//void(*)()
typedef void(*VF_PTR)();//重命名函数指针void PrintVFTable(VF_PTR* pTable)//VF_PTR pTable[]  函数指针数组==虚函数表指针
{for (size_t i = 0; pTable[i] != 0; i++){printf("pTable[%d]=%p->", i, pTable[i]);VF_PTR f = pTable[i];//得到函数的地址==函数名f();}cout << endl;
}int main()
{base b1;derive d2;PrintVFTable((VF_PTR*)(*(int*)&b1));//取b1的地址因为要取到虚函数表指针,它在对象的前四个字节//所以转换成int*在解引用就是取空间b1的前四个字节,因为此时是int*//所以要转成VF_PTR*PrintVFTable((VF_PTR*)(*(int*)&d2));return 0;
}

多继承的虚表:

计算一下test 对象等于多少?

class base
{
public:virtual void func1() { cout << "base::func1()" << endl; }virtual void func2() { cout << "base::func2()" << endl; }int i = 0;
};
class derive
{
public:virtual void func1() { cout << "derive::func1()" << endl; }virtual void func3() { cout << "derive::func3()" << endl; }virtual void func4() { cout << "derive::func4()" << endl; }int i = 0;
};
class test:public base,public derive
{
public:virtual void func3() { cout << "test::func1()" << endl; }virtual void func2() { cout << "test::func3()" << endl; }virtual void func7() { cout << "test::func4()" << endl; }
public:int i = 0;
};//void(*)()
typedef void(*VF_PTR)();//重命名函数指针void PrintVFTable(VF_PTR* pTable)//VF_PTR pTable[]  函数指针数组==虚函数表指针
{for (size_t i = 0; pTable[i] != 0; i++){printf("pTable[%d]=%p->", i, pTable[i]);VF_PTR f = pTable[i];//得到函数的地址==函数名f();}cout << endl;
}int main()
{test i;cout << sizeof(i) << endl;return 0;
}

等于20   

编译器又没显示!!!那我们手动去看看

继承的子类和其父类的表不是同一张表,只有同一类才是用一张表哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/17346.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JAVASE总结一

1、 2、引用也可以是成员变量&#xff08;实例变量&#xff09;&#xff0c;也可以是局部变量&#xff1b;引用数据类型&#xff0c;引用&#xff0c; 我们是通过引用去访问JVM堆内存当中的java对象&#xff0c;引用保存了java对象的内存地址&#xff0c;指向了JVM堆内存当中…

ESP32 - Micropython ESP-IDF 双线教程 脉宽调制(PWM)(1)

ESP32 - Micropython ESP-IDF 双线教程 脉宽调制&#xff08;PWM&#xff09; PWM 的基本原理PWM 的应用PWM 的优点PWM 的实现方式ESP32-micropython 中的 PWM 功能使用 micropython 控制 PWM 的代码示例代码介绍 ESP32-IDF 中的 PWM 功能1. 初始化配置函数2. 引脚绑定函数3. 占…

常见算法200个(5):快速排序(快排)

JS实现快速排序 1.快速排序思路&#xff1a; 选择数组中的一个值作为基准&#xff0c;将数组中小于该值的数置于该数之前&#xff0c;大于该值的数置于该数之后&#xff0c;接着对该数前后的两个数组进行重复操作直至排序完成。 2.代码实现&#xff1a; function quick(arr)…

使用 Snort 进行入侵检测

使用 Snort 进行入侵检测 Snort 是一种流行的开源入侵检测系统。您可以在http://www.snort.org/上获取它。Snort 分析流量并尝试检测和记录可疑活动。Snort 还能够根据其所做的分析发送警报。 Snort 安装 在本课中&#xff0c;我们将从源代码安装。此外&#xff0c;我们不会安…

2024 前端面试每日1小时

三日 1. 如何理解Vue的模板编译原理 Vue的模板编译实际就是将模板字符串通过解析、优化和代码生成等步骤转换为渲染函数的过程。这个过程中&#xff0c;AST扮演了非常重要的角色&#xff0c;它用树形结构描述了模板的内容和结构&#xff0c;是编译过程的核心数据结构&#xff…

MySQL——适合不适合创建索引的情况

那些情况适合创建索引 1、字段的数值具有唯一性的限制 索引本身可以起到约束的作用&#xff0c;比如唯一索引、主键索引都是可以起到唯一性约束的&#xff0c;因此在我们的数据表中&#xff0c;如果某个字段是唯一性的&#xff0c;就可以直接创建唯一性索引&#xff0c;或者主…

Nodejs 爬虫 案例

1.安装&#xff1a; npm install cheerio npm install axios2.介绍&#xff1a; 2.1 cheerio 特点和用途描述&#xff1a; HTML解析和操作&#xff1a;Cheerio 可以将 HTML 字符串加载到内存中&#xff0c;并将其转换为一个可操作的 DOM 树结构&#xff0c;从而可以方便地对…

AURIX TC3xx单片机介绍-启动过程介绍1

从各个域控制器硬件解决方案来看,MPU可能来自多个供应商,有瑞萨,有NXP等,但对于MCU来说,基本都采用英飞凌TC3xx。 今天我们就来看一下TC3xx的启动过程,主要包含如下内容: uC上电过程中,会经过一个上电时序,从复位状态“脱离”出来;Boot Firmware是复位后第一个执行的…

使用 Effect 同步-09

有些组件需要与外部系统同步。例如&#xff0c;你可能希望根据 React state 控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。Effects 会在渲染后运行一些代码&#xff0c;以便可以将组件与 React 之外的某些系统同步。 简单理解&#xff0c;就是需要操…

Python实现对Word文档内容出现“重复标题”进行自动去重(4)

前言 本文是该专栏的第4篇,后面会持续分享Python办公自动化干货知识,记得关注。 在本专栏上一篇文章《Python实现对Word文档内容出现“重复标题”进行自动去重(3)》中,笔者有详细介绍使用python对word文档内容的目标文本进行自动去重。只不过本文要介绍的“去重方法”与上…

计算机专业必考之计算机指令设计格式

计算机指令设计格式 例题&#xff1a; 1.设相对寻址的转移指令占3个字节&#xff0c;第一字节为操作码&#xff0c;第二&#xff0c;第三字节为相对偏移量&#xff0c; 数据在存储器以低地址为字地址的存放方式。 每当CPU从存储器取出一个字节时候&#xff0c;自动完成&…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-24.1,2 SPI驱动实验-SPI协议介绍

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

计算机组成原理易混淆知识点总结(持续更新)

目录 1.机器字长&#xff0c;存储字长与指令字长 2.指令周期,机器周期,时钟周期 3.CPI,IPS,MIPS 4.翻译程序和汇编程序 5.计算机体系结构和计算机组成的区别和联系 6.基准程序执行得越快说明机器的性能越好吗? 1.机器字长&#xff0c;存储字长与指令字长 不同的机器三者…

AI智能体|扣子Coze文生图功能接入微信公众号

大家好&#xff0c;我是无界生长。 AI智能体&#xff5c;扣子Coze文生图功能接入微信公众号本文分享了如何将Coze平台的文生图功能接入微信公众号的详细操作流程&#xff0c;包括创建图像流、创建并配置Bot、设置提示词和开场白、调试、发布等步骤。如果看完还没学会的话&…

网页图片加载慢的求解指南

网页/图片加载慢的求解指南 一、前言与问题描述 今天刚换上华为的HUAWEI AX3 Pro New&#xff0c;连上WIFI后测速虽然比平时慢&#xff0c;但是也不算太离谱&#xff0c;如下图所示&#xff1a; 估计读者们有也和作者一样&#xff0c;还没意识到事情的严重性&#x1f601;。 …

08Django项目--用户管理系统--查(前后端)

对应视频链接点击直达 TOC 一些朋友加我Q反馈&#xff0c;希望有每个阶段的完整项目代码&#xff0c;那从今天开始&#xff0c;我会上传完整的项目代码。 用户管理&#xff0c;简而言之就是用户的增删改查。 08项目点击下载&#xff0c;可直接运行&#xff08;含数据库&…

PHP框架 Laravel

现在因为公司需求&#xff0c;需要新开一个Laravel框架的项目&#xff0c;毫无疑问&#xff0c;我又被借调过去了&#xff0c;最近老是被借调&#xff0c;有点阴郁&#xff0c;不过反观来看&#xff0c;这也是好事&#xff0c;又可以复习和巩固一下自己的知识点&#xff0c;接下…

大数据开发面试题【Spark篇】

115、Spark的任务执行流程 driver和executor&#xff0c;结构式一主多从模式&#xff0c; driver&#xff1a;spark的驱动节点&#xff0c;用于执行spark任务中的main方法&#xff0c;负责实际代码的执行工作&#xff1b;主要负责&#xff1a;将代码逻辑转换为任务、在executo…

编译qt5.15.2(mac/windows)的mysql驱动(附带编译好的文件)

文章目录 0 背景1 编译过程2 福利 0 背景 因为需要连接到mysql数据库&#xff0c;所以需要连mysql驱动。 1 编译过程 1&#xff0c;打开文件/Users/mac/Qt5.14.2/5.14.2/Src/qtbase/src/plugins/sqldrivers/sqldrivers.pro&#xff0c;注释掉QMAKE_USE mysql&#xff1b; 如…

国产【Jetson Xavier NX】——从裸机到深度学习开发环境配置

1、设置系统从固态硬盘启动 英伟达官方NX出厂是直接将SD卡&#xff08;64/128G&#xff09;烧录系统作为系统盘使用&#xff0c;国产NX出厂是将系统配置在8G内存中&#xff0c;在后续使用中需配置大量开发包&#xff0c;故将系统设置为从固态硬盘启动。 参考链接 https://blo…