★ C++进阶篇 ★ 多态

Ciallo~(∠・ω< )⌒☆ ~ 今天,我将继续和大家一起学习C++进阶篇第一章----多态 ~

❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️

澄岚主页:椎名澄嵐-CSDN博客

C++基础篇专栏:★ C++基础篇 ★_椎名澄嵐的博客-CSDN博客

C++进阶篇专栏:★ C++进阶篇 ★_椎名澄嵐的博客-CSDN博客

❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️

目录

一  多态的概念

二  多态的定义及实现

2.1 多态的构成条件

2.2 虚函数

2.3 虚函数的重写

2.4 多态经典选择题

2.5 虚函数重写的其他问题

2.6 override & final

2.1.7 重载/重写/隐藏的对比

三  纯虚函数和抽象类

四  多态的原理

4.1 虚函数表指针

4.2 多态的原理

4.2.1 多态是如何实现的

4.2.2 动态绑定与静态绑定

4.2.3 虚函数表


一  多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)和运行时多态(动态多态)。编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。

运行时多态,具体就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。⽐如买票这个行为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。

二  多态的定义及实现

2.1 多态的构成条件

多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象优惠买票。

实现多态的两个条件:

  • 必须指针或者引用调用虚函数:只有基类的指针或引用才能指向派生类对象
  • 被调用的函数必须是虚函数:派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到。

2.2 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修 饰。

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};

2.3 虚函数的重写

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

以下是一个关于买票的多态实验~

class Person // 父类对象
{
public:virtual void BuyTicket() // 必须是虚函数~{cout << "买票-全价" << endl;}
};class Student : public Person // 子类对象
{
public:virtual void BuyTicket() // 最好虚函数~对父类的虚函数重写{cout << "买票-半价" << endl;}
};void Func(Person* ptr) // 传指针或引用~ 既可以传父类对象指针又可以传子类指针~
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps); // 买票-全价Func(&st); // 买票-半价return 0;
}

传不同类型的指针调用不同的函数。

派生类的虚函数在不加virtual关键字,也可以构成重写,但该写法不是很规范。(会出选择题)

我们再来写一个动物叫声的多态情况~

class Animal
{
public:virtual void talk() const{}
};
class Dog : public Animal
{
public:virtual void talk() const{std::cout << "汪" << std::endl;}
};
class Cat : public Animal
{
public:virtual void talk() const{std::cout << "喵₍˄·͈༝·͈˄*₎◞ ̑̑" << std::endl;}
};
void letsHear(const Animal& animal)
{animal.talk();
}

 

2.4 多态经典选择题

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

重写的本质是重写虚函数的实现在继承中绝不重新定义继承下来的缺省值

2.5 虚函数重写的其他问题

  • 协变

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不大,了解⼀下即可。

class A {};
class B : public A {};
class Person {
public:virtual A* BuyTicket(){cout << "买票-全价" << endl;return nullptr;}
};
class Student : public Person {
public:virtual B* BuyTicket(){cout << "买票-打折" << endl;return nullptr;}
};
void Func(Person* ptr)
{ptr->BuyTicket();
}
int main()
{Person ps;Student st;Func(&ps); //买票 - 全价Func(&st); //买票 - 打折return 0;
}

  • 析构函数的重写

基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

以上代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。

2.6 override & final

从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰

加了override,程序就会在函数名写错参数写错时,用红线标出。

 

2.1.7 重载/重写/隐藏的对比

  •  重载 

1. 两个函数在同一作用域。

2. 函数名相同,参数不同,参数类型或参数个数不同,返回值可同可不同。

  •  重写 

1. 两个函数分别在继承体系的父类和子类不同作用域。

2. 除协变外,函数名参数返回值都相同。

3. 两个函数都是虚函数。

  •  隐藏 

1. 两个函数分别在继承体系的父类和子类不同作用域。

2. 函数名相同。

3. 两个函数只要不构成重写就是隐藏。

4.父子类的成员变量相同也叫隐藏。

三  纯虚函数和抽象类

虚函数的后面写上 =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 car;// 编译报错:error C2259: “Car”: 无法实例化抽象类Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

四  多态的原理

4.1 虚函数表指针

我们先来看下面这个代码~

class Base
{
public:virtual void Func1(){ cout << "Func1()" << endl; }virtual void Func2(){ cout << "Func2()" << endl; }protected:int _b = 1;//4char _ch = 'x';//1->4
};
int main()
{Base b;cout << sizeof(b) << endl; //12return 0;
}

 在32位下程序的运行结果为12,除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

4.2 多态的原理

4.2.1 多态是如何实现的

从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调⽤Student::BuyTicket的呢?通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:string _name;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
protected:int _id;
};class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
protected:string _codename;
};void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();
}int main()
{// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后// 多态也会发⽣在多个派⽣类之间。Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}

指向哪个对象,运行时,到指定对象的虚函数表中找到对应的虚函数地址,进行调用。 

 

4.2.2 动态绑定与静态绑定

  • 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)

4.2.3 虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址
  • 派⽣类由两部分构成继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
  • 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址
  • 派⽣类的虚函数表中包含,基类的虚函数地址派生类重写的虚函数地址派生类自己的虚函数地址三个部分。
  • 虚函数表本质是一个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000 标记,g++系列编译不会放)
  • 虚函数存在哪的?虚函数和普通函数一样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中
  • 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)
int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
栈:010FF954
静态区:0071D000
堆:0126D740
常量区:0071ABA4
Person虚表地址:0071AB44
Student虚表地址:0071AB84
虚函数地址:00711488
普通函数地址:007114BF

~完~

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

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

相关文章

make 和 Makefile/makefile

1.概念 make 是一条命令 &#xff0c; Makefile/makefile是一个文件。 【 makefile 是一个 写了如何编译文件&#xff0c;形成可执行程序的文件】 2. 语法 1. 基本语法 依赖关系 // 依赖关系 由 目标名和依赖文件列表组成&#xff0c;语法为 目标名 : 依赖文件列表 【Ta…

Playwright快速入门(TypeScript版)

文章目录 1. 前言1. 系统环境要求2. Playwright介绍3. 安装Playwright4. 运行示例测试5. HTML 测试报告6. 在UI模式下运行测试示例7. 更新Playwright版本 1. 前言 Playwright 相比 Selenium&#xff0c;具有多浏览器支持、现代化 API、更快性能、精细页面控制、自动等待元素、…

医学数据分析实训 项目三 关联规则分析作业--在线购物车分析--痹症方剂用药规律分析

文章目录 项目三 关联规则分析一、实践目的二、实践平台三、实践内容任务一&#xff1a;在线购物车分析&#xff08;一&#xff09;数据读入&#xff08;二&#xff09;数据理解&#xff08;三&#xff09;数据预处理&#xff08;四&#xff09;生成频繁项集&#xff08;五&…

什么是 HTTP/3?下一代 Web 协议

毫无疑问&#xff0c;发展互联网底层的庞大协议基础设施是一项艰巨的任务。 HTTP 的下一个主要版本基于 QUIC 协议构建&#xff0c;并有望提供更好的性能和更高的安全性。 以下是 Web 应用程序开发人员需要了解的内容。 HTTP/3 的前景与风险 HTTP/3 致力于让互联网对每个人…

[数据集][图像分类]茶叶病害分类数据集6749张7类别

数据集类型&#xff1a;图像分类用&#xff0c;不可用于目标检测无标注文件 数据集格式&#xff1a;仅仅包含jpg图片&#xff0c;每个类别文件夹下面存放着对应图片 图片数量(jpg文件个数)&#xff1a;6749 分类类别数&#xff1a;7 类别名称:["Unlabeled","alg…

项目实现:云备份②(文件操作、Json等工具类的实现)

云备份 前言文件操作实用工具类设计文件属性的获取文件的读写操作文件压缩与解压缩的实现文件目录操作 Json 实用工具类设计编译优化 前言 如果有老铁不知道当前项目实现的功能是什么的话&#xff0c;可以先移步这篇文章内容&#xff1a; 云备份项目的介绍 其中介绍了云备份项…

在 Dify 中高效利用 SiliconCloud API

引言 SiliconCloud 以其丰富的模型库和卓越的处理速度&#xff0c;在 Dify 平台上实现高效工作流和智能代理变得轻而易举。本文将指导您如何在 Dify 中集成 SiliconCloud 的强大功能。 集成 SiliconCloud 模型 步骤一&#xff1a;设置 SiliconCloud 账户 首先&#xff0c;您…

5. Python之数据类型

Python数据类型有数值型&#xff0c;字符串型&#xff0c;布尔型等等 内置函数type()&#xff0c;可以查看变量的数据类型 。 一、数值类型 整数&#xff08;没有小数部分&#xff0c;包含正整数&#xff0c;负整数&#xff0c;0&#xff0c;默认为十进制数&#xff09;&…

PHP:强大的Web开发语言

PHP&#xff1a;强大的Web开发语言 一、PHP 简介及优势 PHP 的基本概念 PHP&#xff08;PHP: Hypertext Preprocessor&#xff09;即 “超文本预处理器”&#xff0c;是一种通用开源脚本语言&#xff0c;最初由 Rasmus Lerdorf 于 1994 年创建。它可以在服务器上执行&#xf…

正则表达式匹配整数与浮点数失败与解决方案

正则表达式匹配整数与浮点数失败与解决方案 问题描述问题分析解决方案总结 问题描述 在处理数据的时候需要提取文本内整数与浮点数&#xff0c;这个时候想到使用正则表达式&#xff0c;咨询百度文心一言给出以下方案及参考代码 import re text "我有100元&#xff0c;…

华为地图服务功能概览 -- HarmonyOS自学7

华为地图服务式Harmony OS生态下的一个地图服务&#xff0c;为开发者提供强大而便捷的地图能力&#xff0c;助力全球开发者实现个性化地图呈现&#xff0c;地图搜索和路线规划功能。 主要包括七大功能&#xff1a;静态图&#xff0c;场景化控件&#xff0c;地点搜索&#xff0c…

函数的认识(二)

函数的基础知识可查看&#xff1a;函数的认识&#xff08;一&#xff09; &#xff08;1&#xff09;函数说明文档 函数是纯代码语言&#xff0c;想要理解其含义&#xff0c;就需要一行行的去阅读理解代码&#xff0c;效率比较低。 我们可以给函数添加说明文档&#xff0c;辅…

Python 解析 Charles JSON Session File (.chlsj)

Charles 代理&#xff0c;是一款抓包软件&#xff0c;可以帮助我们抓取浏览器请求跟响应。 1、在 Filter 里面输入需要抓包的网址 2、右键 Export Session 3、文件类型选择 JSON Session File (.chlsj) 保存 4、解析响应的数据结构 response.body.text 是文本字符串。 # 导入…

Navicat使用 笔记04

Navicat调用数据库 1.创建一个自己的链接&#xff08;文件-->新建连接-->MySQL&#xff09; 进入到这个界面中&#xff1a; 【注意&#xff1a;密码是下载登录软件时设定过的】 创建一个连接完成&#xff08;通过双击激活&#xff09;。 2.在创建好的连接中创建数据库…

Linux-mysql5.7-mysql8.0安装包下载及安装教程,二合一

一、安装包下载 1、手动下载 MySQL :: Download MySQL Community Server 2、wegt下载 wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.24-linux-glibc2.12-x86_64.tar.gz 登录自己的liunx &#xff0c;复制上面的命令下载。 二、手动安装 1、上传压缩包到…

蓝桥杯-基于STM32G432RBT6的LCD进阶(LCD界面切换以及高亮显示界面)

目录 一、页面切换内容详解 1.逻辑解释 2.代码详解 code.c&#xff08;内含详细讲解&#xff09; code.h main.c 3.效果图片展示 ​编辑 二、页面选项高亮内容详解 1.逻辑解释 2.读入数据 FIRST.第一种高亮类型 code.c&#xff08;内含代码详解&#xff09; code.…

kubernetes中pause容器的作用与源码详解

概述 摘要&#xff1a;上一篇文章我们介绍了kubernetes是如何通过pause容器来构建一个pod。本文我们对pause容器做一个总结&#xff0c;并再此基础上次深入浅出&#xff0c;从pause容器的源码详细了解pause容器的实现原理。 正文 pause容器是什么 在 Kubernetes 中&#xff…

超高速传输 -- Fixed Grid与Flexible Grid

Fixed Grid 传统的40波/80波DWDM系统采用Fixed Grid固定光谱方式&#xff0c;波长间隔固定为100GHz或者50GHz&#xff0c;中心频率也是固定的。在Fixed Grid方式下&#xff0c;带宽无法灵活调配。 随着系统中信号速率的提升&#xff0c;光信号的频谱也在变宽&#xff1b;当多速…

SpringSecurity原理解析(八):CSRF防御解析

一、CsrfFilter CsrfFilter 主要功能是用来防止csrf攻击 一、什么是CSRF攻击 跨站请求伪造&#xff08;英语&#xff1a;Cross-site request forgery&#xff09;&#xff0c;也被称为 one-click attack 或者 session riding&#xff0c;通常缩写为 CSRF 或者 XSRF&#xff0c…

有关C# .NET Core 过滤器的使用

想用一个过滤器实现特定接口的审核日志记录&#xff0c;结果报了错&#xff0c;看了看感觉有些基础要补&#xff0c;所以想记录下来 错误&#xff1a; 在属性过滤器中使用了依赖注入&#xff0c;结果在应用在控制层接口时报了传参的错 //过滤器 public class AuditRecordFil…