【C++高阶】掌握C++多态:探索代码的动态之美

news2024/12/24 3:15:19

📝个人主页🌹:Eternity._
⏩收录专栏⏪:C++ “ 登神长阶 ”
🤡往期回顾🤡:C++继承
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

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

❀继承

  • 📒1. 多态的定义及实现
    • 🍁多态的构成条件
    • 🍂虚函数的重写
    • ⛰️override 和 final
    • 🌄重载、覆盖、隐藏
  • 📕2. 抽象类
    • 🎩抽象类概念
    • 🎈接口继承和实现继承
  • 📜3. 多态的原理
    • 🌈虚函数表
    • 🌞虚函数表的特征
    • 🌙验证虚函数表的存放位置
    • ⭐多态的原理
  • 📚4. 虚函数表
    • 🧩单继承中的虚函数表
      • 💧打印虚函数表
    • 🧩多继承中的虚函数表
      • 🔥虚函数的调用
  • 📖5. 总结


前言: 在编程的广阔领域中,多态(Polymorphism) 无疑是一个令人着迷且至关重要的概念。它不仅是面向对象编程(OOP)的三大特性之一(与封装和继承并列),也是实现代码复用、提高软件灵活性和可扩展性的关键所在。当我们谈论C++这门强大的编程语言时,多态更是一个不可或缺的话题

C++作为一种支持多种编程范式的语言,不仅拥有过程式编程的严谨与高效,也具备面向对象编程的丰富与灵活。多态正是这种灵活性的集中体现。它允许我们以统一的方式处理不同类型的对象,无需关心其具体类型,只需知道它们都属于某个共同的基类或接口。这种“以不变应万变”的能力,使得C++程序员在面对复杂多变的业务需求时,能够保持代码的清晰、简洁和可维护性

本文将带领读者一起探索C++多态的奥秘。我们将从多态性的基本概念入手,逐步深入其实现原理,我们将通过丰富的示例代码和详细的解释说明,让我们一起踏上这段探索多态性的旅程吧!


📒1. 多态的定义及实现

🍁多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为

构成多态的两个条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的虚函数必须构成派生类对基类的重写(覆盖)

多态代码示例

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

class Student : public Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	p.BuyTicket();
	s.BuyTicket();
	return 0;
}

在这里插入图片描述


🍂虚函数的重写

虚函数

概念:被virtual修饰的类成员函数称为虚函数

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

虚函数的重写(覆盖)

概念: 派生类中有一个跟基类完全相同的虚函数(即 派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

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

class Student : public Person 
{
public:
	// 返回值类型、函数名字、参数列表完全相同,构成虚函数的重写
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
	}
};

注意:

  1. 在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写,但是该种写法不是很规范,不建议使用
class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
	// 基类不加virtual也构成虚函数重写,但是不规范
	void BuyTicket() { cout << "买票-半价" << endl; }
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

class Person {
public:
	// 析构函数的名称统一处理成destructor
	virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
	// 无论是否加virtual关键字,都与基类的析构函数构成重写
	virtual ~Student() { cout << "~Student()" << endl; }
};

⛰️override 和 final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失

因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写


final:修饰虚函数,表示该虚函数不能再被重写

在这里插入图片描述


override:判断一个虚函数是否重写了基类虚函数,如果没有则报错

在这里插入图片描述


🌄重载、覆盖、隐藏

在这里插入图片描述


📕2. 抽象类

🎩抽象类概念

概念: 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象

在这里插入图片描述


🎈接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数


📜3. 多态的原理

🌈虚函数表

在开始前先问大家一个 简单的 问题,下面这个类的大小是多少?在类和对象时,我们讲过类的大小判定和结构体差不多,那么在x86中,它的大小到底是不是4bytes?

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

在这里插入图片描述
是不是很奇怪为什么它的大小会是8bytes,那么让我们来一探究竟!

通过观察测试我们发现b对象是8bytes,除了_p成员,还多一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
在这里插入图片描述


🌞虚函数表的特征

基类和派生类不会共用一张虚函数表
在这里插入图片描述
同一个自定义类型的对象将会共用一张虚函数表
在这里插入图片描述


通过以上测试,我们发现含有虚函数的类中都至少都有一个虚函数表,虚函数的地址要被放到虚函数表中,那么是所有的虚函数的地址都要放进去嘛?我们再来测试以下

虚函数是否都放入虚函数表代码测试

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

结论:

  • 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚
    表指针也就是存在部分的另一部分是自己的成员
  • 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
    中存的是重写的Derive::Func1
    ,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
    的覆盖。重写是语法的叫法,覆盖是原理层的叫法
  • 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
    数,所以不会放进虚表
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
  • 总结一下派生类的虚表生成:
    1. 先将基类中的虚表内容拷贝一份到派生类虚表中
    2. 然后如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    3. 最后派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  • 注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
    他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针
    。那么虚表存在哪的
    呢?实际我们去验证一下会发现vs下是存在代码段的

在这里插入图片描述


🌙验证虚函数表的存放位置

我们用代码来验证一下vs下虚函数表的存放位置

代码示例(验证时使用上面的类(Base)进行验证)

int main()
{
	Base b1;
	Derive d1;
	int a = 99;
	Base* b = new Base;
	static int c = 99;
	const char* p = "const char";
	printf("栈区地址:%p\n", &a);
	printf("堆区地址:%p\n", b);
	printf("静态区地址:%p\n", &c);
	printf("代码段地址:%p\n", p);
	printf("虚函数表地址:%p\n", *((int*)(&b1))); // 虚表地址比较接近代码段
	printf("虚函数地址:%p\n", &Base::Func1);
	return 0;
}

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


⭐多态的原理

在这里插入图片描述

多态实则是通过不同的虚表,找到不同的虚函数来调用, 这样就实现出了不同对象去完成同一行为时,展现出不同的形态

看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。普通的函数调用时编译时确认好的


📚4. 虚函数表

🧩单继承中的虚函数表

单继承中的虚函数表

class Base {
public :
	virtual void func1() { cout<<"Base::func1" <<endl;}
	virtual void func2() {cout<<"Base::func2" <<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;}
private :
	int _b;
};

在这里插入图片描述
按照上面讲的,我们在d中的虚函数表应该有func3和func4,但是通过监视窗口并没有发现这两个函数,其实编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug,那么我们自己将虚表打印出来


💧打印虚函数表

打印虚函数表代码示例

// 打印虚表
typedef void (*VFUNC)();

void PrintVFT(VFUNC* a)
{
	// 因为虚函数表在vs下最后一个元素是 0,
	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;
	Derive d;
	// 类似于打印虚表指针,只不过最后要强制转换成 VFUNC*
	PrintVFT((VFUNC*)(*((int*)&b)));
	PrintVFT((VFUNC*)(*((int*)&d)));

	return 0;
}

在这里插入图片描述
注意:有的时候可能会莫名其妙多出很多函数指针,这时我们只需要清理以下解决方案即可
在这里插入图片描述


🧩多继承中的虚函数表

多继承中的虚函数表

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int _b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int _b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int _d1;
};
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))); // 打印第一张虚函数表
	PrintVFT((VFUNC*)(*((int*)((char*)&d+sizeof(Base1))))); // 打印第二张虚函数表
	return 0;
}

在这里插入图片描述

我们要想打印第二张虚表就必须跳过第一张,我们来分析一下 ((char*)&d+sizeof(Base1))
在这里插入图片描述
在这里插入图片描述


🔥虚函数的调用

我们通过汇编来观察一下虚函数的调用

int main()
{
	Derive d;
	Base1* p1 = &d;
	p1->func1();
	Base2* p1 = &d;
	p2->func2();
	return 0;
}

p1->func1()
在这里插入图片描述
p2->func1()

在这里插入图片描述
我们发现p2相较于p1调用func1函数进行的步骤多了许多,但是最后发现它们所调用的函数地址相同,所以他们调用的是同一个函数!而进行这么多步骤是为了 修正this指针


注意:

  • inline函数可以是虚函数,如果是普通调用,则inline起作用,如果是多态调用,inline不起作用
  • 静态成员不可以是虚函数,因为静态成员函数没有this指针,无法访问虚函数表
  • 构造函数不可以是虚函数,对象中的虚函数表指针是在构造函数阶段才初始化的,虚函数的多态调用要去虚函数表中找,但虚函数表指针还没初始化

📖5. 总结

经过对C++多态的深入学习,我们不难发现,多态性是面向对象编程中一个不可或缺的概念,它赋予了代码更高的灵活性和可扩展性。通过虚函数和继承机制,C++实现了运行时多态,让我们能够以统一的方式处理不同类型的对象,这无疑极大地提高了软件开发的效率和质量

在学习的过程中,我们或许会遇到一些挑战和疑惑,但正是这些挑战促使我们不断思考、不断探索。多态性的理解和运用需要我们对C++的类继承、虚函数等核心概念有深入的理解,同时也需要我们在实践中不断积累经验

然而,学习多态性并不仅仅是为了掌握一个编程技巧,更重要的是它培养了我们的编程思维和解决问题的能力。通过多态,我们可以更加灵活地设计软件架构,实现代码复用,提高软件的可维护性和可扩展性。而我们不要满足于对多态性的初步了解,而是要继续深入探索,不断实践。只有在实践中,我们才能真正理解和掌握多态性的精髓,才能将其运用到实际项目中,发挥出其最大的价值

让我们一起在学习的道路上不断前行,探索C++多态的无限可能

最后推荐两篇关于菱形虚拟继承的文章
C++ 虚函数表解析
C++ 对象的内存布局

在这 里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述

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

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

相关文章

个人学习算法总结的基础crud与算法思想数据结构解释

建议都从简单的crud入手,结合生活理解了结构与操作在去进阶更难的东西,做事有规划有步骤有时间限制这样比较好进步 跳转阅读

进化生物学的数学原理 知识点总结

1、进化论与自然选择 1.1 进化论 1、进化论 过度繁殖 -> 生存竞争 -> 遗传和变异 -> 适者生存 2、用进废退学说与自然选择理论 用进废退&#xff1a;一步适应&#xff1a;变异 适应 自然选择&#xff1a;两步适应&#xff1a;变异 选择 适应 3、木村资生的中性…

Mysql开启查询日志(General Log)

1、增加配置&#xff1a; /etc/my.cnf [mysqld] general_log1 general_log_file/var/log/mysql/query.log 2、增加目录和文件&#xff0c;并且授权 可以使用以下命令修改权限&#xff1a; 创建目录&#xff1a;sudo mkdir -p /var/log/mysql 更改目录所有者&#xff1a;sudo…

[环境配置]vscode通过ssh连接autodl进行项目开发

警告&#xff1a;如果使用VSCode直接执行或开终端执行训练程序&#xff0c;请在调试完成后最后通过screen/tmux工具开守护进程&#xff0c;确保程序不受SSH连接中断影响程序执行&#xff01; 官方文档&#xff1a;请戳 AutoDL使用方法&#xff1a; 在进行操作前您需要提前安装…

【日常记录】【插件】prisma 链接MySQL数据库 简单入门

文章目录 1、新建项目&#xff0c;使用prisma链接数据库1.1、先创建一个项目1.2、初始化 npm 配置文件及下载依赖1.3、初始化TS配置文件1.4、初始化 prisma1.5、更改 prisma/schema.prisma1.6 更改.env 文件1.7 编写 prisma/schema.prisma1.8 将编写的 prisma/schema.prisma 映…

实时数仓Hologres V2.2发布,Serverless Computing降本20%

Highlight 新发布Serverless Computing&#xff0c;提升大任务稳定性&#xff0c;同时可降低20%计算成本 引擎性能优化&#xff0c;TPC-H 1TB测试相对V1.X 提升100% 实时湖仓加速架构升级&#xff0c;支持Paimon&#xff0c;直读ORC、Parquet数据性能提升5倍以上 新增实例监…

如何安全进行亚马逊、沃尔玛测评?

在亚马逊、沃尔玛、速卖通、阿里国际站等电商平台上&#xff0c;测评已成为一种高效的推广手段&#xff0c;但伴随的风险也不容忽视。这些风险主要源于平台严格的大数据风控机制&#xff0c;它涵盖了多个方面&#xff0c;以确保评价的真实性和合规性。 首先&#xff0c;硬件参数…

解决 uniapp h5 页面在私有企微iOS平台 间歇性调用uni api不成功问题(uni.previewImage为例)。

demo <template><view class"content"><image class"logo" src"/static/logo.png"></image><button click"previewImage">预览图片</button></view> </template><script> //打…

数据可视化实验一:Panda数据处理及matplotlib绘图初步

目录​​​​​​​ 2024-6-17 一、请将所有含有发明家“吴峰”的发明专利的“申请日”打印出来。并将含有“吴峰”的所有发明专利条目保存到Excel中 1.1 代码实现 1.2 运行结果 二、读取文件创建城市、人口、性别比、城镇化率DataFrame对象&#xff0c;计算指标排名&…

AI写代码,CS还有前途吗?加州大学伯克利分校:CDSS申请人数激增48%!

目录 01 CS入学人数暴涨 02 人类Coder可堪大任 03 AI还没有学会创新 04 编程与农耕不同 AI写了这么多代码&#xff0c;你还应该学习计算机科学吗&#xff1f; 新的数据显示&#xff0c;学生们仍然热衷于选修计算机科学&#xff1a;加州大学伯克利分校&#xff08;UCB&#…

AI大模型应用(1)OpenAi API快速入门

AI大模型应用(1)OpenAi API快速入门 2022 年 11 月&#xff0c;ChatGPT 成功面世&#xff0c;成为历史上用户增长最快的消费者应用。与 Google、FaceBook等公司不同&#xff0c;OpenAI 从初代模型 GPT-1 开始&#xff0c;始终贯彻只有解码器&#xff08;Decoder-only&#xff…

动力学笔记01——共振频率和共振带的数学定义

文章目录 0、背景描述1、正文2. 位移、速度、加速度的共振频率并不相同 0、背景描述 过去一年&#xff0c;我基本都在考虑塔架&#xff08;尤其是混塔&#xff09;频率仿真/模态分析的问题。关于这个问题&#xff0c;不仅有地基刚度&#xff0c;还有塔筒本身以及其他影响频率的…

散户必须知道!个股场外期权期限是什么?

今天带你了解散户必须知道&#xff01;个股场外期权期限是什么&#xff1f;场外个股期权是一种交易双方买卖未来某个时间以某个价格购买或卖出某种资产的权力&#xff0c;允许投资者以相对较小的期权费用获得名义本金&#xff0c;以获取更高的回报。 个股场外期权期限是什么&am…

【免费Web系列】大家好 ,今天是Web课程的第二二天点赞收藏关注,持续更新作品 !

这是Web第一天的课程大家可以传送过去学习 http://t.csdnimg.cn/K547r 员工管理 1. 修改员工 对于修改功能&#xff0c;分为两步实现&#xff1a; 点击 “编辑” 根据ID查询员工的信息&#xff0c;回显展示。 点击 “保存” 按钮&#xff0c;修改员工的信息 。 1.1 回显…

Docker(二)-Centos7安装Docker并配置镜像加速

系统用户为非root用户 1.安装条件 确定Centos版本是否是7及以上sudo vim /etc/redhat-release2.官网地址 https://docs.docker.com/engine/install/centos3.卸载已安装的旧版本 sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-lates…

【问题记录】Ubuntu提示: “E: 软件包 gcc 没有可安装候选“

Ubuntu提示: "E: 软件包 gcc 没有可安装候选" 一&#xff0c;问题现象二&#xff0c;问题原因&解决方法 一&#xff0c;问题现象 在虚拟机Ubuntu中进行安装gcc命令时报错&#xff1a;“E: 软件包 gcc 没有可安装候选”: 二&#xff0c;问题原因&解决方法 …

关于使用命令行打开wps word文件

前言 在学习python-docx时&#xff0c;想在完成运行时使用命令行打开生成的docx文件。 总结 在经过尝试后&#xff0c;得出以下代码&#xff1a; commandrstart "C:\Users\86136\AppData\Local\Kingsoft\WPS Office\12.1.0.16929\office6\wps.exe" "./result…

【SpringBoot整合系列】SpringBoot整合kinfe4j

目录 kinfe4j与Swagger的区别 SpringBoot2.x整合kinfe4j1.添加依赖2.启动类注解3.创建Knife4J配置类4.实体类5.接口admin访问 api访问 常用注解汇总SpringBoot3.x整合Kinfe4j启动报错解决1.更换依赖2.启动类3.配置4.配置类5.参数实体类6.接口admin访问 api访问 各版本注解参照 …

openlayers 使用WMTS和XYZ加载天地图切片服务

openlayers 使用WMTS和XYZ加载天地图切片服务 本篇介绍一下使用openlayers加载天地图切片&#xff0c;两种方法&#xff1a; 使用WMTS使用XYZ 1 需求 openlayers加载天地图 2 分析 主要是不同类型source的使用 WMTS&#xff08;Web Map Tile Service&#xff09; 是 OGC…

英伟达开源 3400 亿参数模型;苹果 iOS 18 紧急 SOS 新增实时视频功能丨 RTE 开发者日报 Vol.225

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE&#xff08;Real-Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「…