【C++】深度解剖多态

news/发布时间2024/5/19 8:42:06

> 作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:了解什么是多态,熟练掌握多态的定义,熟读抽象类。

> 毒鸡汤:一半明媚,一半阴霾,这就是人生

> 望小伙伴们点赞👍收藏✨加关注哟💕💕 

🌟前言  

前面我们学习了继承,在c++的面向对象中是重要的部分,而c++另一个重要的部分就是多态,这个板块很实用,在java中使用起来尤其频繁,Java这个语言也是借鉴了c++使用起来比较轻松,学习c++的多态也是必不可少的。

⭐主体

学习多态咱们按照下面的图解:

🌙多态的概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态 。

举个例子:比如买票这个行为 ,当普通人买票时,是全价买票; 学生买票时,是半价买票; 军人买票时是优先买票。再举个栗子: 最近为了争夺在线支付市场 ,支付宝年底经常会做诱人的 扫红包 - 支付 - 给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜 8 块、 10 块 ... ,而有人扫的红包都是 1 毛, 5毛 .... 。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99 ;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1 ;总结一下: 同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。 ps :支付宝红包问题纯属瞎编,大家仅供娱乐。总结:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。 Person 对象买票全价, Student 对象买票半价。

🌙多态的定义及实现

💫多态构成条件

在继承中要构成多态还有两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

举个栗子:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

运行结果:

问题分析:

注意:接受对象为父类的指针或者引用,你传递的是父类就调用父类的函数,传递的是子类就调用子类的函数,在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

💫虚函数的重写和协变

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

1.协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

2.析构函数的重写

举例说明:

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

问题分析:

想让p1调用Person的析构,p2先调用Person的析构在调用Student的析构,但是这里并没有调用Student的析构,只析构了父类,就可能发生内存泄漏。

问题拓展:

因为这里发生了隐藏,~Person()变为 this->destructor()    ~Student()变为this->destructor() 编译器将他们两个的函数名都统一处理成了destructor(析构函数),因此调用的时候只看自身的类型,是Person就调用Person的函数,是Student就调用Student的函数,根本不构成多态,这并不是我们期望的哪样。

问题解决:

析构函数添加上virtual

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

再次运行:

💫final 和 override

在添加父类虚函数后面添加final代表不能再被重写

override代表必须要重写虚函数,如果没有重写便会报错

💫重载、覆盖(重写)、隐藏(重定义)的对比

🌙抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口

类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如Car* pBMW = new BMW;

  1. 子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
  2. 父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似 )另外纯虚函数更体现出了接口继承。
  3. 纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数

问题分析:

class Car
{
public:// 纯虚函数virtual void Drive() = 0;
};int main()
{Car c;return 0;
}

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

🌙多态的原理

💫虚函数表

问题探究:

下面代码运行结果:

我们定义一个Base类,里面有虚函数,还有一个变量int,按照我们之前学习到了,这里Base类的大小应该是4个字节,图中确是8个字节,为什么会发生这种现象呢?

问题分析:

除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。其实应该叫__vftptr(多个t代表table)

总结归纳:

我们多添加几个虚函数,看看这个表里面的内容是怎么样的,可以发现虚函数会放到虚函数表中,普通函数不会,并且表里面的内容是一个数组,是函数指针数组

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

💫多态的原理

1.虚表指针里的内容

举例说明:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void fun() {}
private:int a;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:int b;
};
void Func(Person* p)
{p->BuyTicket();
}
int main()
{Person p;Student s;Func(&p);Func(&s);return 0;
}

问题分析:

从图中我们可以看到,在内存1里面输入&p可以找到p的地址, 因为p的第一个内容就是__vfptr,因此p的地址也是__vfptr的地址,那么我们通过__vfptr的地址就可以找到虚函数表里面的内容,因此我们在内存2里面输入__vfptr的地址,我们便找到了两个虚函数的地址。 

问题拓展:

注意这里Student类和Teacher的类表里的第二个虚函数地址是一样的,因为B类没有重写第二个虚函数,因此继承下来了。为什么第一个虚函数不一样呢?因为子类重写后覆盖掉了(这也是为什么重写被称作覆盖的由来)

2.引用和指针如何实现多态 

可以分析,为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数。父类再去接受这个数据了,一样会有vftptr(是子类的vftptr),再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数这样就完成了多态。

标题
传递子类,首先会进行切割

 💫虚函数表存放位置

举例说明:

我们通过代码来打印各个区地地址,可以判断虚函数表存放位置

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};void func()
{}int main()
{Base b1;Base b2;static int a = 0;int b = 0;int* p1 = new int;const char* p2 = "hello world";printf("静态区:%p\n", &a);printf("栈:%p\n", &b);printf("堆:%p\n", p1);printf("代码段:%p\n", p2);printf("虚表:%p\n", *((int*)&b1));printf("虚函数地址:%p\n", &Base::func1);printf("普通函数:%p\n", func);
}

问题分析:

注意打印虚表这里,vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节b1是类对象,取地址取出类对象的地址,强转为(int*)代表我们只取4个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址

从图中可以发现代码段和虚表地址非常接近,存在代码段的常量区。虚函数和普通函数地址非常接近,存在代码段。

🌙单继承和多继承关系的虚函数表

💫单继承中的虚函数表

举例说明:

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
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; }
private:int b;
};
class X : Derive
{
public:virtual void fun3() { cout << "X::func3" << endl; }
};int main()
{Base b;Derive d;X x;return 0;
}

代码分析:

通过输入__vfptr的地址,我们成功找到了里面虚函数的地址。并且我们还发现似乎下面那两个地址跟上面两个非常接近,我们可以合理的设想,下面两个地址也是虚函数指针。

问题分析:

下面我们typedef了虚函数表指针  typedef void(*VFTPTR)(); 可以通过这个函数指针数组来打印里面的虚函数,这个打印函数终止条件就是 !=0 ,传递的参数内容跟前面我们分析的差不多,只是躲了一个强转,PrintVFPtr((VFTPTR*)*(int*)&b)  ; 因为后面的  *(int*)&b 虽然内容是地址,但是表现形式是一个整形,需要强为  (VFTPTR*) 。

问题拓展:

在*((int*)&d) 就会取到vTableAddress指向的地址,就得到虚函数的地址了。

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
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; }
private:int b;
};
class X : Derive
{
public:virtual void func3() { cout << "X::func3" << endl; }
};typedef void(*VFTPTR)();void PrintVFPtr(VFTPTR a[])
{for (size_t i = 0; a[i] != 0; i++){printf("a[%d]:%p->", i, a[i]);VFTPTR p = a[i];p();}cout << endl;
}int main()
{Base b;Derive d;X x;PrintVFPtr((VFTPTR*)*(int*)&b);PrintVFPtr((VFTPTR*)*(int*)&d);PrintVFPtr((VFTPTR*)*(int*)&x);return 0;
}

运行:

分析:可以确认虚函数都会放到虚表里面

💫多继承中的虚函数表

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{printf("%p\n", &Derive::func1);Derive d;//PrintVTable((VFPTR*)(*(int*)&d));PrintVTable((VFPTR*)(*(int*)&d));    PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
}


PrintVTable((VFPTR*)(*(int*)&d)); 

因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )


结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值

💫菱形继承和菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的 模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看 了,一般我们也不需要研究清楚,因为实际中很少用。

🌙继承和多态常见的面试问题

1.什么是多态?本篇已讲解了

2. 什么是重载、重写(覆盖)、重定义(隐藏)?这里在本篇已讲解了

3. 多态的实现原理?这里在本篇已讲解了

4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。

5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。

 8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。

10. C++菱形继承的问题?虚继承的原理?这里在本篇已讲解了

11. 什么是抽象类?抽象类的作用?抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。

🌟结束语

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

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

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

相关文章

c++对象指针

对象指针在使用之前必须先进行初始化。可以让它指向一个已定义的对象&#xff0c;也可以用new运算符动态建立堆对象。 定义对象指针的格式为&#xff1a; 类名 *对象指针 &对象; //或者 类名 *对象指针 new 类名(参数); 用对象指针访问对象数据成员的格式为&#xff1a…

排序算法-归并排序

Leetcode链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 归并&#xff1a;将原始数组划分为若干个子数组&#xff0c;然后将这些子数组分别排序&#xff0c;最后再将已排序的子数组合并成一个有序的数组。是一种分治思想 思路&#xff1a; 1.分 2.治 3.怎么治 …

深度解析C语言——预处理详解

对C语言有一定了解的同学&#xff0c;相信对预处理一定不会陌生。今天我们就来聊一聊一些预处理的相关知识。预处理是在编译之前对源文件进行简单加工的过程&#xff0c;主要是处理以#开头的命令&#xff0c;例如#include <stdio.h>、#define等。预处理是C语言的一个重要…

2024最新软件测试【测试理论+ 抓包与网络协议】面试题(内附答案)

一、测试理论 3.1 你们原来项目的测试流程是怎么样的? 我们的测试流程主要有三个阶段&#xff1a;需求了解分析、测试准备、测试执行。 1、需求了解分析阶段 我们的 SE 会把需求文档给我们自己先去了解一到两天这样&#xff0c;之后我们会有一个需求澄清会议&#xff0c; …

springboot程序文件上传集成腾讯云cos

前提&#xff1a;有腾讯云服务器并开通cos对象存储 创建cos存储桶&#xff08;访问权限需要设置为共有读私有写&#xff0c;这样到时上传的文件才可以通过链接访问&#xff09; 创建cos对象存储访问密钥拿到secretId和secretKey 注意创建的密钥一定要保存好后期是无法再次次…

工作常用设计模式

设计模式分类 创建者模式&#xff08;5种&#xff09; 单例模式原型模式工厂方法模式抽象工厂模式建造者模式 结构型模式&#xff08;7种&#xff09; 代理模式适配器模式桥接模式装饰者模式外观模式享元模式组合模式 行为型模式&#xff08;11种&#xff09; 模板方法模…

pytorch | torch.contiguous()方法

torch.contiguous()方法语义上是“连续的”&#xff0c;经常与torch.permute()、torch.transpose()、torch.view()方法一起使用&#xff0c;要理解这样使用的缘由&#xff0c;得从pytorch多维数组的低层存储开始说起&#xff1a; touch.view()方法对张量改变“形状”其实并没有…

【讲解下Gitea】

&#x1f308;个人主页:程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

区块链web3智能合约开发学习-开发工具Remix(1)

学习区块链中常用语言solidity时&#xff0c;我们会用到特别的开发工具&#xff0c;对于学习前期&#xff0c;建议是将代码写到Remix IDE中进行编译部署和测试&#xff0c;这就是我们编写和交互智能合约的地方&#xff0c; 在线remix编译器&#xff1a; https://remix.ethereu…

阿里在GitHub发布AI大模型新项目,让张国荣“复活”!

最近&#xff0c;阿里巴巴又推出了一款AI视频新框架——EMO。 特点及官方演示 EMO的功能特点如下&#xff1a; 1.音频驱动的视频生成&#xff1a;根据输入的音频&#xff08;如说话或唱歌&#xff09;和参考图像&#xff0c;生成具有表情变化和头部动作的虚拟角色视频。 2.多语…

Nginx反向代理和缓存

一、Nginx反向代理 1.调度和代理的区别&#xff1a; 1.调度基于内核层面&#xff0c;代理基于应用层面 2.代理必须实现一手托两家 3.调度不需要监听任何端口&#xff0c;不需要工作任何应用程序&#xff0c;代理需要工作和上游服务器一模一样的进程 4.调度没有并发上限&am…

在编程中使用中文到底该不该??

看到知乎上有个热门问题&#xff0c;为什么很多人反对中文在编程中的使用&#xff1f; 这个问题有几百万的浏览热度&#xff0c;其中排名第一的回答非常简洁&#xff0c;我深以为然&#xff1a; 在国内做开发&#xff0c;用中文写注释、写文档&#xff0c;是非常好的习惯&…

Java练习

这个练习我用到了继承&#xff0c;多态和封装。 1.继承&#xff1a; Animal 类是一个抽象类&#xff0c;它有两个子类 Dog 和 Cat。 Dog 和 Cat 分别继承自 Animal 类&#xff0c;因此它们可以使用 Animal 类中定义的属性和方法&#xff0c;同时也可以有自己特有的属性和方法。…

【御控物联】JSON结构数据转换在物联业务中应用(场景案例二)

文章目录 一、物联网业务场景现状二、物联网业务场景数据交互格式三、JSON格式数据转换案例四、JSON数据格式转换DEMO五、在线转换工具六、技术资料 一、物联网业务场景现状 目前&#xff0c;市场上多数物联网关与物联平台捆绑售卖&#xff0c;网关采集到设备数据只能按照指定…

macOS搭建php环境以及调试Symfony

macOS搭建php环境以及调试Symfony macOS搭建php环境以及调试Symfony 古老的传说运行环境快速前置安装环境 php 的安装安装 Xdebug 来调试 php如何找到你的 php.iniXdebug 安装成功 创建并调试的 Hello world 安装 PHP Debug 安装 Symfony 安装 Composer安装 Symfony CLI 创建 …

基于Springboot旅游网站管理系统设计和实现

基于Springboot旅游网站管理系统设计和实现 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系…

ssm013小型企业办公自动化系统的设计和开发+vue

小型企业办公自动化系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对小型企业办公信息管理混乱&am…

Linux系统----------探索mysql数据库MHA高可用

目录 一、MHA概述 1.1 什么是 MHA 1.2MHA 的组成 1.2.1MHA Node&#xff08;数据节点&#xff09; 1.2.2MHA Manager&#xff08;管理节点&#xff09; 1.3MHA 的特点 1.4MHA工作原理 1.5数据同步的方式 1.5.1同步复制 1.5.2异步复制 1.5.3半同步复制 二、搭建 MySQ…

时间管理系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)大学生

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读300套最新项目持续更新中..... 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含ja…

简单工厂模式

1.1 面试受挫 package com.lhx.design.pattern.test;import java.util.Scanner;public class Test {public static void main(String[] args){System.out.println("**********************************************"); System.out.println("《大话设计模式》代…
推荐文章