【C++多态】

news2025/1/17 2:59:40

目录

  • 1.多态的概念
  • 2.多态的定义及实现
    • 2.1多态的构成条件
    • 2.2虚函数的一些细节
    • 2.3析构函数可以是虚函数吗?
    • 2.4 重载、覆盖(重写)、隐藏(重定义)的对比
  • 3.抽象类
  • 4.多态的原理
    • 4.1虚函数表
    • 4.2虚函数地址的打印
    • 4.3多继承的函数虚表

1.多态的概念

什么是多态?
这种思想我们在很早的时候就已经接触过。如:水就有三种形态(固态、液态、气态)由于条件的变化三种形态可以互相转换,而每一种形态都有自己的特定的属性。
在比如:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

2.多态的定义及实现

2.1多态的构成条件

构成多态的两个条件:
1.必须使用父类的指针或者引用调用虚函数
2.被调用的必须是虚函数,且子类必须对父类的虚函数进行重写
如:

#include<iostream>
using namespace std;
class Water
{
public:
	virtual void func()
	{
		cout << "液态的水" << endl;
	}
};
class Ice : public Water
{
public:
	virtual void func()
	{
		cout << "冰" << endl;
	}
};
class Vapor : public Water
{
public:
	virtual void func()
	{
		cout << "水蒸气" << endl;
	}
};
void test(Water& w)
{
	w.func();
}
int main()
{
	Water w;
	Ice i;
	Vapor v;
	test(w);
	test(i);
	test(v);
	return 0;
}

在这里插入图片描述

2.2虚函数的一些细节

1.子类的虚函数前可以不用加virtual关键字
2.虚函数的重写:即子类虚函数与类虚函数的返回值类型、函数名字、参数列表完全相同,称子类的虚函数重写了父类的虚函数。
3.重写的条件本来是虚函数+三同,但是有些例外。如协变。
协变:返回值可以不同,但必须是父子关系的指针或者引用。如:

#include<iostream>
using namespace std;
class Water
{
public:
	virtual Water* func()
	{
		cout << "液态的水" << endl;
		return 0;
	}
};
class Ice : public Water
{
public:
	virtual Ice* func()
	{
		cout << "冰" << endl;
		return 0;
	}
};
class Vapor : public Water
{
public:
	virtual Vapor* func()
	{
		cout << "水蒸气" << endl;
		return 0;
	}
};
void test(Water& w)
{
	w.func();
}
int main()
{
	Water w;
	Ice i;
	Vapor v;
	test(w);
	test(i);
	test(v);

	return 0;
}

2.3析构函数可以是虚函数吗?

问题1:析构函数前加virtual 是虚函数吗?
答:是的。在C++中析构函数都被处理成destructor这个统一的名字。所以满足三同。
问题2:为什么要这样处理呢?
答:因为想让他们构成重写。
问题3:为什么要让他们构成重写呢?
答:因为下面的场景如果不构成重写会造成内存泄漏问题。如:

class person
{
public:
	//virtual ~person()
	~person()
	{
		cout << "~person()" << endl;
	}
};
class student : public person
{
public:
	//virtual ~student()
	~student()
	{
		delete[] arr;
		cout << "~student()" << endl;
	}
private:
	int* arr = new int[10];
};
int main()
{
	person* p;
	p = new student();
	delete p;//p->destructor() + operator delete(p)
	return 0;
}

在这里插入图片描述

2.4 重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

3.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void func() = 0;
};

class Benz : public Car
{
public:
	virtual void func()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW : public Car
{
public:
	virtual void func()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	Car* pbenz = new Benz();
	Car* pbmw = new BMW();
	pbenz->func();
	pbmw->func();
	return 0;
}

4.多态的原理

4.1虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};

上面的代码在x86平台下的答案是8。为什么是8呢?可以通过监视窗口来观察一下它的存储。在这里插入图片描述
在这里插入图片描述
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析。
在这里插入图片描述
通过上面的监视窗口可以观察到:
1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
在这里插入图片描述
5… 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6.虚函数表存放的是虚函数的指针,虚函数与普通函数一样存放在代码段,那么虚函数表存放在哪里?是栈?堆?静态区?常量区?可以验证一下。
1.首先验证所有类型的对象都共用一个虚表。如:
在这里插入图片描述
2.验证虚表的存储位置。如:

int main()
{
	int a = 0;
	printf("栈区:%p\n", &a);

	int* p = new int;
	printf("堆区:%p\n", p);

	static int b = 10;
	printf("静态区:%p\n", &b);

	const char* str = "hello tt";
	printf("常量区:%p\n", str);

	Base be;
	printf("虚表:%p\n", *((int*)&be));
	return 0;
}

在这里插入图片描述
可以看出虚表是在常量区。

4.2虚函数地址的打印

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}

private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _d = 2;
};
typedef void (*FUNC) ();
void PrintVFT(FUNC* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p\n", i, table[i]);
	}
	printf("\n");

}
int main()
{
	/*int a = 0;
	printf("栈区:%p\n", &a);

	int* p = new int;
	printf("堆区:%p\n", p);

	static int b = 10;
	printf("静态区:%p\n", &b);

	const char* str = "hello tt";
	printf("常量区:%p\n", str);

	Base be;
	printf("虚表:%p\n", *((int*)&be));*/

	Base b;
	Derive d;
	int vft1 = *((int*)&b);
	int vft2 = *((int*)&d);
	PrintVFT((FUNC*)vft1);
	PrintVFT((FUNC*)vft2);
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3多继承的函数虚表

#include<iostream>
using namespace std;
class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2" << endl;
	}
};
class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}
};
class Derive :public Base1, public Base2
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}
	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
};
typedef void (*FUNC_VFT) ();
void PrintVFT(FUNC_VFT* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%zd]:%p\n", i, table[i]);
		table[i]();
	}
	printf("\n");
}
int main()
{
	Derive d;
	Base1 b1;
	//Base2 b2;
	Base2* b2=&d;
	int vft1 = *((int*)&d);
	int vft2 = *((int*)b2);
	PrintVFT((FUNC_VFT*)vft1);
	PrintVFT((FUNC_VFT*)vft2);
	return 0;
}

在这里插入图片描述

注意点:
1.子类的func3的虚函数地址放在从第一个父类继承过来的虚函数表中(可能跟不同的编译器有关)
2.子类func1看似在两个父类的虚函数表中的存放的地址不一样,实际只有一个地址且调用的同一个函数(原因感兴趣的可以观察汇编)

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

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

相关文章

FLV 封装格式解析

1、FLV 简介 FLV(Flash Video) 是 Adobe 公司推出的一种流媒体格式&#xff0c;由于其封装后的音视频文件体积小、封装简单等特点&#xff0c;非常适合于互联网上使用。目前主流的视频网站基本都支持FLV。采用 FLV 格式封装的文件后缀为.flv。直播场景下拉流比较常见的是 http…

git学习笔记(重实践) | 版本管理 - 分支管理 - 常见场景

文章目录 git学习笔记Git是什么仓库常见的命令commit 备注规范在文件下设置git忽略文件 .gitignore 版本管理git log | git reflog 查看提交日志/历史版本版本穿梭 git resetgit reset HEAD <file> git checkout -- fileName 丢弃工作区的修改git revertGit恢复之前版本的…

成功项目风险预防可控的5个重点

成功的项目往往重视项目风险的预防和管控&#xff0c;这样有利于可能风险的及时控制和解决&#xff0c;将其不利影响降到最小。如果不重视对风险的预防和管控&#xff0c;不及时发现和处理项目风险&#xff0c;那么项目风险往往会为我们带来意想不到的不利后果&#xff0c;往往…

【LeetCode-中等题】146. LRU 缓存

文章目录 题目方法一&#xff1a;直接继承LinkedHashMap调用api方法二&#xff1a;自定义LinkedHashMap HashMap ListNode LinkedHashMap 题目 LRU缓存是什么&#xff1a;LRU缓存机制&#xff0c;你想知道的这里都有 实现 LRU 缓存算法 方法一&#xff1a;直接继承Linked…

深度图相关评测网站

文章目录 1 单目/Stereo相关测评网站介绍12 单目/Stereo相关测评网站介绍23 单目/Stereo相关测评网站介绍3 1 单目/Stereo相关测评网站介绍1 https://vision.middlebury.edu/stereo/eval3/ 2 单目/Stereo相关测评网站介绍2 http://www.cvlibs.net/datasets/kitti/eval_stereo…

Vscode画流程图

1.下载插件 Draw.id Integration 2.桌面新建文件&#xff0c;后缀名改为XXX.drawio 在vscode打开此文件 &#xff0c;就可以进行绘制流程图啦

无涯教程-Android - Broadcast Receivers

Broadcast Receivers 仅响应来自其他应用程序或系统本身的广播消息&#xff0c;这些消息有时称为events或intents。例如&#xff0c;应用程序还可以启动广播&#xff0c;以使其他应用程序知道某些数据已下载到设备并可供他们使用&#xff0c;因此广播接收器将拦截此通信并启动适…

做区块链卡牌游戏有什么好处?

区块链卡牌游戏是一种基于区块链技术的创新性游戏形式&#xff0c;它将传统的卡牌游戏与区块链技术相结合&#xff0c;实现了去中心化、数字化资产的交易和收集。这种新型游戏形式正逐渐在游戏行业引起了广泛的关注和热潮。本文将深入探讨区块链卡牌游戏的定义、特点以及其在未…

自定义node-red节点中,如何编写节点的配置信息弹窗

前言 最近有读者通过博客向我咨询,在自定义node-red节点时,如何编写该节点的配置页面,就是我们通常见到的,双节节点打开的信息弹窗。如下图: 上面两张图,展示了inject节点与mqtt in 节点的配置弹窗。 在弹窗中,除了上面的删除,取消,完成,和下面的失效按钮。 中间…

以“迅”防“汛”!5G视频快线筑牢防汛“安全堤”

近期&#xff0c;西安多地突发山洪泥石流灾害。防洪救灾刻不容缓&#xff0c;为进一步做好防汛工作&#xff0c;加强防洪调度监管&#xff0c;切实保障群众的生命财产安全&#xff0c;当地政府管理部门亟需拓展智能化技术&#xff0c;通过人防技防双保障提升防灾救灾应急处置能…

模型的保存加载、模型微调、GPU使用及Pytorch常见报错

序列化与反序列化 序列化就是说内存中的某一个对象保存到硬盘当中&#xff0c;以二进制序列的形式存储下来&#xff0c;这就是一个序列化的过程。 而反序列化&#xff0c;就是将硬盘中存储的二进制的数&#xff0c;反序列化到内存当中&#xff0c;得到一个相应的对象&#xff…

深度学习论文: Segment Any Anomaly without Training via Hybrid Prompt Regularization

深度学习论文: Segment Any Anomaly without Training via Hybrid Prompt Regularization Segment Any Anomaly without Training via Hybrid Prompt Regularization PDF: https://arxiv.org/pdf/2305.10724.pdf PyTorch代码: https://github.com/shanglianlm0525/CvPytorch Py…

【计算机系统概论Yale.patt】第二章

文章目录 2.数据的表示与计算2.1 bit2.1.1 信号的编码表示2.1.2 计算机采用二进制的原因2.1.3 数据类型无符号整数有符号整数原码反码补码编码方式与范围移码4-bit的不同编码方式 2.1.4 IEEE754浮点数尾数指数0000 0000 含义1111 1111含义 例 2.2 进制转换2.2.1 二转十2.2.2 十…

python面试:使用cProfile剖析程序性能

我们需要安装tuna&#xff1a;pip install tuna 程序执行完毕后&#xff0c;我们会得到一个results.prof&#xff0c;在CMD中输入指令&#xff1a;“tuna results.prof”。 import time import cProfile import pstatsdef add(x, y):resulting_sum 0resulting_sum xresulti…

mysql数据表Table is marked as crashed and should be repaired 的解决办法

错误原因 网上查了一下&#xff0c;错误的产生原因&#xff0c;有网友说是频繁查询和更新XXXX表造成的索引错误&#xff0c;还有说法是Mysql数据库因某种原因而受到了损坏。 【如&#xff1a;数据库服务器突发性断电&#xff0c;在数据表提供服务时对表的源文件进行某种操作都…

Java程序生成可执行exe文件及可安装程序

Java程序生成可执行exe文件及可安装程序 文章目录 Java程序生成可执行exe文件及可安装程序整体流程Maven项目打成jar包打成可执行文件准备工作&#xff1a;exe4j的下载、安装与激活使用exe4j打包 打成可安装文件参考 整体流程 将项目打包成可正常运行的jar包&#xff08;使用命…

涉及结构体的排序问题

简单举一个例子来介绍涉及结构体的排序问题。 例&#xff1a;输入若干学生姓名、语文成绩、数学成绩、英语成绩&#xff0c;根据三科成绩总分由高到低进行排序。 输入数据&#xff1a; 小明 78 89 90 小红 87 88 77 小华 91 92 96 输出样例&#xff1a; 小华 91 92 96 279 小明…

通义千问本地化部署不调用GPU只调用CPU的检查方法

今天部署本地版通义千问的时候遇到一个问题。 启动他的cli_demo.py调用的一直都是CPU模式的。 检查cuda已经正确安装&#xff0c;后面发现是torch即PyTorch的安装问题。 我安装torch的时候&#xff0c;用的是默认指令&#xff0c;没有增加别的参数。 检测一下&#xff0c;输出…

正中优配:创业板指大涨3.47%!减速器等概念板块掀涨停潮!

周二&#xff08;8月29日)&#xff0c;三大股指团体涨超1%。截至上午收盘&#xff0c;上证指数涨1.39%&#xff0c;报3141.82点&#xff1b;深证成指和创业板指别离涨2.41%和3.47%&#xff1b;沪深两市算计成交额6264.51亿元&#xff0c;总体来看&#xff0c;两市个股涨多跌少&…

教会你怎么玩转 文件下载

&#x1f600;前言 教会你怎么玩转 文件下载 &#x1f3e0;个人主页&#xff1a;尘觉主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是尘觉&#xff0c;希望我的文章可以帮助到大家&#xff0c;您的满意是我的动力&#x1f609;&#x1f609; 在csdn获奖荣誉: …