多态(难的起飞)

2024-05-28 07:36
文章标签 多态 起飞

本文主要是介绍多态(难的起飞),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

注意   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.chinasem.cn/article/1009930

相关文章

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

JavaSE——封装、继承和多态

1. 封装 1.1 概念      面向对象程序三大特性:封装、继承、多态 。而类和对象阶段,主要研究的就是封装特性。何为封装呢?简单来说就是套壳屏蔽细节 。     比如:对于电脑这样一个复杂的设备,提供给用户的就只是:开关机、通过键盘输入,显示器, USB 插孔等,让用户来和计算机进行交互,完成日常事务。但实际上:电脑真正工作的却是CPU 、显卡、内存等一些硬件元件。

【JVM】JVM栈帧中的动态链接 与 Java的面向对象特性--多态

栈帧 每一次方法调用都会有一个对应的栈帧被压入栈(虚拟机栈)中,每一个方法调用结束后,都会有一个栈帧被弹出。 每个栈帧中包括:局部变量表、操作数栈、动态链接、方法返回地址。 JavaGuide:Java内存区域详解(重点) 动态链接 动态链接:指向运行时常量池中该栈帧所属方法的引用。 多态 多态允许不同类的对象对同一消息做出响应,但表现出不同的行为(即方法的多样性)。 多态

【IPV6从入门到起飞】4-RTMP推流,ffmpeg拉流,纯HTML网页HLS实时直播

【IPV6从入门到起飞】4-RTMP推流,ffmpeg拉流,纯HTML网页HLS实时直播 1 背景2 搭建rtmp服务器2.1 nginx方案搭建2.1.1 windows 配置2.1.2 linux 配置 2.2 Docker方案搭建2.2.1 docker 下载2.2.2 宝塔软件商店下载 3 rtmp推流3.1 EV录屏推流3.2 OBS Studio推流 4 ffmpeg拉流转格式

java基础总结14-面向对象10(多态)

面向对象最核心的机制——动态绑定,也叫多态 1 通过下面的例子理解动态绑定,即多态 package javastudy.summary;class Animal {/*** 声明一个私有的成员变量name。*/private String name;/*** 在Animal类自定义的构造方法* @param name*/Animal(String name) {this.name = n

OOP三个基本特征:封装、继承、多态

OOP三个基本特征:封装、继承、多态 C++编程之—面向对象的三个基本特征 默认分类 2008-06-28 21:17:04 阅读12 评论1字号:大中小     面向对象的三个基本特征是:封装、继承、多态。     封装 封装最好理解了。封装是面向对象的特征之一,是对象和类概念的主要特性。   封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信

泛型第三课,自定义泛型、无多态、通配符、无泛型数组

泛型没有多态 package com.pkushutong.genericity4;/*** 多态的两种形式* 注:泛型没有多态* @author dell**/public class Test01 {public static void main(String[] args) {Fruit f = new Fruit();test(new Apple());}//形参使用多态publi

Python中的方法重写与多态:解锁编程的无限可能

在编程的世界里,灵活性与扩展性往往是衡量一个语言是否强大、易于维护的关键指标。Python,作为一种被广泛使用的高级编程语言,不仅以其简洁易读的语法赢得了众多开发者的喜爱,更因其支持多种面向对象特性而备受青睐。其中,“方法重写”与“多态”便是两个核心概念,它们不仅能够极大地提高代码的复用性和可维护性,还能帮助我们构建更加灵活、健壮的软件系统。本文将通过一系列由浅入深的例子,带你一起探索这两个概念的

《C++中的面向对象编程三大特性:封装、继承与多态》

在 C++编程的广阔世界中,面向对象编程(Object-Oriented Programming,OOP)的三大特性——封装、继承和多态,犹如三把强大的利器,帮助程序员构建出高效、可维护和可扩展的软件系统。本文将深入探讨如何在 C++中实现这三大特性,并通过具体的代码示例展示它们的强大之处。 一、封装(Encapsulation) 封装是将数据和操作数据的方法封装在一个类中,以实现信息隐藏和数

多态的概念及实现

目录 前言 2. 多态的定义及实现 2.1多态的构成条件 2.2 虚函数 2.3虚函数的重写 重写、重载、重定义的辨析 虚函数重写的两个例外: 2.4 C++11 override 和 final 1. final:修饰虚函数,表示该虚函数不能再被重写 这样就会报错。 2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。 2.5