C++笔记---多态

news2024/12/23 13:51:08

1. 多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。

多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)和运行时多态(动态多态)。

编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们一般把编译时归为静态,运行时归为动态。

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是”(>^ω^<)喵“,传狗对象过去,就是"汪汪"。

2. 多态的定义及实现

具体来说,动态的多态发生在继承体系中,父类与子类或统一父类的各子类之间,是在调用函数时发生不同的行为的现象。

比如Student继承了Person。Person对象买票全价,Student对象优惠买票。

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

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

void func(Person* p)
{
	p->ByTicket();
}

int main()
{
	Person p;
	Student s;
	func(&p);
	func(&s);

	return 0;
}

2.1 构成多态的条件

1. 必须用父类的指针或引用来调用函数(对象不行)

2. 被调用的必须是虚函数(被virtual关键字修饰的函数)

3. 对象构造完成

说明:要实现多态效果,第一必须是父类的指针或引用,因为只有父类的指针或引用才能既可指向子类对象,又可指向自身;第二子类必须对父类的虚函数重写/覆盖;第三在对象构造阶段多态不生效。

2.1.1 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。

注意非成员函数以及静态成员函数都不能加virtual修饰。

class Person
{
public:
	virtual void ByTicket()
	{
		cout << "全价买票" << endl;
	}
};
2.1.2 虚函数的重写/覆盖

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

在这里,重写意味着重写父类虚函数的函数体如果父类和子类虚函数的参数带有不同的缺省值的话,构成多态时,以父类虚函数的缺省值为准

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};

class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

注意:在重写父类虚函数时,子类的虚函数在不加virtual关键字时,也可以构成重写(因为继承后父类的虚函数被继承下来了在子类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

2.1.3 override和final关键字

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。

例如下面这个函数名写错的情况:

// error C3668: “Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类方法
class Car {
public:
	virtual void Dirve()
	{}
};

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

int main()
{
	return 0;
}

如果我们不想让子类重写这个虚函数,那么可以用final去修饰:

// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{
public:
	virtual void Drive() final {}
};

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

int main()
{
	return 0;
}
2.1.4 协变

即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,也构成重写,称为协变。

协变的实际意义并不大,所以我们了解一下即可。

class A {};
class B : public A {};

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

class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

这个语法的设计可能是为了在函数返回值上体现多态。

2.1.5 析构函数的重写

父类的析构函数为虚函数,此时子类析构函数只要定义,就与父类的析构函数构成重写。

虽然父类与子类析构函数名字不同看起来不符合重写的规则,但实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,子类的析构函数就构成重写。

由于析构函数的名称会统一被处理成destructor,所以在不加virtual关键字时,父类和子类的析构函数构成隐藏关系。

下面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。

注意:这个问题问试中经常考察,大家一定要结合类似下面的样例才能讲清楚,为什么父类中的析构函数建议设计为虚函数。

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

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

// 只有派⽣类Student的析构函数重写了Person的析构函数
// 下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;

	return 0;
}
2.1.6 重载/隐藏/重写

 2.2 纯虚函数及抽象类

在虚函数的后面写上 "=0" ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被子类重写,但是语法上可以实现),只要声明即可。

包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果子类继承后不重写纯虚函数,那么子类也是抽象类。

纯虚函数某种程度上强制了子类重写虚函数,因为不重写实例化不出对象。

纯虚函数及抽象类的存在是为了描述一些抽象的概念,如:动物,植物,玩具……

以动物为例,假设有个动物类,我们知道动物是可以叫的,那么这个抽象的“动物”怎么叫呢?我们无法明确,但是我们确实需要描述这一动作,这时就可以采用虚函数以及抽象类。

class Animal
{
public:
	virtual void talk() const = 0
	{}
};

class Dog : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};

class Cat : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};

void letsHear(const Animal& animal)
{
	animal.talk();
} 

int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);
	return 0;
}

我们之前提到,父类和子类之间在类别上存在一种包含关系,上面的例子就是一种具体体现。

由此我们也可以看出,抽象类就是为了实现属于同一类别的事物之间的多态关系而存在的

3. 多态的原理

3.1 虚表指针

先来看一段代码:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

这是怎么回事呢?

按照内存对齐的规则(C语言结构体的大小,结构体内存对齐_c语言 struct大小-CSDN博客), Base类的大小应该是8才对啊。

我们通过调试可以找到答案:

可以看到,在Base对象中多了一个成员变量"__vfptr",使其大小增加到了16个字节。

对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

父类的虚函数放到父类的虚表中;子类继承(拷贝)了父类的虚表,如果有新的虚函数则添加进去,如果重写了父类的虚函数则用子类虚函数的地址对其进行覆盖。

3.2 多态的实现

上面的例子中,func函数中的Ptr->ByTicket()是如何识别指向对象的类型并调用正确的函数的呢?

给每个对象做上一个标记似乎是个不错的选择,这样在访问相应的内存空间时就可以通过这个标记找到合适的函数了。

没错,这个标记就是虚表指针。

对于虚函数的调用,在编译时并不会通过符号表来确定其地址,而是通过对对象的函数虚表进行查找来调用,这样就实现了函数的地址是在运行时(接收到的对象不同,查找到的函数也不同)才被确定,也就是动态的多态。

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

// ptr是指针 + BuyTicket是虚函数满足多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调用函数地址
ptr->BuyTicket();
00EF2001 mov eax, dword ptr[ptr]
00EF2004 mov edx, dword ptr[eax]
00EF2006 mov esi, esp
00EF2008 mov ecx, dword ptr[ptr]
00EF200B mov eax, dword ptr[edx]
00EF200D call eax

// BuyTicket不是虚函数,不满足多态条件。
// 这⾥就是静态绑定,编译器直接确定调用函数地址
ptr->BuyTicket();
00EA2C91 mov ecx, dword ptr[ptr]
00EA2C94 call Student::Student(0EA153Ch)

函数的调用与对象本身相绑定(动态绑定),而不与其符号表相绑定(静态绑定),这也就解释了为什么多态的构成需要用指针或引用来调用虚函数,而不能用对象(会发生拷贝,虚表会根据类型变化,从而有可能错调函数)。

3.3 虚函数表的具体说明

• 父类对象的虚函数表中存放父类所有虚函数的地址。

父类的虚函数放到父类的虚表中;子类继承(拷贝)了父类的虚表,如果有新的虚函数则添加进去,如果重写了父类的虚函数则用子类虚函数的地址对其进行覆盖。

• 多继承时,包含虚函数的父类有几个就会有几个虚表,子类新增的虚函数地址放到第一张虚表后面。
• 虚函数表本质是⼀个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会在后面放个0x00000000标记,g++系列编译不会放)。

• 虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址存到了虚表中,虚表的指针又存到了对象中。

• 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下的代码可以对比验证一下。vs下是存在代码段(常量区)

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;
} 

运行结果:
栈 : 010FF954
静态区 : 0071D000
堆 : 0126D740
常量区 : 0071ABA4
Person虚表地址 : 0071AB44
Student虚表地址 : 0071AB84
虚函数地址 : 00711488
普通函数地址 : 007114BF

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

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

相关文章

MySQL中定义空值

如果一行中的某个列缺少数据值&#xff0c;该值被置为null&#xff0c;或者说包含一个空。 空是一个难以获得的、未分配的、未知的&#xff0c;或不适用的值。空和0或者空格不相同。0是一个数字&#xff0c;而空格是一个字符。 算术表达式中的空值 示例&#xff1a;计算年薪包…

CSS 布局技巧实现元素左右排列

开发中经常会遇到一个场景&#xff0c;使用 CSS 实现一个子元素靠右&#xff0c;其余子元素靠左。 这里总结一下常见的实现方式。 1. flex 布局 flexbox 是一种常用且灵活的布局方式&#xff0c;适合完成这种需求。将父容器设置为 display: flex&#xff0c;然后使用 margin…

Matlab Simulink 主时间步(major time step)、子时间步(minor time step)

高亮颜色说明&#xff1a;突出重点 个人觉得&#xff0c;&#xff1a;待核准个人观点是否有误 高亮颜色超链接 文章目录 对Simulink 时间步的理解Simulink 采样时间的类型Discrete Sample Times(离散采样时间)Controllable Sample Time(可控采样时间) Continuous Sample Times(…

51单片机-系列-单片机基础知识入门流水灯

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 单片机基础知识入门 常用的单片机封装 DIP直插 在DIP直插中&#xff0c;我们根据引脚数量的不同分为8P,14P,16P,18P,20P&#xff0c;这些是窄体&#xff0c;除了窄体之外&…

调用百度翻译API遇到的跨域问题解决方案

&#x1f389; 前言 这几天在学习前端的时候需要写一个实例&#xff0c;是关于翻译功能的。于是便想着在网上找一些API看能不能调用。这里遇到一个很坑的问题&#xff0c;就是我在暑假学习的时候曾经调用过心知天气的API、QQ音乐的API和今日头条的API&#xff0c;都未曾遇到过…

RT-DETR改进策略:BackBone改进|Swin Transformer,最强主干改进RT-DETR

摘要 在深度学习与计算机视觉领域,Swin Transformer作为一种强大的视觉Transformer架构,以其卓越的特征提取能力和自注意力机制,正逐步引领着图像识别与检测技术的革新。近期,我们成功地将Swin Transformer引入并深度整合至RT-DERT(一种高效的实时目标检测与识别框架)中…

BSV区块链上的覆盖网络服务现已开放公测

​​发表时间&#xff1a;2024年8月30日 BSV区块链的覆盖网络服务现已正式开放公测。对于BSV区块链生态系统内的特定交易类型和数据管理及访问&#xff0c;覆盖网络服务都可以为它们提供强大、可扩展、并且合规的解决方案。覆盖网络以及其它即将推出的BSV服务将赋予开发者、企业…

文件误删除?助你一键恢复

文件误删除之痛 在日常的数字生活中&#xff0c;文件误删除是许多用户不时会遭遇的“小确丧”。无论是手滑点击了“删除”键&#xff0c;还是系统崩溃导致的文件丢失&#xff0c;这些意外事件总能让人心急如焚。文件误删除不仅可能意味着重要资料的永久消失&#xff0c;还可能…

Linux驱动编程 - platform平台设备驱动总线

目录 简介&#xff1a; 一、初识platform平台设备驱动 1、platform_driver驱动代码框架 2、platform_device设备代码框架 3、测试结果 3.1 Makefile编译 3.2 加载驱动 二、platform框架分析 1、注册platform总线 1.1 创建platform平台总线函数调用流程 1.2 platform_b…

鸿蒙开发之ArkTS 基础三 数组

数组可以存储多个数据 语法为: let 数组名字:数组类型[] [数据一,数据二 ,数据三 ,数据四 ,数据5⃣️] 例如:学生类数组 let students:string[] [小美,小红,小张,小西] console.log("students",students) 输出 小美,小红,小张,小西 这里不需要遍历就能输出内容…

C Primer Plus 第5章习题

你该逆袭了 红色标注的是&#xff1a;错误的答案 蓝色标注的是&#xff1a;正确的答案 绿色标注的是&#xff1a;做题时有疑问的地方 橙色标注的是&#xff1a;答案中需要着重注意的地方 练习题 一、复习题1、2、3、4、错误答案&#xff1a;正确答案&#xff1a; 5、我的答案&a…

十三,Spring Boot 中注入 Servlet,Filter,Listener

十三&#xff0c;Spring Boot 中注入 Servlet&#xff0c;Filter&#xff0c;Listener 文章目录 十三&#xff0c;Spring Boot 中注入 Servlet&#xff0c;Filter&#xff0c;Listener1. 基本介绍2. 第一种方式&#xff1a;使用注解方式注入&#xff1a;Servlet&#xff0c;Fil…

Cobbler 搭建方法

统信服务器操作系统行业版V20-1000c【Cobbler 搭建】手册 统信服务器操作系统行业版 V20版本上Cobbler 搭建方法 文章目录 功能概述一、使用范围二、cobbler工作流程1. Server 端2. Client 端三、 环境准备1. 测试环境告知,以提供配置时参考:2. 关闭防火墙、selinux:3. 注意…

C#学习笔记(三)Visual Studio安装与使用

博主刚开始接触C#&#xff0c;本系列为学习记录&#xff0c;如有错误欢迎各位大佬指正&#xff01;期待互相交流&#xff01; 上一篇文章中安装了Visual Studio Code来编写调试C#程序&#xff0c;但是博主的目标是编写带窗口的应用程序&#xff0c;了解之后发现需要安装Visual …

python-素数对

题目描述 定义两个相差为 2 的素数称为素数对&#xff0c;如 5 和 7,17 和 19 等&#xff0c;要求找出所有两个数均不大于 n 的素数对。输入 一个正整数 n。1≤n≤10000。输出 所有小于等于 n 的素数对。每对素数对输出一行&#xff0c;中间用单个空格隔开。若没有找到任何素数…

VS2019配置TIFF

1.下载 Index of /libtiff/ (osgeo.org) 2.配置 3.使用 4.测试程序 #include <iostream> #include <cstdint> // 包含 stdint.h 头文件 #include "tiffio.h"int main() {std::cout << "Hello World!\n";// 打开一个 TIFF 文件const ch…

06_Python数据类型_元组

Python的基础数据类型 数值类型&#xff1a;整数、浮点数、复数、布尔字符串容器类型&#xff1a;列表、元祖、字典、集合 元组 元组&#xff08;Tuple&#xff09;是一种不可变的序列类型&#xff0c;与列表类似&#xff0c;但有一些关键的区别。本质&#xff1a;只读的列表…

java程序崩了不会看怎么办,那就用jconsole试试

性能监控工具 jconsole JConsole工具是JDK自带的图形化性能监控工具。并通过JConsole工具&#xff0c; 可以查看Java应用程序的运行概况&#xff0c; 监控堆信息、 元空间使用情况及类的加载情况等。 JConsole程序在%JAVA_HOM E%/bin目录下 或者你可以直接在命令行对他进行…

排序算法-交换排序

目录 基本思想 一、冒泡排序 二、快速排序分析 1. hoare版本 2. 挖坑法 3. 前后指针版本 4. 快速排序的优化 三、代码示例 1. hoare版本 2. 挖坑法 3. 前后指针版本 四、快速排序&#xff08;三路划分) 五、总结 基本思想 基本思想&#xff1a;所谓交换&#xff0…

VS Code终端命令执行后老是出现 __vsc_prompt_cmd_original: command not found

VS Code终端命令执行后老是出现 __vsc_prompt_cmd_original: command not found。 如下图&#xff08;vscode终端中&#xff09;&#xff1a; 解决方案&#xff1a; 1、vim ~/.bashrc 2、在~/.bashrc里面加入命令&#xff1a;unset PROMPT_COMMAND 3、source ~/.bashrc