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

news2024/12/25 14:03:26

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.coloradmin.cn/o/2234680.html

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

相关文章

第二十六章 Vue之在当前组件范围内获取dom元素和组件实例

目录 一、概述 二、获取dom 2.1. 具体步骤 2.2. 完整代码 2.2.1. main.js 2.2.2. App.vue 2.3. BaseChart.vue 三、获取组件实例 3.1. 具体步骤 3.2. 完整代码 3.2.1. main.js 3.2.2. App.vue 3.2.3. BaseForm.vue 3.3. 运行效果 一、概述 我们过去在想要获取一…

plt中subplot综合实战

目录 背景介绍实战 背景介绍 下面是一份贸易数据&#xff08;Prod_Trade.xlsx&#xff09;&#xff0c;需要多角度针对2012年数据进行报表分析&#xff0c;需使用subplot分格展示。Prod_Trade的数据结构包括 Date,Order_Class,Sales Transport,Trans_Cost, Region ,Category, …

DevOps开发运维简述

DevOps平台是一套集成的解决方案&#xff0c;旨在协调软件开发&#xff08;Development&#xff09;和信息技术运维&#xff08;Operations&#xff09;。它促进跨功能团队合作&#xff0c;实现自动化流程&#xff0c;确保持续集成与持续交付&#xff08;CI/CD&#xff09;。 一…

基于java+SpringBoot+Vue的微服务在线教育系统设计与实现

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis Maven mysql5.7或8.0等等组成&#x…

粒子群优化双向深度学习!PSO-BiTCN-BiGRU-Attention多输入单输出回归预测

粒子群优化双向深度学习&#xff01;PSO-BiTCN-BiGRU-Attention多输入单输出回归预测 目录 粒子群优化双向深度学习&#xff01;PSO-BiTCN-BiGRU-Attention多输入单输出回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现PSO-BiTCN-BiGRU-Attention粒子…

终端文件管理神器 !!!

项目简介 nnn是一款专为命令行爱好者打造的高效终端文件管理器。它以其超小的体积、几乎零配置的要求以及卓越的速度表现而著称。nnn不仅适用于Linux、macOS、BSD等操作系统&#xff0c;还能够在诸如树莓派、Android上的Termux、WSL、Cygwin等多个平台运行。它遵循POSIX标准&am…

Uniapp 实现app自动检测更新/自动更新功能

实现步骤 配置 manifest.json 在 manifest.json 中设置应用的基本信息&#xff0c;包括 versionName 和 versionCode。 一般默认0.0.1&#xff0c;1. 服务器端接口开发 提供一个 API 接口&#xff0c;返回应用的最新版本信息&#xff0c;版本号、下载链接。客户端检测更新 使…

基础算法——排序算法(冒泡排序,选择排序,堆排序,插入排序,希尔排序,归并排序,快速排序,计数排序,桶排序,基数排序,Java排序)

1.概述 比较排序算法 算法最好最坏平均空间稳定思想注意事项冒泡O(n)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)Y比较最好情况需要额外判断选择O( n 2 n^2 n2)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)N比较交换次数一般少于冒泡堆O( n l o g n nlogn nlogn)O( n l o g n nlogn nlogn)O( n l…

多元数据库时代,云和恩墨携手鲲鹏引领数据库一体机新变革

近年来&#xff0c;随着企业数据存储结构日益多元化&#xff0c;传统架构数据库面临发展瓶颈&#xff0c;越来越多企业倾向于采用不同类型的数据库满足多样化的数据需求。这一趋势下&#xff0c;国内数据库市场呈现百花齐放的态势&#xff0c;产业加速迈入多元数据库时代。 作为…

[SWPUCTF 2022 新生赛]Cycle Again -拒绝脚本小子,成为工具糕手

1.题目 打开&#xff0c;一张图片&#xff0c;一个压缩包 2.分析 图片丢进随波逐流中 发现第一部分的flag NSSCTF{41d769db- 丢进b神的工具中 爆出第二段flag 9f5d-455e-a458-8012ba3660f3} 两段进行拼接 NSSCTF{41d769db-9f5d-455e-a458-8012ba3660f3} 直接拿下 遥遥领…

机场电子采购信息系统

摘 要 互联网的发展&#xff0c;改变了人类原来繁琐的生活和消费习惯&#xff0c;人们的时间观念也在不断加强&#xff0c;所以各种信息系统的数量越来越多&#xff0c;方便了用户&#xff0c;用户习惯也发生了改变。对于传统的企业采购模式来说由于费用高、速度慢、不透明化…

RabbitMQ设置消息过期时间

RabbitMQ设置消息过期时间 1、过期消息&#xff08;死信&#xff09;2、设置消息过期的两种方式2.1、设置单条消息的过期时间2.1.1、配置文件application.yml2.1.2、配置类RabbitConfig2.1.3、发送消息业务类service&#xff08;核心代码&#xff09;2.1.4、启动类2.1.5、依赖文…

android数组控件Textview

说明&#xff1a;android循环控件&#xff0c;注册和显示内容 效果图&#xff1a; step1: E:\projectgood\resget\demozz\IosDialogDemo-main\app\src\main\java\com\example\iosdialogdemo\TimerActivity.java package com.example.iosdialogdemo;import android.os.Bundl…

【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/BV148411D7d2github代码&#xff1a;https:…

指标+AI+BI:构建数据分析新范式丨2024袋鼠云秋季发布会回顾

10月30日&#xff0c;袋鼠云成功举办了以“AI驱动&#xff0c;数智未来”为主题的2024年秋季发布会。大会深度探讨了如何凭借 AI 实现新的飞跃&#xff0c;重塑企业的经营管理方式&#xff0c;加速数智化进程。 作为大会的重要环节之一&#xff0c;袋鼠云数栈产品经理潮汐带来了…

goframe开发一个企业网站 前端界面 拆分界面7

将页面拆出几个公用部分 在resource/template/front创建meta.html header.html footer.html meta.html <head><meta charset"utf-8"><meta content"widthdevice-width, initial-scale1.0" name"viewport"><title>{{.…

SpringBoot3集成Junit5

目录 1. 确保项目中包含相关依赖2. 配置JUnit 53. 编写测试类4、Junit5 新增特性4.1 注解4.2 断言4.3 嵌套测试4.4 总结 在Spring Boot 3中集成JUnit 5的步骤相对简单。以下是你可以按照的步骤&#xff1a; 1. 确保项目中包含相关依赖 首先&#xff0c;确保你的pom.xml文件中…

[数据结构从小白到大牛]第五篇:3分钟带你吃透双链表并用C语言模拟实现

目录 1->前言 2->链表的概念和结构 2.1链表概念 2.2->带头双向循环链表结构 3->模拟实现带头双向循环链表 3.1定义链表结点 struct ListNode 3.2创建链表结点 CreateLTNode 函数 3.3链表初始化函数 ListInit函数 3.4链表打印函数 ListPrint函数 3.5链表…

前端通过nginx部署一个本地服务的方法

前端通过nginx部署一个本地服务的方法&#xff1a; 1.下载ngnix nginx 下载完成后解压缩后运行nginx.exe文件 2.打包你的前端项目文件 yarn build 把生成的dist文件复制出来&#xff0c;替换到nginx的html文件下 3.配置conf目录的nginx.conf文件 主要配置server监听 ser…

不同的浮点数类型

不同的浮点数类型 尽管4字节的浮点数可表达相当大的数值&#xff0c;但对于人类而言&#xff0c;总不够用。一般而言&#xff0c;浮点数有3种类型&#xff0c;单精度的float和双精度的double以及更长的long double, 可参考&#xff1a;数据类型大小 不同语言的浮点数类型 C/Obj…