Cpp多态机制的深入理解(20)

文章目录

  • 前言
  • 一、多态的概念
  • 二、多态的定义与实现
    • 两个必要条件
    • 虚函数
    • 虚函数的重写
    • 重写的三个例外
    • override 和 final
    • 重载、重写(覆盖)、重定义(隐藏)
  • 三、抽象类
    • 概念
    • 接口继承和实现继承
  • 四、多态的原理
    • 虚表和虚表指针
    • 虚函数调用过程
    • 动态绑定与静态绑定
  • 五、那...那单继承甚至多继承呢?
  • 总结


前言

  多态也是三大面向对象语言的特性之一,同时我也觉得他也蛮有意思的
  与封装“一个方法,多个接口”不同的是,多态可以实现 “一个接口,多种方法

  调用同名函数时,可以根据不同的对象(父类对象或子类对象)调用属于自己的函数,实现不同的方法,因此 多态 的实现依赖于 继承


一、多态的概念

  在使用多态的代码中,不同对象完成同一件事会产生不同的结果

  比如在购买高铁票时,普通人原价,学生半价,而军人可以优先购票,对于 购票 这一相同的动作,需要 根据不同的对象提供不同的方法

二、多态的定义与实现

两个必要条件

  1. virtual 修饰后形成的虚函数,与其他类中的虚函数形成 重写(三同:返回值、函数名、参数均相同)
  2. 必须通过【父类指针】或【父类引用】进行虚函数调用

在这里插入图片描述

虚函数

  被virtual修饰的类成员函数称为虚函数

全局虚函数没有意义,因为虚函数是为多态而用的

在这里插入图片描述

虚函数的重写

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

// 基类
class Person 
{
public:// 虚函数virtual void BuyTicket() { cout << "买票-全价" << endl; }
};// 派生类
class Student : public Person 
{
public:// 虚函数重写virtual void BuyTicket() { cout << "买票-半价" << endl; }
};// 三种函数实现
// 引用
void Func(Person& p)
{p.BuyTicket();
}// 指针
//void Func(Person* p)
//{
//	p->BuyTicket();
//}// 非引用指针,调用父类
//void Func(Person p)
//{
//	p.BuyTicket();
//}

测试结果:
在这里插入图片描述

重写的三个例外

  1. 协变(基类与派生类虚函数返回值类型不同)

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

这个了解一下就行,实际我感觉挺没啥用处的
如果你也有这种感觉,鼓励你致电老本,去好好批斗他!

class A {};
class B : public A {};class Person
{
public:// 协变 返回值可以是父子类对象指针或引用//virtual A* BuyTicket() // 返回值是父类指针virtual Person* BuyTicket(){cout << "Person-> 买票-全价" << endl;return nullptr;}
};class Student : public Person
{
public://virtual B* BuyTicket()// 返回值是子类指针virtual Student* BuyTicket(){cout << "Student-> 买票-半价" << endl;return nullptr;}
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

  如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

class Person
{
public:// 析构函数名不同,构成重写,编译器将析构函数名字统一处理成destructorvirtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:virtual ~Student(){cout << "delete[]" << _ptr << endl;delete[] _ptr;cout << "~Student()" << endl;}
private:int* _ptr = new int[10];
};void Func(Person& p)
{p.BuyTicket();
}int main()
{// 正常情况调用析构没有问题//Person p;//Student s;//Func(p);//Func(s);// 派生类有动态开辟的内存,需要调用多态// 指向谁调用谁Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

在这里插入图片描述

  1. 派生类重写虚函数virtual关键字可以省略
class Person
{
public:virtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:// 派生类virtual关键字省略~Student(){cout << "~Student()" << endl;}
};

在这里插入图片描述

override 和 final

  C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载

  1. final:修饰虚函数,表示该虚函数不能再被重写
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
// final 修饰虚函数,不能重写
class Car
{
public:// 加了final关键字,虚函数不能被重写virtual void Drive() final {}
};class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};int main()
{Benz b;return 0;
}

在这里插入图片描述

重载、重写(覆盖)、重定义(隐藏)

在这里插入图片描述
在这里插入图片描述

三、抽象类

概念

  在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

class Car
{
public:// 纯虚函数 强制派生类重写虚函数 virtual void Drive() = 0;
};int main()
{Car c;return 0;
}

在这里插入图片描述

class Car
{
public:// 纯虚函数 强制派生类重写虚函数 virtual void Drive() = 0;
};class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};int main()
{// Car c;Benz b1;BMW b2;// 基类可以定义指针 指向谁调用谁Car* ptr1 = &b1;Car* ptr2 = &b2;ptr1->Drive();ptr2->Drive();return 0;
}

在这里插入图片描述

接口继承和实现继承

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

四、多态的原理

  在讲解原理之前,不如我们先来看这么一段神奇代码

#include <iostream>
using namespace std;class Test
{virtual void func() {};
};int main()
{Test t;	//创建一个对象cout << "Test sizeof(): " << sizeof(t) << endl;return 0;
}

  可能你会觉得没有对象,会觉得是0,但是你突然想起了之前讲过的空类也占内存空间,你可能会想是不是1

  但是其实都错了,真相是4/8(取决于你的系统是32位还是64位),可能我这么一说,你也猜到了其实有一个隐藏变量,且类型是指针类型

其实,就是靠着这个虚表指针和虚表实现了多态

虚表和虚表指针

  在 vs 的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表

  虚函数表中存储的是虚函数指针,可以在调用函数时根据不同的地址调用不同的方法

可能有点混,有三个“虚”,大家别被整虚了!
虚表指针指向虚表,虚表里面存放着虚函数指针,所以虚表的本质其实是个函数指针数组

  接下来我会给出一段代码,在该代码中父类 Person 有两个虚函数(func3 不是虚函数),子类 Student 重写了 func1 这个虚函数,同时新增了一个 func4 虚函数

#include <iostream>using namespace std;class Person
{
public:virtual void func1() { cout << "Person::fun1()" << endl; };virtual void func2() { cout << "Person::fun2()" << endl; };void func3() { cout << "Person::fun3()" << endl; };	//fun3 不是虚函数
};class Student : public Person
{
public:virtual void func1() { cout << "Student::fun1()" << endl; };virtual void func4() { cout << "Student::fun4()" << endl; };
};int main()
{Person p;Student s;return 0;
}

在这里插入图片描述

//打印虚表
typedef void(*VF_T)();void PrintVFTable(VF_T table[])	//也可以将参数类型设为 VF_T*
{//vs中在虚表的结尾处添加了 nullptr//如果运行失败,可以尝试清理解决方案重新编译int i = 0;while (table[i]){printf("[%d]:%p->", i, table[i]);VF_T f = table[i];f();	//调用函数,相当于 func()i++;}cout << endl;
}int main()
{//提取出虚表指针,传递给打印函数Person p;Student s;//第一种方式:强转为虚函数地址(4字节)PrintVFTable((VF_T*)(*(int*)&p));PrintVFTable((VF_T*)(*(int*)&s));return 0;
}

子类重写后的虚函数地址与父类不同
在这里插入图片描述

因为平台不同指针大小不同,因此上述传递参数的方式(VF_T * )( * (int * )&p 具有一定的局限性
假设在 64 位平台下,需要更改为 (VF_T * )( * (long long * )&p

//64 位平台下指针大小为 8字节
PrintVFTable((VF_T*)(*(long long*)&p));
PrintVFTable((VF_T*)(*(long long*)&s));

除此之外还可以间接将虚表指针转为 VF_T* 类型进行参数传递

//同时适用于 32位 和 64位 平台
PrintVFTable(*(VF_T**)&p);
PrintVFTable(*(VF_T**)&s);

传递参数时的类型转换路径
在这里插入图片描述
  不能直接写成 PrintVFTable((VF_T*)&p);,因为此时取的是整个虚表区域的首地址地址,无法定位我们所需要虚表的首地址,打印时会出错

  综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系

虚表是在 编译 阶段生成的
虚表指针是在构造函数的 初始化列表 中初始化的
虚表一般存储在 常量区(代码段),有的平台中可能存储在 静态区(数据段)

int main()
{//验证虚表的存储位置Person p;Student s;int a = 10;	//栈int* b = new int;	//堆static int c = 0;	//静态区(数据段)const char* d = "xxx";	//常量区(代码段)printf("a-栈地址:%p\n", &a);printf("b-堆地址:%p\n", b);printf("c-静态区地址:%p\n", &c);printf("d-常量区地址:%p\n", d);printf("p 对象虚表地址:%p\n", *(VF_T**)&p);printf("s 对象虚表地址:%p\n", *(VF_T**)&s);return 0;
}

在这里插入图片描述

显然,虚表地址与常量区的地址十分接近,因此可以推测 虚表 位于常量区中,因为它需要被同一类中的不同对象共享,同时不能被修改(如同代码一样)

虚函数调用过程

综上,我们可以大概想象出多态的原理了:

  1. 首先确保存在虚函数且构成重写
  2. 其次使用【父类指针】或【父类引用】指向对象,其中包含切片行为
  3. 切片后,将子类中不属于父类的切掉,只保留父类指针可调用到的部分函数
  4. 实际调用时,父类指针的调用逻辑是一致的:比如虚表第一个位置调用第一个函数,虚表第二个位置调用第二个函数,但是因为此时的虚表是切片得到的,所以 同一位置 可以调用到不同的函数,这就是多态

也就是说,父类和子类的虚表其实是不一样的,在构成重写的前提下!
这就是多态!

int main()
{Person* p1 = new Person();Person* p2 = new Student();p1->func1();p2->func1();delete p1;delete p2;return 0;
}

通过汇编代码来看的话:
在这里插入图片描述
在这里插入图片描述

动态绑定与静态绑定

  其实我们想一想,函数重载某种程度上也是一种多态,也是一个函数面对不同对象的时候有不同的效果,但是不同的是,重载在编译的时候就确定了待调用函数的地址,而动态绑定的代码,待调用地址存放在 eax 中,不确定
在这里插入图片描述

五、那…那单继承甚至多继承呢?

  坦白说,这很麻烦,我也不敢说我很懂,于是我在这里贴两篇文章,大家自行参阅吧!

《C++虚函数表解析》
《C++对象的内存布局》


总结

  我们终于学完三大面向对象特性了,坦白说,多态还是蛮困难的,但是,我们难度的最高峰再过几篇就要来了,怕不怕!

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

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

相关文章

数字IC后端实现之Innovus Place跑完density爆涨案例分析

下图所示为咱们社区a7core后端训练营学员的floorplan。 数字IC后端实现 | Innovus各个阶段常用命令汇总 该学员跑placement前density是59.467%&#xff0c;但跑完place后density飙升到87.68%。 仔细查看place过程中的log就可以发现Density一路飙升&#xff01; 数字IC后端物…

大数据新视界 -- 大数据大厂之大数据环境下的网络安全态势感知

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

如何解决mingw64安装后配置完环境变量仍然执行不了gcc命令以及Vscode中的环境路径配置中找不到gcc

配置环境变量教程很多&#xff0c;就不多说&#xff0c;说下耗费一小时解决的问题&#xff1a;mingw64安装后配置完环境变量仍然执行不了gcc命令 配置 了N次了&#xff0c;都还是在终端找不到指令&#xff0c;然后&#xff0c;将路径放到第一个&#xff0c;然后再看下&#xf…

【AI日记】24.11.01 LangChain、openai api和github copilot

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 工作 工作1 内容&#xff1a;学习deeplearning.ai的免费课程地址&#xff1a;LangChain Chat with Your DataB站地址&#xff1a;https://www.bilibili.com/video/BV148411D7d2时间&#xff1a;2小时评估&am…

位运算算法及习题 ,丢弃的数字 , 两整数之和 ,只出现一次的数字II

文章目录 位运算基础1.基础位运算2. 给一个数n,确定他的二进制位中的第x为是0还是13.将一个数n的二进制表示的第x位修改为14.将一个数n的二进制表示的第x位修改为05.位图的思想6. 提取一个数n二进制表示中最右侧的17. 去掉一个数n二进制表示中最右侧的18. 异或运算的运算律 丢弃…

使用form表单的action提交并接收后端返回的消息

使用form表单的action提交表单是同步提交的方式&#xff0c;会跳转页面&#xff0c;所以无法获取后端返回来到消息。这样描述或许没有太大感觉&#xff0c;如果我要通过表单的方式上传文件&#xff0c;并接收后台返回来的响应数据&#xff1b;这样说是不是就感同深受了呢。 1.…

曹操出行借助 ApsaraMQ for Kafka Serverless 提升效率,成本节省超 20%

本文整理于 2024 年云栖大会主题演讲《云消息队列 ApsaraMQ Serverless 演进》&#xff0c;杭州优行科技有限公司消息中间件负责人王智洋分享 ApsaraMQ for Kafka Serverless 助力曹操出行实现成本优化和效率提升的实践经验。 曹操出行&#xff1a;科技驱动共享出行未来 曹操…

2024年10月文章一览

2024年10月编程人总共更新了21篇文章&#xff1a; 1.2024年9月文章一览 2.《Programming from the Ground Up》阅读笔记&#xff1a;p147-p180 3.《Programming from the Ground Up》阅读笔记&#xff1a;p181-p216 4.《Programming from the Ground Up》阅读笔记&#xff…

【果蔬识别】Python+卷积神经网络算法+深度学习+人工智能+机器学习+TensorFlow+计算机课设项目+算法模型

一、介绍 果蔬识别系统&#xff0c;本系统使用Python作为主要开发语言&#xff0c;通过收集了12种常见的水果和蔬菜&#xff08;‘土豆’, ‘圣女果’, ‘大白菜’, ‘大葱’, ‘梨’, ‘胡萝卜’, ‘芒果’, ‘苹果’, ‘西红柿’, ‘韭菜’, ‘香蕉’, ‘黄瓜’&#xff09;…

Partition架构

优质博文&#xff1a;IT-BLOG-CN Partition架构 【1】结构&#xff1a; Region至少3个Zone&#xff0c;Zone内至少两个Partition&#xff0c;Partition内至少1个K8S Member Cluster&#xff1b; 【2】故障域&#xff1a; 故障域及核心链路至少Zone内收敛&#xff0c;甚至Part…

xlrd.biffh.XLRDError: Excel xlsx file; not supported

文章目录 一、问题报错二、报错原因三、解决思路四、解决方法 一、问题报错 在处理Excel文件时&#xff0c;特别是当我们使用Python的xlrd库来读取.xlsx格式的文件&#xff0c;偶尔会遇到这样一个错误&#xff1a;“xlrd.biffh.XLRDError: Excel xlsx file; not supported”。…

Java XML一口气讲完!(p≧w≦q)

Java XML API Java XML教程 - Java XML API SAX API 下面是关键的SAX API的摘要: 类用法SAXParserFactory创建由系统属性javax.xml.parsers.SAXParserFactory确定的解析器的实例。SAXParserSAXParser接口定义了几个重载的parse()方法。SAXReaderSAXParser包装一个SAXReader…

CTF顶级工具与资源

《Web安全》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484238&idx1&snca66551c31e37b8d726f151265fc9211&chksmc0e47a12f793f3049fefde6e9ebe9ec4e2c7626b8594511bd314783719c216bd9929962a71e6&scene21#wechat_redirect 《网安面试指南》h…

腾讯云视频文件上传云存储时自动将mp4格式转码成m3u8

针对问题&#xff1a; 弱网环境下或手机网络播放mp4格式视频卡顿。 存储环境&#xff1a;腾讯云对象存储。 处理流程&#xff1a; 1&#xff1a;登录腾讯云控制台&#xff0c;进入对象存储服务&#xff0c;找到对应的存储桶&#xff0c;点击进入。 在任务与工作流选项卡中找…

【AIGC】逆向拆解OpenAI官方提示词Prompt技巧:高效提升ChatGPT输出质量

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;OpenAI官方提示词的介绍OpenAI官方提示词的结构与组成如何通过分析提示词找到其核心组件 &#x1f4af;OpenAI官方提示词分析案例一&#xff1a;制定教学计划案例二&…

Linux之nfs服务器和dns服务器

NFS服务器 NFS&#xff08;Network File System&#xff0c;网络文件系统)&#xff0c;NFS服务器可以让PC将网络中的NFS服务器共享的目录挂载到本地端的文件系统中&#xff0c;而在本地端的系统 中看来&#xff0c;那个远程主机的目录就好像是自己的一个磁盘分区一样。 注&am…

【机器学习】25. 聚类-DBSCAN(density base)

聚类-DBSCAN-density base 1. 介绍2. 实现案例计算 3. K-dist4. 变化密度5. 优缺点 1. 介绍 DBSCAN – Density-Based Spatial Clustering of Applications with Noise 与K-Means查找圆形簇相比&#xff0c;DBSCAN可以查找任意形状和复杂形状的簇&#xff0c;如S形、椭圆、半圆…

计组-层次化存储结构

这里主要看存储的整体结构&#xff0c;cache&#xff0c;内存 这里看存储结构是按什么样的层次来划分存储结构&#xff0c;速度由慢到快&#xff0c;容量由大到小&#xff0c;这是基于性价比的考虑&#xff0c;所以分为多级多层次&#xff0c;可以做到提高速度的同时没有增加多…

奇瑞不客气智驾 晚不晚?

文/孔文清 一直很好奇&#xff1a; 尹同跃董事长的金句“智驾不客气”&#xff0c;应该怎么翻译成英语&#xff1f; 谷俊丽的演讲PPT给了我答案&#xff1a; All in Ai Cars ——全力以赴、全情投入智能化汽车。 谷俊丽是奇瑞全球创新大会上最兴奋的人之一&#xff0c;有一种闭…

【万兴科技-注册_登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…