Cpp多态机制的深入理解(20)

news2024/11/23 18:12:52

文章目录

  • 前言
  • 一、多态的概念
  • 二、多态的定义与实现
    • 两个必要条件
    • 虚函数
    • 虚函数的重写
    • 重写的三个例外
    • override 和 final
    • 重载、重写(覆盖)、重定义(隐藏)
  • 三、抽象类
    • 概念
    • 接口继承和实现继承
  • 四、多态的原理
    • 虚表和虚表指针
    • 虚函数调用过程
    • 动态绑定与静态绑定
  • 五、那...那单继承甚至多继承呢?
  • 总结


前言

  多态也是三大面向对象语言的特性之一,同时我也觉得他也蛮有意思的
  与封装“一个方法,多个接口”不同的是,多态可以实现 “一个接口,多种方法

  调用同名函数时,可以根据不同的对象(父类对象或子类对象)调用属于自己的函数,实现不同的方法,因此 多态 的实现依赖于 继承


一、多态的概念

  在使用多态的代码中,不同对象完成同一件事会产生不同的结果

  比如在购买高铁票时,普通人原价,学生半价,而军人可以优先购票,对于 购票 这一相同的动作,需要 根据不同的对象提供不同的方法

二、多态的定义与实现

两个必要条件

  1. virtual 修饰后形成的虚函数,与其他类中的虚函数形成 重写(三同:返回值、函数名、参数均相同)
  2. 必须通过【父类指针】或【父类引用】进行虚函数调用

在这里插入图片描述

虚函数

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

全局虚函数没有意义,因为虚函数是为多态而用的

在这里插入图片描述

虚函数的重写

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

// 基类
class Person 
{
public:
    // 虚函数
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

// 派生类
class Student : public Person 
{
public:
    // 虚函数重写
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

// 三种函数实现
// 引用
void Func(Person& p)
{
	p.BuyTicket();
}
 
// 指针
//void Func(Person* p)
//{
//	p->BuyTicket();
//}
 
// 非引用指针,调用父类
//void Func(Person p)
//{
//	p.BuyTicket();
//}

测试结果:
在这里插入图片描述

重写的三个例外

  1. 协变(基类与派生类虚函数返回值类型不同)

  派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

这个了解一下就行,实际我感觉挺没啥用处的
如果你也有这种感觉,鼓励你致电老本,去好好批斗他!

class A {};
class B : public A {};
 
class Person
{
public:
	// 协变 返回值可以是父子类对象指针或引用
	//virtual A* BuyTicket() // 返回值是父类指针
	virtual Person* BuyTicket()
	{
		cout << "Person-> 买票-全价" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:
	//virtual B* BuyTicket()// 返回值是子类指针
	virtual Student* BuyTicket()
	{
		cout << "Student-> 买票-半价" << endl;
		return nullptr;
	}
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

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

class Person
{
public:
	// 析构函数名不同,构成重写,编译器将析构函数名字统一处理成destructor
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person
{
public:
    virtual ~Student()
	{
		cout << "delete[]" << _ptr << endl;
		
		delete[] _ptr;
		cout << "~Student()" << endl;
	}
private:
	int* _ptr = new int[10];
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	// 正常情况调用析构没有问题
	//Person p;
	//Student s;
	//Func(p);
	//Func(s);
 
	// 派生类有动态开辟的内存,需要调用多态
	// 指向谁调用谁
	Person* p1 = new Person;
	Person* p2 = new Student;
 
	delete p1;
	delete p2;
 
	return 0;
}

在这里插入图片描述

  1. 派生类重写虚函数virtual关键字可以省略
class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
 
class Student : public Person
{
public:
    // 派生类virtual关键字省略
	~Student()
	{
		cout << "~Student()" << endl;
	}
};

在这里插入图片描述

override 和 final

  C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载

  1. final:修饰虚函数,表示该虚函数不能再被重写
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
// final 修饰虚函数,不能重写
class Car
{
public:
	// 加了final关键字,虚函数不能被重写
	virtual void Drive() final {}
};

class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};

int main()
{
	Benz b;
	return 0;
}

在这里插入图片描述

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

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

三、抽象类

概念

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

class Car
{
public:
	// 纯虚函数 强制派生类重写虚函数 
	virtual void Drive() = 0;
};

int main()
{
	Car c;
	return 0;
}

在这里插入图片描述

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()
{
	// Car c;
	Benz b1;
	BMW b2;
 
	// 基类可以定义指针 指向谁调用谁
	Car* ptr1 = &b1;
	Car* ptr2 = &b2;
 
	ptr1->Drive();
	ptr2->Drive();
	
	return 0;
}

在这里插入图片描述

接口继承和实现继承

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

四、多态的原理

  在讲解原理之前,不如我们先来看这么一段神奇代码

#include <iostream>
using namespace std;

class Test
{
	virtual void func() {};
};

int main()
{
	Test t;	//创建一个对象
	cout << "Test sizeof(): " << sizeof(t) << endl;
	
	return 0;
}

  可能你会觉得没有对象,会觉得是0,但是你突然想起了之前讲过的空类也占内存空间,你可能会想是不是1

  但是其实都错了,真相是4/8(取决于你的系统是32位还是64位),可能我这么一说,你也猜到了其实有一个隐藏变量,且类型是指针类型

其实,就是靠着这个虚表指针和虚表实现了多态

虚表和虚表指针

  在 vs 的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表

  虚函数表中存储的是虚函数指针,可以在调用函数时根据不同的地址调用不同的方法

可能有点混,有三个“虚”,大家别被整虚了!
虚表指针指向虚表,虚表里面存放着虚函数指针,所以虚表的本质其实是个函数指针数组

  接下来我会给出一段代码,在该代码中父类 Person 有两个虚函数(func3 不是虚函数),子类 Student 重写了 func1 这个虚函数,同时新增了一个 func4 虚函数

#include <iostream>

using namespace std;

class Person
{
public:
	virtual void func1() { cout << "Person::fun1()" << endl; };
	virtual void func2() { cout << "Person::fun2()" << endl; };
	void func3() { cout << "Person::fun3()" << endl; };	//fun3 不是虚函数
};

class Student : public Person
{
public:
	virtual void func1() { cout << "Student::fun1()" << endl; };

	virtual void func4() { cout << "Student::fun4()" << endl; };
};

int main()
{
	Person p;
	Student s;
	
    return 0;
}

在这里插入图片描述

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

void PrintVFTable(VF_T table[])	//也可以将参数类型设为 VF_T*
{
	//vs中在虚表的结尾处添加了 nullptr
	//如果运行失败,可以尝试清理解决方案重新编译
	int i = 0;
	while (table[i])
	{
		printf("[%d]:%p->", i, table[i]);
		VF_T f = table[i];
		f();	//调用函数,相当于 func()
		i++;
	}
	cout << endl;
}


int main()
{
	//提取出虚表指针,传递给打印函数
	Person p;
	Student s;

	//第一种方式:强转为虚函数地址(4字节)
	PrintVFTable((VF_T*)(*(int*)&p));
	PrintVFTable((VF_T*)(*(int*)&s));

	return 0;
}

子类重写后的虚函数地址与父类不同
在这里插入图片描述

因为平台不同指针大小不同,因此上述传递参数的方式(VF_T * )( * (int * )&p 具有一定的局限性
假设在 64 位平台下,需要更改为 (VF_T * )( * (long long * )&p

//64 位平台下指针大小为 8字节
PrintVFTable((VF_T*)(*(long long*)&p));
PrintVFTable((VF_T*)(*(long long*)&s));

除此之外还可以间接将虚表指针转为 VF_T* 类型进行参数传递

//同时适用于 32位 和 64位 平台
PrintVFTable(*(VF_T**)&p);
PrintVFTable(*(VF_T**)&s);

传递参数时的类型转换路径
在这里插入图片描述
  不能直接写成 PrintVFTable((VF_T*)&p);,因为此时取的是整个虚表区域的首地址地址,无法定位我们所需要虚表的首地址,打印时会出错

  综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系

虚表是在 编译 阶段生成的
虚表指针是在构造函数的 初始化列表 中初始化的
虚表一般存储在 常量区(代码段),有的平台中可能存储在 静态区(数据段)

int main()
{
	//验证虚表的存储位置
	Person p;
	Student s;

	int a = 10;	//栈
	int* b = new int;	//堆
	static int c = 0;	//静态区(数据段)
	const char* d = "xxx";	//常量区(代码段)

	printf("a-栈地址:%p\n", &a);
	printf("b-堆地址:%p\n", b);
	printf("c-静态区地址:%p\n", &c);
	printf("d-常量区地址:%p\n", d);

	printf("p 对象虚表地址:%p\n", *(VF_T**)&p);
	printf("s 对象虚表地址:%p\n", *(VF_T**)&s);

	return 0;
}

在这里插入图片描述

显然,虚表地址与常量区的地址十分接近,因此可以推测 虚表 位于常量区中,因为它需要被同一类中的不同对象共享,同时不能被修改(如同代码一样)

虚函数调用过程

综上,我们可以大概想象出多态的原理了:

  1. 首先确保存在虚函数且构成重写
  2. 其次使用【父类指针】或【父类引用】指向对象,其中包含切片行为
  3. 切片后,将子类中不属于父类的切掉,只保留父类指针可调用到的部分函数
  4. 实际调用时,父类指针的调用逻辑是一致的:比如虚表第一个位置调用第一个函数,虚表第二个位置调用第二个函数,但是因为此时的虚表是切片得到的,所以 同一位置 可以调用到不同的函数,这就是多态

也就是说,父类和子类的虚表其实是不一样的,在构成重写的前提下!
这就是多态!

int main()
{
	Person* p1 = new Person();
	Person* p2 = new Student();

	p1->func1();
	p2->func1();

	delete p1;
	delete p2;
	return 0;
}

通过汇编代码来看的话:
在这里插入图片描述
在这里插入图片描述

动态绑定与静态绑定

  其实我们想一想,函数重载某种程度上也是一种多态,也是一个函数面对不同对象的时候有不同的效果,但是不同的是,重载在编译的时候就确定了待调用函数的地址,而动态绑定的代码,待调用地址存放在 eax 中,不确定
在这里插入图片描述

五、那…那单继承甚至多继承呢?

  坦白说,这很麻烦,我也不敢说我很懂,于是我在这里贴两篇文章,大家自行参阅吧!

《C++虚函数表解析》
《C++对象的内存布局》


总结

  我们终于学完三大面向对象特性了,坦白说,多态还是蛮困难的,但是,我们难度的最高峰再过几篇就要来了,怕不怕!

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

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

相关文章

.net core 接口,动态接收各类型请求的参数

[HttpPost] public async Task<IActionResult> testpost([FromForm] object info) { //Postman工具测试结果&#xff1a; //FromBody,Postman的body只有rawjson时才进的来 //参数为空时&#xff0c;Body(form-data、x-www-form-urlencoded)解析到的数据也有所…

高清解压视频素材从哪儿下载?推荐5个高清推文素材资源网站

做抖音小说推文&#xff0c;哪里找爆款素材&#xff1f;作为一名从业多年的视频剪辑师&#xff0c;今天就来给大家揭秘可以下载爆款推文视频素材的网站&#xff0c;如果你也在苦苦找寻找&#xff0c;赶紧进来看看吧&#xff5e; 1. 稻虎网 首先是稻虎网&#xff0c;作为国内顶…

Python的全局锁GIL解析

Python的全局锁&#xff08;GIL&#xff09;是 CPython 解释器实现中的一个机制&#xff0c;用来确保任何时候只有一个线程执行 Python 字节码。这一机制存在于 CPython 中&#xff0c;主要是为了确保线程操作中的数据一致性&#xff0c;但也因此限制了多线程的并行执行效率。尤…

ELK的ElasticStack概念

目录 传送门前言一、ElasticStack是什么二、ElasticStack数据格式1、Elasticsearch的概述2、Elasticsearch核心概念&#xff08;1&#xff09;接近实时&#xff08;NRT&#xff09;&#xff08;2&#xff09;集群&#xff08;cluster&#xff09;&#xff08;3&#xff09;节点…

硅谷甄选(9)SKU模块

SKU模块 8.1 SKU静态 <template><el-card><el-table border style"margin: 10px 0px"><el-table-column type"index" label"序号" width"80px"></el-table-column><el-table-columnlabel"名称…

如何将ppt转换成word文档?8款ppt转word免费的软件大揭秘,值得收藏!

在日常办公中&#xff0c;将ppt转换成word文档的需求日益增长。这种需求源自多个方面&#xff0c;比如制作详尽的报告、创建课程讲义&#xff0c;或者将信息转化为可编辑的格式。作为一种普遍使用的演示工具&#xff0c;ppt在许多商业环境中扮演着重要角色。然而&#xff0c;随…

pandas——DataFrame

一、dataframe &#xff08;一&#xff09;创建dataframe file.csv Name,Age,City Alice,30,New York Bob,25,Los Angeles Charlie,35,Chicagoimport pandas as pd 1.使用字典创建DataFrame&#xff1a; 其中字典的键是列名&#xff0c;值是数据列表。print(1.使用字典创建D…

vxe-table v4.8+ 与 v3.10+ 虚拟滚动支持动态行高,虚拟渲染更快了

Vxe UI vue vxe-table v4.8 与 v3.10 解决了老版本虚拟滚动不支持动态行高的问题&#xff0c;重构了虚拟渲染&#xff0c;渲染性能大幅提升了&#xff0c;行高自适应和列宽拖动都支持&#xff0c;大幅降低虚拟渲染过程中的滚动白屏&#xff0c;大量数据列表滚动更加流畅。 自适…

关于武汉芯景科技有限公司的马达驱动芯片AT6237开发指南(兼容DRV8837)

一、芯片引脚介绍 1.芯片引脚 二、系统结构图 三、功能描述 逻辑功能

青出于“蓝”的合资第一新能源,“换壳”背后有门道

文/王俣祺 导语&#xff1a;千呼万唤始出来的新能源“马6”终于亮相了&#xff0c;这款马自达EZ-6本以为凭借马自达多年来在国内市场深耕的底蕴可以收获一片支持&#xff0c;但最近却深陷“换壳”风波。那么今天我们就一起看看&#xff0c;这款马自达EZ-6和被冠以“原型”的深蓝…

Github上的十大RAG(信息检索增强生成)框架

信息检索增强生成(Retrieval-Augmented Generation,简称RAG)是一种强大的技术,能够显著提升大型语言模型的性能。RAG框架巧妙地结合了基于检索的系统和生成模型的优势,可以生成更加准确、符合上下文、实时更新的响应。随着对先进人工智能解决方案需求的不断增长,GitHub上涌现出…

【小白学机器学习28】 统计学脉络+ 总体+ 随机抽样方法

目录 参考书&#xff0c;学习书 0 统计学知识大致脉络 1 个体---抽样---整体 1.1 关于个体---抽样---整体&#xff0c;这个三段式关系 1.2 要明白&#xff0c;自然界的整体/母体是不可能被全部认识的 1.2.1 不要较真&#xff0c;如果是人为定义的一个整体&#xff0c;是可…

5、片元着色器之基础光照模型:Phong模型和Blinn-Phong模型

1、什么是Phong光照模型&#xff1f; Phong模型就是在兰伯特模型的基础上增加了镜面反射光的计算。具体来说&#xff0c;兰伯特模型只考虑漫反射光&#xff0c;而Phong模型在此基础上引入了镜面反射光的概念&#xff0c;以模拟光线在光滑表面反射时产生的高光效果。镜面反射光的…

Ubuntu使用Qt虚拟键盘,支持中英文切换

前言 ​ 最近领导给了个需求&#xff0c;希望将web嵌入到客户端里面&#xff0c;做一个客户端外壳&#xff0c;可以控制程序的启动、停止、重启&#xff0c;并且可以调出键盘在触摸屏上使用(我们的程序虽然是BS架构&#xff0c;但程序还是运行在本地工控机上的)&#xff0c;我…

ES(ElaticSearch)详解(含工作原理、基本知识、常见问题和优化方法)

文章目录 一、Lucene 和 ELK 的组成二、ES 配置文件参数解读三、ES 基本知识1、索引&#xff08;Index&#xff09;&#xff1a;类似于关系型数据库的工作表2、类型&#xff08;Type&#xff09;&#xff1a;废弃3、文档&#xff08;Document&#xff09;&#xff1a;类似于关系…

巨好看的登录注册界面源码

展示效果 源码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewport" content"widthdevic…

记一次:使用使用Dbeaver连接Clickhouse

前言&#xff1a;使用了navicat连接了clickhouse我感觉不太好用&#xff0c;就整理了一下dbeaver连接 0、使用Navicat连接clickhouse 测试连接 但是不能双击打开&#xff0c;可是使用命令页界面&#xff0c;右键命令页界面&#xff0c;然后可以用sql去测试 但是不太好用&#…

ts:使用fs内置模块简单读写文件

ts&#xff1a;使用fs内置模块简单读写文件 一、主要内容说明二、例子&#xff08;一&#xff09;、fs模块的文件读写1.源码1 &#xff08;fs模块的文件读写&#xff09;2.源码1运行效果 三、结语四、定位日期 一、主要内容说明 在ts中&#xff0c;我们可以使用内置的fs模块来…

RFID技术让档案管理更高效、更可靠

RFID档案应用&#xff0c;即利用射频识别技术对档案进行管理&#xff0c;其价值主要体现在以下几个方面&#xff1a; PART01效率提升 RFID技术通过无线射频识别&#xff0c;能够快速、准确地识别档案信息&#xff0c;大大提高了档案管理的效率。在传统的档案管理中&#xff0c;…

《数字图像处理基础》学习04-图像的量化

在上一篇文章中&#xff0c;已经实现了对图像的采样。 《数字图像处理基础》学习03-图像的采样-CSDN博客 接着就需要对图像进行量化操作。 目录 一&#xff0c;量化的相关概念 二&#xff0c;matlab编写程序生成量化图像 1&#xff0c;要求 2&#xff0c;思路及注意点…