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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

nginx-proxy-manager实现反向代理+自动化证书(实战)

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 &#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 cnginx-proxy-manager实现反向代理自动化证书 nginx-proxy-manager是什么搭建nginx-proxy-manage…

人才画像系统:助力企业打造动态人才成长体系

在当今竞争激烈的市场环境中&#xff0c;人才已成为企业发展的核心竞争力。为了满足企业发展对人才的需求&#xff0c;人才画像系统应运而生&#xff0c;通过以岗位胜任力模型为基础定义人才标准&#xff0c;多维度采集员工信息进行人才对标和盘点&#xff0c;为企业的人才选拔…

【Hadoop和Hbase集群配置】3台虚拟机、jdk+hadoop+hbase下载和安装、环境配置和集群测试

目录 一、环境 二、虚拟机配置 三、 JDK、Hadoop、HBase的安装和配置 【安装和配置JDK】 【安装和配置Hadoop】 【安装和配置Hbase】 四、 Hadoop和HBase集群测试 【Hadoop启动测试】 【Hbase启动测试】 一、环境 OS: CentOS-7 JDK: v1.8.0_131 Hadoop: v2.7.6 Hb…

制作一个3D建模只需10秒:腾讯发布3D开源模型“混元3D”

混元 3D 模型 腾讯在科技领域投下一颗重磅炸弹&#xff0c;宣布推出混元 3D 生成大模型 “hunyuan3d - 1.0”&#xff0c;这是业界首个同时支持文字、图像生成 3D 的开源模型。它具有生成速度快、泛化能力强、可控性好等特点&#xff0c;直接引起了 AI 界众人的关注。 混元3D-1…

情怀系列国际版棋牌完整源码具备强大的多语言扩展功能,涵盖了900多款子游戏,专为全球市场的游戏开发和运营设计。

情怀棋牌源代码的服务器端使用JAVA和Node.js开发&#xff0c;采用RocketMQ作为消息队列中间件&#xff0c;有效防止服务器堵塞、消峰。数据库使用MySQL&#xff0c;媒体存储采用MongoDB&#xff0c;缓存系统使用Redis。管理后台则采用PHP语言开发。 客户端使用Cocos Creator进…

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文件中…

Google Guava 发布订阅模式/生产消费者模式 使用详情

目录 Guava 介绍 应用场景举例 1. 引入 Maven 依赖 2. 自定义 Event 事件类 3. 定义 EventListener 事件订阅者 4. 定义 EventBus 事件总线 5. 定义 Controller 进行测试 Guava 介绍 Guava 是一组来自 Google 的核心 Java 库&#xff0c;里面包括新的集合 类型&#xff08…

Idea如何推送项目到gitee

第一步&#xff1a;先在你的gitee创建一个仓库 第二步&#xff1a; 点击推送 点击定义远程&#xff0c;将URL换成你仓库的&#xff0c;填好你的用户名和密码 可以看到已经推送到仓库了

gdb和make工具

gdb工具&#xff1a; GDB的主要功能 断点设置&#xff1a;允许开发者在特定的代码行设置断点&#xff0c;当程序执行到该行时会自动暂停&#xff0c;方便开发者进行调试和分析。 变量查看与修改&#xff1a;在程序运行过程中&#xff0c;可以查看和修改变量的值&#xff0c;以…

一周内从0到1开发一款 AR眼镜 相机应用?

目录 1. &#x1f4c2; 前言 2. &#x1f4a0; 任务拆分 2.1 产品需求拆分 2.2 开发工作拆分 3. &#x1f531; 开发实现 3.1 代码目录截图 3.2 app 模块 3.3 middleware 模块 3.4 portal 模块 4. ⚛️ 拍照与录像 4.1 前滑后滑统一处理 4.2 初始化 View 以及 Came…

推荐一款功能强大的数据库开发管理工具:SQLite Expert Pro

SQLite Expert Professional是一个功能强大的工具&#xff0c;旨在简化SQLite3数据库的开发。 它是SQLite的一个功能丰富的管理和开发工具&#xff0c;旨在满足所有用户从编写简单SQL查询到开发复杂数据库的需求。 图形界面支持所有SQLite功能。 它包括一个可视化查询构建器&a…

C#与C++交互开发系列(十七):线程安全

前言 在跨平台开发和多线程编程中&#xff0c;线程安全是不可忽视的重要因素。C和C#中提供了各自的线程同步机制&#xff0c;但在跨语言调用中&#xff0c;如何确保数据一致性、避免数据竞争和死锁等问题&#xff0c;是开发人员必须考虑的重点。 本文将介绍在C#和C交互开发中确…

数据库SQL学习笔记

第 1 章 绪论 1.1 数据库系统概述 1.1.1 四个基本概念 数据库系统(DBS) 定义&#xff1a;是指在计算机系统中引入数据库后的系统构成 构成&#xff1a;数据库&#xff0c;数据库管理系统&#xff08;及其开发工具&#xff09;&#xff0c;应用系统&#xff0c;数据库管理员…

Java项目实战II基于Spring Boot的智慧生活商城系统的设计与实现(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 一、前言 随着科技的飞速发展&#xff0c;人们的…

.net Core 使用Panda.DynamicWebApi动态构造路由

我们以前是通过创建controller来创建API&#xff0c;通过controller来显示的生成路由&#xff0c;这里我们讲解下如何不通过controller&#xff0c;构造API路由 安装 Panda.DynamicWebApi 1.2.2 1.2.2 Swashbuckle.AspNetCore 6.2.3 6.2.3添加ServiceAction…

[ 内网渗透实战篇-1 ] 单域环境搭建与安装域环境判断域控定位CS插件装载CS上线

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

还在使用ElementUI不如试一试DaisyUI,DaisyUI: Tailwind CSS 的高效组件库,

DaisyUI: Tailwind CSS 的高效组件库 daisyUI官网&#xff1a;https://daisyui.com/ 在现代网页开发中&#xff0c;快速构建美观且响应式的用户界面是每个开发者追求的目标。Tailwind CSS 是一个流行的实用程序优先的 CSS 框架&#xff0c;它允许开发者直接在 HTML 中使用预…

《大数据与人工智能:提升数据质量与数量的利器》

《大数据与人工智能&#xff1a;提升数据质量与数量的利器》 一、大数据与人工智能的融合趋势二、大数据增加数据数量的方法&#xff08;一&#xff09;不同途径的数据增量&#xff08;二&#xff09;数据增强的多样方法 三、人工智能提升数据数量的手段&#xff08;一&#xf…

通义灵码实操—飞机大战游戏

通义灵码实操—飞机大战游戏 有没有想象过自己独立编写一个有趣的小游戏。在本实践课程中&#xff0c;你不仅可以实现这个想法&#xff0c;而且还将得到通义灵码智能编程助手的支持与指导。我们将携手步入编程的神奇世界&#xff0c;以一种简洁、高效且具有创造性的方式&#…