【C++第十六章】多态

news2024/9/22 15:27:47

【C++第十六章】多态

定义🧐

  多态通俗来说,就是多种形态,我们去完成某个行为时,不同对象完成时会产生出不同的状态。比如游乐园中,1.2米以上买票就需要买成人票,1.2米以下就可以买儿童票。

  多态是在不同继承关系的类对象,去调用同一函数,产生不同行为。我们通过下面代码来学习:

#include <iostream>
using namespace std;

class Person {
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-全价" << endl; 
	}
};
class Student : public Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
	}
};
//多态条件
//1.虚函数重写
//2.父类的指针或者引用去调用虚函数

//虚函数重写要求
//父子继承关系的两个虚函数,要求同函数名、参数、返回

//virtual只能修饰成员
//三同的例外:协变->返回类型可以不同,但必须是父子类关系的指针或者引用
//派生类重写的虚函数可以不加virtual
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);
	
	return 0;
}

  student类继承了person类,并拥有同一函数BuyTicket,此时我们使用**父类的指针或者引用去调用该函数**就可以形成多态。

  总结一下,多态形成的条件为:1.虚函数的重写(由于一些特性,子类虚重写的虚函数可以不加virtual) 2.需要父类指针或者引用去调用该函数。而虚函数重写需要满足三同——函数名、参数、返回类型(除协变)都要相同。普通函数的继承是实现继承,虚函数的继承是接口继承

Pasted image 20240814120002

协变🔎

  协变是三同的例外,协变的返回类型可以不同,但必须是父子类关系的指针或者引用

Pasted image 20240813205432

子类virtual🔎

  子类重写的虚函数可以不加virtual,但父类必须加上virtual

Pasted image 20240813205612

  原因在于,父类指针只会调用父类析构,但是我们可能将父类指针指向子类,而delete由两部分构成——destructor(析构的统一处理,继承章节提到过)和operator delete,析构函数的名称由于多态被统一处理了,所以delete时会先调用析构再调用operator delete,在该代码中,我们想要p指向谁就调用谁的析构,此时就需要用到虚函数,而person和student满足父子关系,也有统一函数名destructor,此时只缺少virtual关键字,所以我们在析构加上virtual就可以变为多态,实现指向谁析构谁。

  不过在设计时,为了不让我们忘记给子类加上virtual而导致内存泄漏,所以统一设计,即使子类不写virtual也可以重写。

#include <iostream>
using namespace std;

class Person {
public:
	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person* p = new Person;
	delete p;

	p = new Student;
	delete p;

	return 0;
}

Pasted image 20240813214552

Pasted image 20240813214616

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

Pasted image 20240814125718

小试牛刀🔎

Pasted image 20240814124514

  B类创建了一个指针,该指针指向test函数,而test是A类的成员,所以test的参数为A* this,内部为this->fucn(),而this是B,B与A是父子关系,满足虚函数重写,所以是多态调用,但虚函数重写是父类的实现,用的是父类的接口,所以val还是父类的值,则选B

final和override🧐

  final用于修饰虚函数,被修饰的虚函数不能被重写

Pasted image 20240814130212

  override修饰派生类的虚函数,可以检查是否完成重写,没有重写则会报错。

Pasted image 20240814130450

抽象类🧐

  在虚函数后面加上"=0",则这个函数为纯虚函数,含有纯虚函数的类叫做抽象类(接口类),抽象类不能实例出对象派生类继承后必须要重写纯虚函数,才能实例化对象。

  纯虚函数规范了抽象类必须要重写,也体现出接口继承。抽象类一般用于不需要实例化的对象,比如person类,我们没有赋予属性前这个类就可以看做是抽象的,当我们继承person类后,重写它的各种属性(身高年龄职业等),让其变为一个具体的对象再进行实例化。

Pasted image 20240814132812

虚函数表🧐

  C++会把虚函数存到虚函数表(_vfptr)中,所以会多开一个指针指向该表(本篇博客代码在32位环境下运行),虚函数编译后也存在代码段中,只不过会把虚函数的地址单独拿出来放在表中。

Pasted image 20240814174804

Pasted image 20240814175304

  虚函数的重写,也叫虚函数的覆盖,虚函数表也会进行覆盖。所以当我们传子类对象时,实际上是父类对子类的切片,通过子类虚函数表找到虚函数地址。

#include <iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func(){}

private:
	int a = 0;
};

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

private:
	int b = 1;
};

void Func(Person ptr)
{
	ptr.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	
	Func(p);
	Func(s);

	return 0;
}

Pasted image 20240814211250

  那么为什么可以用指针和引用,而不能用对象呢?首先引用的底层也是指针,所以它俩都是能指向子类对象中切割出来的父类。用对象也是子类切割出来的父类,成员拷贝给父类,但是不会拷贝虚函数表的指针,原因在于当出现父类=子类的情况时,不一定能调用到父类的虚函数。

Pasted image 20240814205451

  如果父类写了虚函数,子类没写虚函数,虚函数表的内容一样,但是存储在不同的位置,因为多开一个虚函数表不会耗费太多资源,从安全性考虑新开一个表更为保险。

Pasted image 20240814210906

  同一个类可以共用一张虚表。

Pasted image 20240814210537

  我们将虚表地址打印出来,发现虚表更靠近代码段,所以我们认为虚表是存在代码段中的。

Pasted image 20240816134602

  并且,所有的虚函数一定会被放进类的虚函数表中,我们以下面代码来说明:

#include <iostream>
using namespace std;

class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func1" << endl;
	}
private:
	int _a;
};

class Derive : public Base
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}
	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
	virtual void func4()
	{
		cout << "Derive::func4" << endl;
	}
	void func5()
	{
		cout << "Derive::func5" << endl;
	}
private:
	int _b;
};
class X : public Derive
{
public:
	virtual void func3()
	{
		cout << "X::func3" << endl;
	}
};
//打印虚函数
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		(*f)();
	}
	printf("\n");
}

int main()
{
	Base b;
	PrintVFT((VFUNC*)(*((int*)&b)));

	Derive d;
	PrintVFT((VFUNC*)(*((int*)&d)));

	X x;
	PrintVFT((VFUNC*)(*((int*)&x)));
	return 0;
}

  我们用监视窗口发现只有两个虚函数。

Pasted image 20240816144909

  但实际上用函数指针数组打印出虚函数地址,发现实际有四个,不过监视窗口给我们隐藏了。

Pasted image 20240816144939

  我们打印出虚函数地址的原理为,先取到Derive对象的地址,然后强转成int*只取前4个字节,也就是取到虚函数表,解引用拿到虚函数的地址,最后强转一下传过去。

Pasted image 20240816150251

  在多继承下,会有多个虚表存在。

#include <iostream>
using namespace std;

class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2" << endl;
	}
private:
	int _a1;
};

class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}
private:
	int _a2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}

	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
	
private:
	int _b;
};
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		(*f)();
	}
	printf("\n");
}

int main()
{
	Derive d;
	PrintVFT((VFUNC*)(*((int*)&d)));

	Base2* ptr = &d; //自动切片
	PrintVFT((VFUNC*)*(int*)ptr);

	return 0;
}

Pasted image 20240817142309

  我们打印发现,重写的两个fun1地址不一样。

Pasted image 20240817160641

  我们用下面代码讲解:

#include <iostream>
using namespace std;

class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2" << endl;
	}
private:
	int _a1;
};

class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}
private:
	int _a2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}

	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
	
private:
	int _b;
};
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		(*f)();
	}
	printf("\n");
}

int main()
{
    Derive d;
	Base1* p1 = &d;
	p1->func1();

	Base1* p2 = &d;
	p2->func1();
    return 0;
}

  我们从p1的汇编指令来看,首先p1所call的不是真正的地址,而是call到jmp,再由jmp跳到真正的地址,开始建立fun1的栈帧,而p2发现call和jmp以及func1的地址也不一样,并且会进行多段跳。

Pasted image 20240817153059

Pasted image 20240817153107

  原因在于func1所接受的是Derive* this,而this应该能够访问整个对象,所以我们需要修正p2让它指向Derive,如图2的ecx-8就是在修正p2,让其指向Derive对象,而p1恰好与this重叠,所以没有多段跳

结尾👍

  以上便是多态的全部内容,如果有疑问或者建议都可以私信笔者交流,大家互相学习,互相进步!🌹

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

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

相关文章

Spring之整合Mybatis底层源码解析

整合核心思路 由很多框架都需要和Spring进行整合&#xff0c;而整合的核心思想就是把其他框架所产生的对象放到Spring容器中&#xff0c;让其成为Bean。 ​ 比如Mybatis&#xff0c;Mybatis框架可以单独使用&#xff0c;而单独使用Mybatis框架就需要用到Mybatis所提供的一些类…

ctfshow之文件包含(web78~web86)

web78 if(isset($_GET[file])){$file $_GET[file];include($file); }else{highlight_file(__FILE__); } 解法一&#xff1a;data伪协议 ?filedata://text/plain,<?php system("tac flag.php"); 解法二&#xff1a;php:filter伪协议 ?filepHp://FilTer/conver…

数理天地杂志数理天地杂志社数理天地编辑部2024年第12期目录

基础精讲 “瓜豆模型”与动点问题探析 明倩妤; 2-3 二次函数参数取值范围常见题型分析 邹纯; 4-5 基于数形结合思想的初中数学解题实践——以初中函数问题为例 李玉平; 6-7 旋转思想在构造全等三角形中的渗透 赵兴燕; 8-9 初中数学“一元一次方程”试题设计…

作业0903

1.封装栈 #include <iostream>using namespace std;class myStack { private:int size; // 大小int capacity;int *ptr;int top; // 栈顶下标 public:// 无参构造函数myStack():size(0), top(-1), capacity(10) {ptr new int[capacity];}// 有参构造函数myStack(in…

【数据结构】你真的了解栈和队列吗?

文章目录 1. 栈1.1 概念与结构1.2 栈的实现 2. 队列2.1 概念与结构2.2 队列的实现 3. 栈和队列的算法题3.1 有效的括号3.2 用队列实现栈3.3用栈实现队列3.4 设计循环队列 4. 结语 1. 栈 1.1 概念与结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插…

.hackme靶机通关攻略

第一步查找ip 通过御剑扫描到IP进入尝试 成功找到靶场 步骤二来到这个靶场注册登录 进来点一下提交出来书名 只有一个框框那就来试试sql注入 1’ and 12 -- 然后查看数据库 -1 union select database(),2,3 # 查看数据库表名 -1 union select group_concat(table_name),2…

Java面试题精选:消息队列(三)

1、RabbitMQ中的vhost起什么作用&#xff1f; &#xff08;1&#xff09;vhost本质上就是一个mini版的mq服务器&#xff08;虚拟消息服务器&#xff09;。 有自己的队列、交换器和绑定&#xff0c;最重要的&#xff0c;拥有独立的权限机制&#xff0c;可以做到vhost范围的用户…

VoxEdit 比赛|创作农场灵感资产

为大家带来另一场 VoxEdit 迷你比赛&#xff01;在一周时间内创建可用资产&#xff01; 主题&#xff1a;踏入农场世界&#xff0c;使用 VoxEdit 创建农场主题资产。 从古朴的农舍到可爱的鸡舍&#xff0c;甚至是充满胡萝卜的农田。 你将专注于捕捉农场生活的精髓&#xf…

计算机毕业设计Spark+PyTorch知识图谱中药推荐系统 中药数据分析可视化大屏 中药爬虫 机器学习 中药预测系统 中药情感分析 大数据毕业设计

创新点&#xff1a;知识图谱、大数据虚拟机、可视化大屏、python爬虫、lstm深度学习情感分析、sparkhadoophive离线计算实时计算全部实现、前后台完整、支付宝沙箱支付、短信、AI识别、4-20种推荐算法、中药价格机器学习预测算法等上百种创新点 开发技术&#xff1a;spark hiv…

如何从 Bak 文件中恢复 SQL数据库?(3种方法)

如何从 .bak 文件恢复 SQL数据库&#xff1f; 在数据库管理和维护过程中&#xff0c;数据的安全性和完整性至关重要。备份文件&#xff08;.bak 文件&#xff09;是 SQL Server 中常用的数据库备份格式&#xff0c;它包含了数据库的完整副本&#xff0c;用于在数据丢失、系统故…

在线思维导图怎么快速绘制?5个软件帮助你绘制思维导图不求人

在线思维导图怎么快速绘制&#xff1f;5个软件帮助你绘制思维导图不求人 在线绘制思维导图是整理思路、计划项目、或者学习新知识的绝佳方式。通过使用专门的软件和工具&#xff0c;你可以快速、便捷地创建出清晰的思维导图。以下是五款在线思维导图工具&#xff0c;可以帮助你…

惠中科技PV-Wiper全自动光伏组件清洁系统:智能清洁赋能光伏产业

在全球绿色能源转型的浪潮中&#xff0c;光伏产业作为清洁能源的重要支柱&#xff0c;正以前所未有的速度发展。然而&#xff0c;光伏组件的清洁维护问题一直是制约其发电效率与使用寿命的关键因素。针对这一挑战&#xff0c;惠中科技凭借其深厚的行业积累和技术创新实力&#…

C++基础多态

目录 学习内容&#xff1a; 1. 多态 1.1 多态的实现 1.2 函数重写&#xff08;override&#xff09; 1.3 虚函数 1.4 使用多态实现的实例 1.5 虚函数的底层实现 1.6 重载&#xff08;voerload&#xff09;、重写&#xff08;override&#xff09;和隐藏&#xff08;h…

数据结构,单向链表

数据结构是计算机科学中的一个核心概念&#xff0c;它研究的是数据的组织、管理和存储方式&#xff0c;以及在这些数据上进行操作时的算法。数据结构为数据的高效访问和修改提供了基础。 数组&#xff08;Array&#xff09;&#xff1a;一种线性数据结构&#xff0c;可以存储固…

kubeadm方式安装k8s

⼀、安装环境 1. 安装说明 本次以⼆进制⽅式安装⾼可⽤ k8s 1.28.0 版本&#xff0c;但在⽣产环境中&#xff0c;建议使⽤⼩版本⼤于 5 的 Kubernetes 版本&#xff0c;⽐如 1.19.5 以后。 2. 系统环境 3. ⽹络及版本环境 注&#xff1a;宿主机⽹段、Pod ⽹段、Service ⽹段…

项目管理利器:2024五款精选项目管理软件深度解析

在项目管理这个纷繁复杂的领域中摸爬滚打二十年&#xff0c;我见证了无数项目从策划到实施&#xff0c;再到成功交付的全过程。在这个过程中&#xff0c;项目管理软件如同导航灯塔&#xff0c;为团队指明了方向&#xff0c;提高了效率&#xff0c;降低了风险。今天&#xff0c;…

Spring声明式事务使用详情(知识点+案例)

目录 1、声明式事务的概念 2、Spring事务管理器 3、基于注解的Spring事务 4、Spring事务属性 4.1只读&#xff08;Read-Only&#xff09; 4.2事务超时&#xff08;Timeout&#xff09; 4.3事务异常回滚&#xff08;rollbackFor&#xff09; 4.4事务隔离级别&#xff08…

如何用Java SpringBoot打造助农捐赠平台?2025年25届毕业生必看+最新设计实现攻略!

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

【深度学习与NLP】——词嵌入Embedding技术

目录 1.词嵌入的作用 2.嵌入矩阵的计算 3.Embedding层的代码实验 词嵌入&#xff08;Embedding&#xff09;技术是一种将词汇映射到低维连续向量空间的方法。将离散的单词数据处理成连续且固定长度的向量&#xff0c;使模型可以学习和处理语义信息。 假设需要将["Are&…

终于有人把数据中台讲明白了

在大数据发展的黄金期&#xff0c;几乎所有的高科技企业都在思考一个问题&#xff1a;海量数据作为大多数企业发展不可避免的一个趋势之后&#xff0c;企业该怎么去应用这部分数据资产&#xff0c;会对其商业产生什么影响&#xff0c;如何使数据对企业产生正面的推动而不是成为…