多态(难的起飞)

news2024/11/17 19:35:08

注意   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.coloradmin.cn/o/1704671.html

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

相关文章

c++ 将指针转换为 void* 后,转换为怎么判断原指针类型?

当将指针转换为void后&#xff0c;擦除了指针所指向对象的类型信息&#xff0c;因此无法通过void指针来判断原始指针的类型。我这里有一套编程入门教程&#xff0c;不仅包含了详细的视频讲解&#xff0c;项目实战。如果你渴望学习编程&#xff0c;不妨点个关注&#xff0c;给个…

每日一题 <leetcode--2326.螺旋矩阵>

https://leetcode.cn/problems/spiral-matrix-iv/ 函数中给出的int* returnSize和int** returnColumnSizes是需要我们返回数值的&#xff0c;这点需要注意。其中int** returnColumnSizes 是需要额外开辟一块空间。 这道题我们首先需要malloc出一快空间来把链表存放在数组中&…

指纹识别经典图书、开源算法库、开源数据库

目录 1. 指纹识别书籍 1.1《精通Visual C指纹模式识别系统算法及实现》 1.2《Handbook of Fingerprint Recognition》 2. 指纹识别开源算法库 2.1 Hands on Fingerprint Recognition with OpenCV and Python 2.2 NIST Biometric Image Software (NBIS) 3. 指纹识别开源数…

QQ名片满级会员装x助手HTML源码

源码介绍 QQ名片满级会员展示生成HTML源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;保存素材去选择QQ个性名片-选择大图模板-把图上传照片墙即可 源码效果 源码下载 蓝奏云&#xff1a;http…

Golang | Leetcode Golang题解之第116题填充每个节点的下一个右侧节点指针

题目&#xff1a; 题解&#xff1a; func connect(root *Node) *Node {if root nil {return root}// 每次循环从该层的最左侧节点开始for leftmost : root; leftmost.Left ! nil; leftmost leftmost.Left {// 通过 Next 遍历这一层节点&#xff0c;为下一层的节点更新 Next …

VS Code开发Python配置和使用教程

在Visual Studio Code (VSCode) 中配置和使用Python进行开发是一个相对直接的过程&#xff0c;下面是一份简明的指南&#xff0c;帮助你从零开始设置环境&#xff1a; 1. 安装Visual Studio Code 首先&#xff0c;确保你已经安装了Visual Studio Code。如果还没有安装&#x…

入门编程,一定要从C语言开始吗?

入门编程并不一定非得从C语言开始。我这里有一套编程入门教程&#xff0c;不仅包含了详细的视频讲解&#xff0c;项目实战。如果你渴望学习编程&#xff0c;不妨点个关注&#xff0c;给个评论222&#xff0c;私信22&#xff0c;我在后台发给你。 C语言在过去是一种常见的入门语…

《python编程从入门到实践》day40

# 昨日知识点回顾 编辑条目及创建用户账户 暂没能解决bug&#xff1a; The view learning_logs.views.edit_entry didnt return an HttpResponse object. It returned None instead.# 今日知识点学习 19.2.5 注销 提供让用户注销的途径 1.在base.html中添加注销链接 …

基于OrangePi AIpro的后端服务器构建

一. OrangePi AIpro简介 1.1 OrangePi AIpro外观 1.2 OrangePi AIpro配置 OrangePi AIpro(8T)采用昇腾AI技术路线&#xff0c;具体为4核64位处理器AI处理器&#xff0c;集成图形处理器&#xff0c;支持8TOPS AI算力&#xff0c;拥有8GB/16GB LPDDR4X&#xff0c;可以外接32GB…

边境牧羊犬优化算法,原理详解,MATLAB代码免费获取

边境牧羊犬优化算法&#xff08;Border Collie Optimization&#xff0c;BCO&#xff09;是一种受自然启发的群智能优化算法。该算法是通过模仿边境牧羊犬的放牧风格来开发的。本文成功地采用了边境牧羊犬从正面和侧面的独特放牧风格。在这个算法中&#xff0c;整个种群被分成两…

实现 Vue 标签页切换效果的组件开发

在本次开发中&#xff0c;我们将实现一个 Vue 组件&#xff0c;用于展示和切换标签页。 背景有移动动画效果 该组件将具有以下功能&#xff1a; 标签页左右滚动点击标签页切换内容关闭指定标签页支持多种标签页风格 以下是实现该组件的具体步骤&#xff1a; 创建 Vue 组件…

esp8266的rtos和nonos区别

https://bbs.espressif.com/viewtopic.php?t75242#p100294 https://blog.csdn.net/ydogg/article/details/72598752

Function Calling学习

Function Calling第一篇 Agent&#xff1a;AI 主动提要求Function Calling&#xff1a;AI 要求执行某个函数场景举例&#xff1a;明天上班是否要带伞&#xff1f;AI反过来问你&#xff0c;明天天气怎么样&#xff1f; Function Calling 的基本流程 Function Calling 完整的官…

【传知代码】私人订制词云图-论文复现

文章目录 概述原理介绍核心逻辑1、选取需要解析的txt文档2、选取背景图明确形状3、配置停用词4、创建分词词典&#xff0c;主要解决新的网络热词、专有名词等不识别问题 技巧1、中文乱码问题&#xff0c;使用的时候指定使用的文字字体2、更换背景图3、词库下载以及格式转换方式…

数组单调栈-901. 股票价格跨度、leetcode

单调栈作为一种数据结构在求解类递增、递减方面的题目中有较为广泛的应用&#xff0c;在以往的leetcode中所见到的相关单调栈的题目均为单一元素&#xff0c;今天刷到901题目时&#xff0c;想到了将数组元素作为单调栈中元素的方法进行求解。 题目链接及描述 901. 股票价格跨…

C++ | Leetcode C++题解之第108题将有序数组转换为二叉搜索树

题目&#xff1a; 题解&#xff1a; class Solution { public:TreeNode* sortedArrayToBST(vector<int>& nums) {return helper(nums, 0, nums.size() - 1);}TreeNode* helper(vector<int>& nums, int left, int right) {if (left > right) {return nu…

Python 全栈体系【四阶】(五十四)

第五章 深度学习 十二、光学字符识别&#xff08;OCR&#xff09; 3. 文字识别技术 3.1 CRNNCTC(2015) CRNN&#xff08;Convolutional Recurrent Neural Network&#xff09;即卷积递归神经网络&#xff0c;是DCNN和RNN的组合&#xff0c;专门用于识别图像中的序列式对象。…

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

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

VMware ESXi 兼容性查询

官网兼容性查询地址&#xff1a;https://www.vmware.com/resources/compatibility/search.php

Android Studio自带Profiler工具进行CPU资源及线程问题分析步骤

1、运行需要检测CPU资源问题与线程问题的程序 这里以“com.example.opengltest”程序为例。 2、点击Profiler按钮 3、点击SESIONS ""号按钮选择设备&#xff0c;选择对应设备下的应用或进程 4、双击CPU区块 5、选择Trace config选项&#xff0c;选择“Java/Kotli…