从零开始的c++之旅——多态

1. 多态的概念

        通俗来说就是多种形态。

        多态分为编译时多态(静态多态)和运行时多态(动态多态)。

        编译时多态主要就是我们之前提过的函数重载和函数模板,同名提高传不同的参数就可以调
        用不同的函数,通过参数不同达到多种形态,由于他们实参传递给形参匹配是在编译时完}
        成,我们把编译时⼀般归为静态,运⾏时归为动态。

        运行时多态,就是指完成某个行为,通过传不同的参数可以产生不同的行为,达到多种形
        态。比如买票,普通人全价购买,学生则可以搬家,军人则是优先买票。

2. 多态的定义及实现

2.1 多态的构成条件

        多态就是一个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了 Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态还有两个必须的条件:

        • 必须指针或者引⽤调⽤虚函数

        • 被调⽤的函数必须是虚函数。

要想实现多态的效果,第一必须是基类的指针或者引用,因为只有基类指针或者引用才即可以指向基类的对象又可以指向派生类对象。第二派生类必须对基类的虚函数重写/覆盖,只有重写/覆盖之后,派生类才能有不同的形态,达到多态的效果。

 2.1.2 虚函数

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

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

这里的virtual与虚继承的vircual式一个关键字,但是不同的作用,我们一定要区分清楚。

2.1.3 虚函数的重写/覆盖

        若派生类和基类有一个完全相同的基函数(要求三同,即函数返回值相同,函数名相同,函
        数参数的个数及类型和顺序相同),称派生类的虚函数重写了基类的虚函数。

        注意: 在重写基类虚函数时候,派生类虚函数在不加virtual的情况下,也构成重写,因为派
        生类把基类继承下来了,其依然保持虚函数的属性),但这种写法不规范,也不推荐,但这
        是比试中的一大坑点,需要注意一下。

2.1.4 多态场景的⼀个选择题

以下程序输出结果是什么()

A: A->0  B: B->1  C: A->1  D: B->0  E: 编译出错  F: 以上都不正确

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;
}

这是一道非常经典的面试题,许多大厂曾经都考过。

        首先,调用了test函数之后又调用了fun函数,这里我们先要明确的一点是这个类的成员函数因为是在A的类域里面,使用调用的是A的this指针,这符合了构成多态的第一条规则,即调用了基类的指针。
        第二我们可以发现基类的虚函数fun和派生类的有着相同的返回值,函数名,以及参数列表,因此也符合了构成多态的第二条规则,因此fun函数构成了多态。
        构成多态之后,由于调用的是 p->test() ,p是B类型的指针,因此调用的是派生类的fun函数。
        但这里还有一个坑点,由于这两个函数的虚函数参数的缺省值不同,可能很多人都会认为调用的是 val =0 的缺省值。但虚函数的重写/覆盖规则,原理是将基类的虚函数覆盖到派生类的虚函数,因此这里的缺省值用的其实是1,所以最后输出的是 B->1 。

2.1.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();
}
析构函数的重写

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

有以下程序

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};
// 只有派⽣类B的析构函数重写了A的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

 若基类的析构函数不加virtual,那么delete p2的时候只会调用析构A的析构函数。这就会导致吧B中申请的内存没有及时还给系统,造成内存泄漏。

 如何解决这样的问题?

        首先我们要明白delete的工作原理,首先调用对应的析构函数 p->destructor(),再调用重载的operator delete[ ]清理空间,所以我们可以得出问题出现在第一步,由于p1,p2都是A* 类型的指针,所以我们在基类A的析构函数前面加上virtual使其与派生类虚函数构成重写,只有重写之后形成了多态,才能保证根据指向的对象不同产生不同的行为,调用对应的析构函数。

析构函数需不需要重写? 这个问题⾯试中经常考察,⼤家⼀定要结合类似上面的样例才能讲清楚,为什么基类中的析构 函数建议设计为虚函数。

 2.1.6 override和final关键字

        override函数可以帮我们检测出是否重写。
        因为动态多态在编译期间是无法检测出问题的,只有在运行期间我们根据输入没有得出我们
        需要的结果时候才会发现错误,因此有了这个关键字之后我们在编译期间就可以调试出错
        误。

        如果我们不想让派 ⽣类重写这个虚函数,那么可以⽤final去修饰。

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

3. 纯虚函数和抽象类

        在虚函数的后面加上 “ = 0 ” ,这个函数就被叫做纯虚函数。纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写,但是语法上可以实现),只要声明即可。有包含了纯虚函数的类被称为抽象类,抽象类不能实例化出对象,若派生类当中无纯虚函数,但是继承的基类当中有纯虚函数,那么这个派生类也是抽象类。

        纯虚函数在某种意义上强制了派生类重写虚函数,因为如果不重写的就实例化不出对象。

下面举一个简单的例子

        比如我们创建一个汽车类,基类是汽车,派生类是具体的品牌,我们不希望基类实例化出对象,因为对单独的车实例化的对象没意义,因此我们便在基类Car中写一个纯虚函数使其变为抽象类。

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()
{// 编译报错:error C2259: “Car”: ⽆法实例化抽象类 Car car;Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

4. 多态的原理

4.1 虚函数表指针

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

对于以上的程序,可能会有人认为输出是8字节大小,但是,实际上输出大小是12字节/16字节。

b对象中除了成员变量 _h 和 _ch 还多了一个 指针 _vfptr,我们称其为虚函数表指针。一个含有虚函数的类至少都有一个虚函数表指针,因为这个类中所有的虚函数的地址都会被放在这个虚函数表指针指向的一个指针数组也就是虚函数表中,虚函数也简称虚表。

4.2 多态的原理

 4.2.1 多态是如何实现的

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
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;
}

        从底层来看,上述代码的Func函数中的ptr->BuyTicket(),是如何做到ptr指向Person对象就调用Person对象的BuyTicket,指向Student对象就调用Student对象的BuyTickrt函数的呢?

        在4.1中我们提到过,每一个包含了虚函数的类中都有一个虚函数表指针(也就是虚表)存放着这个类中所有的虚函数的地址。在满足了多态的条件之后,底层就不再是编译的时候通关调用对象来确定函数的地址了,而是通过运行时指向的对象来确定对应对象的虚表中对应的虚函数地址。

        这样就实现了指针指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。

下图调用的是Person对象虚表中的虚函数。

下列对象调用的是Student对象中的虚函数。 

我们可以看到这两个函数虽然同名,但是是存放在了不同的地址。

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下是存在代码段(常量区)。

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

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

相关文章

火山引擎VeDI数据服务平台:在电商场景中,如何解决API编排问题?

01 平台介绍 数据服务平台可以在保证服务高可靠性和高安全性的同时&#xff0c;为各业务线搭建数据服务统一出口&#xff0c;促进数据共享&#xff0c;为数据和应用之间建立了一座“沟通桥梁”。 同时&#xff0c;解决数据理解困难、异构、重复建设、审计运维困难等问题&#x…

Object 内部类 异常

Objbect类 java提供了Object,它是所有类的父类,每个类都直接或间接的继承了Object类,因此Object类通常被称为超类 当定义一个类时,如果没有使用extends关键字直接去指定父类继承,只要没有被继承的类,都是会默认的去继承Object类,超类中定义了一些方法 方法名称方法说明boole…

Linux 高级IO

学习任务&#xff1a; 高级 I/O&#xff1a;select、poll、epoll、mmap、munmap 要求&#xff1a; 学习高级 I/O 的用法&#xff0c;并实操 1、高级 I/O&#xff1a; 前置知识&#xff1a; 阻塞、I/O 多路复用 PS: 非阻塞 I/O ------ 非阻塞 I/O 阻塞其实就是进入了休眠状态&am…

JAVA WEB — HTML CSS 入门学习

本文为JAVAWEB 关于HTML 的基础学习 一 概述 HTML 超文本标记语言 超文本 超越文本的限制 比普通文本更强大 除了文字信息 还可以存储图片 音频 视频等标记语言 由标签构成的语言HTML标签都是预定义的 HTML直接在浏览器中运行 在浏览器解析 CSS 是一种用来表现HTML或XML等文…

雷池社区版 7.1.0 LTS 发布了

LTS&#xff08;Long Term Support&#xff0c;长期支持版本&#xff09;是软件开发中的一个概念&#xff0c;表示该版本将获得较长时间的支持和更新&#xff0c;通常包含稳定性、性能改进和安全修复&#xff0c;但不包含频繁的新特性更新。 作为最受欢迎的社区waf&#xff0c…

出海企业如何借助云计算平台实现多区域部署?

云计算de小白 如需进一步了解&#xff0c;请单击链接了解有关 Akamai 云计算的更多信息 在本文中我们将告诉大家如何在Linode云计算平台上借助VLAN快速实现多地域部署。 首先我们需要明确一些基本概念和思想&#xff1a; 部署多区域 VLAN 为了在多区域部署中在不同的 VLAN …

AI赋能酒店设计|莱佛士学生成功入围WATG设计大赛

近日&#xff0c;由Wimberly Allison Tong & Goo&#xff08;WATG&#xff09;主办的“用人工智能重新构想酒店行业的未来”设计比赛正式拉开帷幕。这场设计比赛&#xff0c;不仅是为了庆祝WATG即将步入80周年&#xff0c;更是为了激发年轻设计师们的创造力和探索实践精神&…

Netty原来就是这样啊(二)

前言: Netty其实最大的特点就是在于对于对NIO进行了进一步的封装,除此以外Netty的特点就是在于其的高性能 高可用性,下面就会一一进行说明。 高性能: 我在Netty原来就是这样啊(一)-CSDN博客 解释了其中的零拷贝的技术除此以外还有Reactor线程模型,这个Reactor线程模型的思想…

对于相对速度的重新理解

狭义相对论速度合成公式如下&#xff0c; 现在让我们尝试用另一种方式把它推导出来。 我们先看速度的定义&#xff0c; 常规的速度合成方式如下&#xff0c; 如果我们用速度的倒数来理解速度&#xff0c; 原来的两个相对速度合成&#xff0c; 是因为假定了时间单位是一样的&am…

idea 导入Spring源码遇到的坑并解决

1.下载相关文件 通过百度网盘分享的文件&#xff1a;Spring 链接&#xff1a;https://pan.baidu.com/s/1r9rkGOCaY9SFn9ecng5cIg?pwd8888 提取码&#xff1a;8888 2.配置gradle环境 gradle下载地址 需要翻墙下 https://services.gradle.org/distributions/ 我选择的是 grad…

红队-linux基础(1)

声明 通过学习 泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频,做出的文章如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 一.openssl 1、openssl passwd -1 123 openssl是一个开源的…

迈入国际舞台,AORO M8防爆手机获国际IECEx、欧盟ATEX防爆认证

近日&#xff0c;深圳市遨游通讯设备有限公司&#xff08;以下简称“遨游通讯”&#xff09;旗下5G防爆手机——AORO M8&#xff0c;通过了CSA集团的严格测试和评估&#xff0c;荣获国际IECEx及欧盟ATEX防爆认证证书。2024年11月5日&#xff0c;CSA集团和遨游通讯双方领导在遨游…

[Unity Demo]从零开始制作空洞骑士Hollow Knight第十八集补充:制作空洞骑士独有的EventSystem和InputModule

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、制作空洞骑士独有的EventSystem和InputModule总结 前言 hello大家好久没见&#xff0c;之所以隔了这么久才更新并不是因为我又放弃了这个项目&#xff0c;而…

你们要的App电量分析测试来了

Batterystats 是包含在 Android 框架中的一种工具&#xff0c;用于收集设备上的电池数据。您可以使用 adb 将收集的电池数据转储到开发计算机&#xff0c;并创建一份可使用 Battery Historian 分析的报告。Battery Historian 会将报告从 Batterystats 转换为可在浏览器中查看的…

<项目代码>YOLOv8 学生课堂行为识别<目标检测

YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0c;YOLOv8具有更高的…

智慧水肥一体化:道品科技现代农业的智能管理模式

智慧水肥一体化是现代农业中一种重要的管理模式&#xff0c;它通过信息技术和物联网技术的结合&#xff0c;实现对水资源和肥料的智能化管理。这一系统的主要功能包括环境监测、集中管理、智能控制、主动报警和数据管理。以下将分别对这些功能进行详细阐述&#xff0c;并探讨智…

ES入门:查询和聚合

安装完ElasticSearch 和 Kibana后我们开始学习 为了方便测试&#xff0c;使用kibana的dev tool来进行学习测试&#xff1a; 测试工具 从索引文档开始 插入 向 Elasticsearch 索引 customer 的 _doc 类型的文档 id 为 1 的文档发送 PUT 请求的例子。 请求体为 JSON 格式&am…

Docker Remote API TLS 认证_docker远程接口未授权访问漏洞怎么解决

漏洞描述&#xff1a; Docker Remote API 是一个取代远程命令行界面的REST API&#xff0c;其默认绑定2375端口&#xff0c;如管理员对其配置不当可导致未授权访问漏洞。攻击者利用docker client或者http直接请求就可以访问这个API&#xff0c;可导致敏感信息泄露&#xff0c;…

华为eNSP:QinQ

一、什么是QinQ&#xff1f; QinQ是一种网络技术&#xff0c;全称为"Quantum Insertion"&#xff0c;也被称为"Q-in-Q"、"Double Tagging"或"VLAN stacking"。它是一种在现有的VLAN&#xff08;Virtual Local Area Network&#xff0…

利用SCF文件构建网络渗透

SMB是一种广泛用于企业组织中文件共享目的的网络协议。在内部的渗透测试中&#xff0c;发现包含明文密码和数据库连接字符串等敏感信息的共享文件并不罕见。但是&#xff0c;即使一个文件共享不包含任何可用于连接到其他系统的数据&#xff0c;但是未经身份验证的用户配置拥有该…