【C++】C++的多态

news2024/9/20 10:01:54

目录

多态的使用

多态的概念

多态的定义和实现

虚函数 

构成多态的条件

特殊情况:协变

析构函数的重写

 怎么实现

 为什么实现 

override和final关键字

 override

 final 

重载/重写/隐藏的对比

纯虚函数和抽象类 

 纯虚函数

 抽象类

多态的实现 

 虚函数表指针

 多态的实现

 实现方式

 动态绑定与静态绑定

 虚函数表的存储地址


多态的使用

多态的概念

         多态从字面意思来看就是多种状态,比如说我们前面使用的函数模版swap,重载函数就是多态的一种体现只不过是在编译时就决定的是静态的,我们今天要了解的是多态的动态形式,比如我们在12306购买车票时,作为成人是原价票,学生可以享受优惠,军人可以买到提前票!

多态的定义和实现 

虚函数 

         在前面继承方面我们可能了解到了虚继承,那么什么是虚函数呢?虚函数和虚继承一样需要使用到virtual关键字,虚函数指的是被virtual修饰的成员函数,并且虚函数的出现是为了多态!

         虚函数在类域内外声明和实现时,只需要在类域声明处加上即可!

注意: 静态变量static和virtual不可以同时修饰成员函数

class Base
{
public:
	// 虚函数
	virtual void Func()
	{
		//...
	}
};

构成多态的条件

  •  需要存在继承关系,如基类和派生类
  •  被调用的成员函数需要是虚函数,并且构成重写/覆盖关系
  •  必须是基类的指针或者引用指向派生类对象进行调用函数

就拿我们12306买票来收,Person类和Student类,这两者是继承关系,我们需要他们的买票函数构成重写关系,重写关系就是在原本是隐藏关系的基础上给成员函数加上virtual关键字! 

// 买票举例
class Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "买票-原价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "买票-打折" << endl;
	}
};

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用!

// 买票举例
class Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "买票-原价" << endl;
	}
};

class Student : public Person
{
public:
	void Buy_Ticket()
	{
		cout << "买票-打折" << endl;
	}
};

         那么最后我们需要满足最后的第三点,使用基类的指针和引用指向派生类对象,由于继承的规定,只有基类的指针和引用可以指向派生类对象,基类的对象可以赋值派生类对象(切片的原因),为了实现多态效果我们必须使用基类的指针或者引用指向派生类对象进行调用函数!

// 买票举例
//写法一:
class Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "买票-原价" << endl;
	}
	void Test()
	{
		Buy_Ticket();
	}
};

class Student : public Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "买票-打折" << endl;
	}
};

int main()
{
	Person* p = new Student;
	p->Test();
	delete p;
	return 0;
}

         这段代码由于继承关系,Student继承了Person的两个函数,其中Buy_Ticket()函数构成多态被重写了,但是Test()并没有,该函数在调用时this指针是Person*的类型,我们传入的对象是Student刚好构成多态的条件!

// 写法二:
class Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "买票-原价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "买票-打折" << endl;
	}
};

void Test(Person* p)
{
	p->Buy_Ticket();
}

int main()
{
	Person* p = new Student;
	Test(p);
	delete p;
	return 0;
}

        第二种写法同样构成的多态的第三个条件,但是我们需要注意只有在多态的情况下,Test函数才会去掉用Student:: Buy_Ticket(),如果没有构成多态,那么函数Test函数调用的内容由参数类型决定!也就是Person类型。

说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向派⽣类对象;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。 

特殊情况:协变

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

 

析构函数的重写

 怎么实现

         基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

 为什么实现 

 为什么基类中的析构函数建议设计成虚函数

防止内存泄漏:

如果不设计成虚函数 

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

class B : public A
{
public:
	~B()
	{
		cout << "~B()->delete" << _p << endl;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}

我们可以看到并没有释放B类中申请的内存! 

class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A
{
public:
	virtual ~B()
	{
		cout << "~B()->delete" << _p << endl;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}

override和final关键字

 override
  •  这个关键字和assert的作用差不多,但是override是在编译时检查,assert是在运行时检查
  •  override可以检查用户是否重写成功虚函数,如果重写失败,编译时就会报错 
 final 
  •  我们不希望派生类去重写基类的某个虚函数时,可以加上这个关键字,这样就无法重写了

 

重载/重写/隐藏的对比

 

纯虚函数和抽象类 

 纯虚函数
  •  在虚函数的后⾯写上=0,则这个函数为纯虚函数
  • 纯虚函数只需要声明即可,不用定义。注意:纯虚函数是可以定义的,只是没有必要 
// 纯虚函数和抽象类
class Pumping_paper
{
public:
	virtual void using_feel() = 0;
	void Test()
	{
		using_feel();
	}
};

class ManHua : public Pumping_paper
{
public:
	 void using_feel() 
	{
		cout << "蓬松" << endl;
	}
};

class QingFeng : public Pumping_paper
{
public:
	 void using_feel() 
	{
		cout << "柔软" << endl;
	}
};

int main()
{
	Pumping_paper* p1 = new ManHua;
	Pumping_paper* p2 = new QingFeng;
	p1->Test();
	p2->Test();
	delete p1;
	delete p2;
	return 0;
}

 抽象类
  •  只要类中存在纯虚函数都是抽象类
  • 抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。 

多态的实现 

 虚函数表指针

  • 虚函数表是一个数组

  • 这是数组存放的是指针

  • 指针是函数指针

  • 总结:存放虚函数指针的数组

/*下⾯编译为32位程序的运⾏结果是什么()
A.编译报错  B.运⾏报错  C.8  D.12*/
class A
{
public:
	virtual void Func()
	{
		//...
	}
protected:
	int _a = 1;
	char _b;
};

int main()
{
	A a1;
	cout << sizeof(a1) << endl;
	return 0;
}

 

 

只要class存在虚函数,这些函数的指针都会存放在虚函数表中 

  • 相同类生成的多个对象共用同一种虚函数表 

 多态的实现

 实现方式
class Person
{
public:
	virtual void Print()
	{
		cout << "全价" << endl;
	}

	virtual void Func()
	{
		//...
	}

	void Test()
	{
		Print();
	}

protected:
	string _name = "欧阳";
};

class Student : public Person
{
public:
	virtual void Print()
	{
		cout << "打折" << endl;
	}
	virtual void Func_S()
	{
		//...
	}
protected:
	int _id = 1;
};

class Child : public Person
{
public:
	virtual void Print()
	{
		cout << "免费" << endl;
	}
protected:
	int _age = 6;
};

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

	p1->Test();
	p2->Test();
	p3->Test();

	delete p1;
	delete p2;
	return 0;
}

        类中虚函数的指针都会被存放在虚函数表中,派生类会额外开辟一块空间拷贝基类的虚函数表,然后将可以重写的虚函数地址更换为派生类的虚函数 

 动态绑定与静态绑定
  •          对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
  •         满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。

静态绑定 

 动态绑定 

 

 虚函数表的存储地址

虚函数和普通函数一样存放在栈区域 

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};

class Derive : public Base
{
public :
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);
	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}

可以看出虚函数表在vs2022是被编译器存放在常量区中 !

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

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

相关文章

魔方财务安装指南

本文将详细介绍魔方财务的安装、升级和迁移过程&#xff0c;确保您能够顺利地部署和使用魔方财务系统。 服务器配置一览表 以下是魔方财务1.0.0及更高版本的最低和推荐系统要求&#xff1a; 需求名称推荐配置最低要求OSCentOS/Debian/UbuntuLinux&#xff08;不要使用window…

IP协议及相关特性

IP协议负责地址管理和路由选择。它的组成为&#xff1a; 接下来我们将对其中较重要的部分进行介绍。 4位版本&#xff1a;这里的四位版本只有两个取值 分别为IPv4和IPv6&#xff0c;这两个额分别为不同的IP协议&#xff0c;但是现在主流的还是IPv4但是近年来IPv6在中国的普及率…

2022高教社杯全国大学生数学建模竞赛C题 问题一(1) Python代码演示

目录 问题 11.1 对这些玻璃文物的表面风化与其玻璃类型、纹饰和颜色的关系进行分析数据探索 -- 单个分类变量的绘图树形图条形图扇形图雷达图Cramer’s V 相关分析统计检验列联表分析卡方检验Fisher检验绘图堆积条形图分组条形图分类模型Logistic回归随机森林import matplotlib…

中秋之际,唱响工体!玛丽亚·凯莉2024演唱会北京站璀璨上演

续写传奇华章 启幕音乐盛典 中秋之际&#xff0c;全国数万乐迷翘首以待的音乐盛典如约而至。时隔多年&#xff0c;传奇天后玛丽亚凯莉惊艳开唱工体&#xff01; 夜幕降临&#xff0c;圆月高悬&#xff0c;在不绝于耳的欢呼声中&#xff0c;玛丽亚凯莉以一袭流光溢彩的礼服优雅…

【LIO】FAST-LIO论文详解

FAST-LIO论文详解 1. 摘要2. 简介1. 相关工作A. LiDAR 里程计和地图绘制 2. 实现方法A. 基础知识1. 连续模型在这里插入图片描述 B. 激光雷达测量的预处理C. 状态估计1) 前向传播&#xff1a;2) 反向传播与运动补偿&#xff1a;3) 残差计算&#xff1a; 1. 摘要 提出了一种计算…

简单题21 - 合并两个有序链表(Java)20240917

问题描述&#xff1a; java代码&#xff1a; /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val…

Java 技巧 如何在IDEA2024 中快速打出System.out.println();

1.基本用法 键入sout回车 回车后变成&#xff1a; 2.打印变量 快速打印变量,以打印变量名为set为例&#xff0c;set.sout回车&#xff0c; 回车后变成

简单题26 - 删除有序数组中的重复项(Java)20240917

问题描述&#xff1a; java代码&#xff1a; class Solution {public int removeDuplicates(int[] nums) {if (nums.length 0) return 0; // 处理空数组情况int i 0; // 指向新数组中的最后一个不重复元素for (int j 1; j < nums.length; j) {if (nums[j] ! nums[i]) { …

室内灯具检测系统源码分享

室内灯具检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

从kaggle竞赛零基础上手CV实战(Deepfake检测)

关注B站可以观看更多实战教学视频&#xff1a;hallo128的个人空间 从kaggle竞赛零基础上手CV实战 从kaggle竞赛零基础上手CV实战&#xff08;Deepfake检测&#xff09; 目录 从kaggle竞赛零基础上手CV实战&#xff08;Deepfake检测&#xff09;背景介绍学习地址课程大纲课程特色…

方法引用(Java)

把已经有的方法拿过来用&#xff0c;当做函数式接口中抽象方法的方法体 1.引用处必须是函数式接口 2.被引用的方法必须已经存在 3.被引用的方法形参的返回值需要跟抽象方法保持一致 4.被引用方法的功能要满足当前需求 package function;import java.util.Arrays;public cl…

网络高级项目( 基于webserver的工业数据采集和控制项目)

目录 一、项目要求&#xff1a; 二、演示效果&#xff1a; 设备端&#xff1a; Modbus用户控制端&#xff1a; 服务器端&#xff1a; 网页端&#xff1a; 三、 项目代码&#xff1a; Modbus用户控制端代码&#xff1a; 服务器端代码&#xff1a; 网页端代码&#xff1…

C++3D迷宫

目录 开头程序程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 #include <iostream> using namespace std; void printmaze(char strmaze[5][5][5]) {cout << "-----" << endl;int i 0;int ia 0…

pdf去水印怎么去掉免费?6个pdf去除水印的方法快码住,超级好用!

pdf去水印怎么去掉免费&#xff1f;您是否有一些带有水印的pdf文档&#xff0c;让您感觉到头疼&#xff1f;您又是否希望能够去除这些水印&#xff0c;或者想用其他水印来替换现有的水印&#xff1f;如果是这样的话&#xff0c;我非常推荐您继续阅读本篇文章。本文将为您提供一…

如何在Linux下升级R版本和RStudio

一、升级R版本 在Linux上&#xff0c;R的安装通常通过包管理器完成。不同的Linux发行版&#xff08;如Ubuntu、Debian、Fedora等&#xff09;可能略有不同。下面以Ubuntu为例&#xff0c;介绍如何升级R版本。如果你使用其他发行版&#xff0c;步骤可能类似。 二.更新步骤 2.…

Git:版本控制工具介绍

目录 全文概要版本控制工具介绍版本控制系统的概念**版本控制系统的历史****版本控制系统的分类****本地版本控制系统****集中式版本控制****分布式版本控制系统** Git 介绍Git 概念Git 与 SVN 对比**SVN的记录方式****Git 的记录快照** Git 安装Git 安装Bash、CMD与GUIGit 的配…

黑神话悟空黄凤岭

黑神话悟空黄风岭支线任务大全 第二关“黄风寨”的难度可以说是飙升&#xff1a; 虎先锋从名字来看只是一般妖王&#xff0c;但是他的天命人“猴头下酒”怕是吃到撑也吃不完&#xff0c;关底的黄风大圣二阶段放法宝之后的飓风更是重量级&#xff0c;从视线和移动能力对玩家会造…

Git提交类型

说明&#xff1a;Git提交类型指的是代码commit时&#xff0c;写在comment前面的标志&#xff0c;表示此次commit的提交类型&#xff0c;如下&#xff1a; Git提交类型 常见的Git提交类型有&#xff1a; feat&#xff1a;新特性、新功能或优化&#xff1b; fix&#xff1a;修复…

再临TSC原创24年CSP-J全真模拟卷-阅读程序篇(1)

没有看基础题的可以点击我的主页查看基础题部分 阅读程序题&#xff1a; 1. 16. 如果删除第6行&#xff0c;程序仍然能正确运行 A正确B错误 答案&#xff1a;B 17.输入负数时&#xff0c;程序什么也不会输出&#xff0c;并正常结束程序 A正确B错误 答案&#xff1a;A …

Linux内核(Kernel)启动过程分析

文章目录 Linux内核&#xff08;Kernel&#xff09;启动过程一、内核启动的基本流程1. 启动加载程序 (Bootloader)2. 内核解压阶段3. 内核启动&#xff08;Kernel Startup&#xff09;4. start_kernel函数5. 启动初始进程 二、内核文件加载及解压缩1.为什么是压缩文件2.文件类型…