三大特性之多态

news2024/11/20 15:16:15

文章目录

    • 静态的多态
    • 动态的多态
      • 虚函数
        • 虚函数的重写(覆盖)
        • 利用虚函数重写实现多态
        • 重写的两个例外
          • 1.协变
          • 2.析构函数的函数名不同
        • C++11的override和final
      • 重载,重写(覆盖),重定义(隐藏)
      • 抽象类
      • 接口继承和实现继承
      • 多态实现的原理
        • 虚函数表
        • 虚函数表的存储位置
      • 一些关于多态的问题

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

静态的多态

调用同一个函数,产生不同效果的行为,这不就是函数重载吗!函数重载其实是一种静态的多态,相同的函数名传不同的参数调用的函数也就不同,但是调用哪个函数是在编译阶段就已经被确定好了。函数重载是一种编译时绑定,也就是静态绑定。常用的流插入和流体取也是一种函数重载

动态的多态

动态的多态才是本篇文章中要讲的主要内容,它在调用函数时与与类型无关而是与它所存放的对象有关(普通调用是按类型)。具体调用哪个函数运行时才知道,又叫运行时绑定,也就是动态绑定。

多态的构成必须要满足两个条件:

1.必须要通过父类的引用或者指针作为形参来调用

为什么一定要是父类的引用或者指针,对于这个问题《深度探索C++模型》中这样说:“一个pointer或一个reference之所以支持多态,是因为它们并不引发内存任何“与类型有关的内存委托操作; 会受到改变的。只有它们所指向内存的大小和解释方式 而已”。

对于上述话可以这样理解:

  1. 指针和引用类型只是要求了基地址和这种指针所指对象的内存大小,与对象的类型无关,相当于把指向的内存解释成指针或引用的类型。
  2. 如果直接把子类对象赋值给父类对象,就牵扯到内存模型,编译器就会回避虚机制,无法达到多态的效果。

2.被调用的函数必须是虚函数

虚函数

所谓的虚函数就是被virtual关键字所修饰的函数

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

虽然虚函数和虚继承都使用了virtual关键字,但是两者之间没有任何联系

虚函数的重写(覆盖)

如果子类中有和父类一样的虚函数(返回值类型,函数名称,参数列表相同),那么就称该子类的虚函数重写了父类的虚函数。

这里有一点需要注意:如果父类在声明的时候加了virtual,即使子类在声明同名函数时不加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();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

在这里插入图片描述

可以看到虽然我都是调用Func函数,但最后Func函数帮我调用到了不同的函数,这就是多态。

有了多态以后在调用函数的时候首先要看该函数是否构成多态,如果构成多态那么就不用考虑类型,只需要看该变量中存放的是何种对象,按照对象去调用函数;如果不构成多态,那么就只看类型,无论该变量中存的是何对象都不影响,只看类型调用(这里如果不是多态就都调用Person的函数)。

重写的两个例外

1.协变

子类对于父类函数的重写,返回类型可以不同,但必须要是返回父子类关系的指针或引用(即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用),称为协变

class Person 
{
public:
	virtual A* f() {return new A;}
};
class Student : public Person 
{
public:
	virtual B* f() {return new B;}
};
2.析构函数的函数名不同

析构函数的函数名要求必须要与类名相同,也就是说各个类的析构函数名都不同,但是其实编译器会将析构函数的函数名统一处理成destructor。

析构函数不但能重写,并且析构函数建议定义成虚函数

如果我定义了一个子类的对象,并将该子类对象赋值给一个父类的指针,当我释放父类的时候只会调用父类的析构函数,也就是说只释放了子类中父类的那一部分资源,而没有释放子类的资源,这就可能会导致内存泄漏。

如果我将析构函数定义为虚函数并重写,那么我在释放父类指针的时候,调用的是子类的析构函数,子类析构函数对于父类那一部分资源通过父类的析构函数清理,同时也会清理自己的资源。

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

public:
	int _a;
};

class Student :public Person
{
public:
    //virtual ~Student
	~Student()
	{
		cout << "~Student" << endl;
	}

public:
	int _b;
};

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

	delete p1;
	delete p2;

	return 0;
}

在这里插入图片描述

C++11的override和final

1.被final关键字修饰的函数不能被重写

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

在这里插入图片描述

2.使用override关键字检查该函数是否被重写,如果没有重写就报错

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

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

重载

1.要在同一个作用域中

2.函数名相同,参数列表相同,返回值可以不同

重写(覆盖)

1.两个函数分别在父类和子类的作用域中

2.返回值相同(协变除外),函数名相同,参数列表相同

3.只有虚函数才构成重写

重定义(隐藏)

1.两个函数分别在父类和子类的作用域中

2.函数名相同只要不构成重写就是重定义

抽象类

与虚函数对应的还有一个纯虚函数,只要在虚函数声明的最后加上=0那么这个虚函数就变成了纯虚函数。如果一个类包含纯虚函数,那么这个类就是抽象类。抽象类不能实例化对象,并且如果继承抽象类的子类不对纯虚函数进行重写的话,子类也是一个抽象类无法实例化对象。纯虚函数规范了子类必须重写,此外纯虚函数更体现了接口继承。

class A
{
public:
	virtual void test() = 0;
	int _a;
};

class B :public A
{
public:
	virtual void test()
	{
		cout << "重写了纯虚函数" << endl;
	}
	int _b;
};
int main()
{
	A a;
	B b;

	return 0;
}

在这里插入图片描述

接口继承和实现继承

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

多态实现的原理

首先我们来计算一下下面这个类的大小

class A
{
public:
    virtual void test();
}

按照我们类和对象阶段所说,一个没有成员变量的类就是空类,空类的大小为1字节,用来占位。

但这个类的大小为4字节

在这里插入图片描述

这是因为如果一个类中有虚函数,那么该类中会有一个隐藏的指针,该指针指向一个虚函数表。

虚函数表

在这里插入图片描述

可以看到虚函数表中存放的是虚函数的地址,所谓虚函数的重写其实就是将重写过的虚函数的地址覆盖到原虚函数地址上。

在这里插入图片描述

因为每个类都有自己独立的虚函数表,所以不同的类对象就可以通过不同的虚函数表访问到不同的虚函数。

在vs下虚函数表都是以空结尾,但是Linux下就不是;

一个变量中如果存放的是子类的对象,那么该变量中的前四个字节就是子类所对应的虚函数表,该表中存放的是子类所对应的虚函数。如果放的是父类的对象,那么该变量的前四个字节就是父类的虚函数表,其中存放的也就是父类所对应虚函数的地址。多态的产生也就是根据对象的前四个字节找到虚函数表,调用其中的虚函数实现的。

虚函数表的存储位置

此外虚函数表存储在常量区中(代码段),通过下面的代码可以看到各个区域的地址,虚表的地址和哪个最为接近就可以说明虚表在哪个区域,经观察确实是在代码段上。

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

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

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	int a = 0;
	cout << "栈:" << &a << endl;

	int* p1 = new int;
	cout << "堆:" << p1 << endl;

	const char* str = "hello world";
	cout << "代码段/常量区:" << (void*)str << endl;

	static int b = 0;
	cout << "静态区/数据段:" << &b << endl;
	Base be;
	cout << "虚表:" << (void*)*((int*)&be) << endl;

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

	Derive de;
	cout << "虚表:" << (void*)*((int*)&de) << endl;

	Base b1;
	Base b2;

	return 0;
}

补充

子类不但有从父类继承下来的成员,还有子类自己特有的成员,也就是说子类除了有重写父类的虚函数还有自己特有的虚函数。

对于单继承的类来说:子类特有的虚函数和重写的虚函数放在同一张虚函数表中

对于多继承的子类来说:它继承了多少个有虚函数的类就有几张虚函数表,但子类所特有的虚函数会放在第一张虚函数表中

一些关于多态的问题

  1. 什么是多态

答:多态就是多种形态,不同的对象来做同一件事产生不同效果的行为就可以称为多态。

  1. inline函数可以是虚函数吗?

答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。

  1. 静态成员可以是虚函数吗?

​ 答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

  1. 构造函数可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

  1. 对象访问普通函数快还是虚函数更快?

答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

  1. 虚函数表是在什么阶段生成的,存在哪的?

答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

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

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

相关文章

什么是客户旅程,为什么它很重要?

多年来&#xff0c;企业专注于客户接触点来衡量客户满意度。近年来&#xff0c;企业已经看到接触点并不能捕捉到全貌。客户接触点只能在一个时间点衡量满意度。他们不一定保证客户对他们的整个旅程感到满意。但什么是客户接触点&#xff1f;是客户与企业互动的特定时刻&#xf…

怎么格式化只读U盘?

用户在格式化期间遇到磁盘只读&#xff08;写保护&#xff09;问题是比较常见的。通常&#xff0c;如果你的U盘处于写保护状态&#xff0c;则它便是只读&#xff0c;不允许你删除或修改U盘上的任何文件&#xff0c;以保护U盘的数据安全。如果需要只读u盘怎么格式化的问题&#…

免费的SVG在线编辑器大推荐

随着响应网络的发展&#xff0c;越来越多的高质量的SVG在线编辑器被公众所熟知。SVG矢量图形也越来越受欢迎&#xff0c;以便在任何设备上呈现图像&#xff0c;甚至一些易于使用的SVG在线编辑器&#xff0c;可以替代PS&#xff0c;本文总结了五种流行的SVG在线编辑器。 1.即时…

使用Windbg静态分析dump文件的完整过程介绍

目录 1、概述 2、静态分析dump文件的一般步骤 2.1、查看异常类型 2.2、使用.ecxr命令切换到发生异常的线程上下文&#xff0c;查看发生异常的那条汇编指令 3、问题分析实例说明 4、使用Windbg详细分析dump文件&#xff0c;展现完整分析过程 4.1、查看异常类型和发生崩溃…

FreeRTOS:队列

目录 前言一、队列简介1.1数据存储1.2多任务访问1.3出队阻塞1.4入队阻塞1.5队列操作过程图示1.5.1创建队列1.5.2向队列发送第一个消息1.5.3向队列发送第二个消息1.5.4从队列中读取消息 二、队列结构体三、队列创建3.1创建函数3.2函数xQueueCreateStatic()3.3函数xQueueCreate()…

隐私计算论文合集「联邦学习系列」第2期

前言&#xff1a; 隐语awesome-PETs&#xff08;PETs即Privacy-Enhancing Technologies &#xff0c;隐私增强技术&#xff09;精选业内优秀论文&#xff0c;按技术类型进行整理分类&#xff0c;旨在为隐私计算领域的学习研究者提供一个高质量的学习交流社区。awesome-pets包含…

集权设施攻防兵法:实战攻防之AD篇

一、黑客眼中的AD AD域是攻击者经常攻击的目标&#xff0c;因为AD域作为企业的核心身份验证和授权系统&#xff0c;攻击AD域可以使攻击者获得系统内所有计算机和用户的权限&#xff0c;从而轻松获取敏感信息和控制企业系统。 另外&#xff0c;AD域内存在众多的计算机资产&…

使用git在Github上创建自己的项目及一些基础操作

使用git在Github上创建自己的项目及一些基础操作 一、什么是git Git 是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或小或大的项目。 Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 Git 与常用的版本控制工具…

Doris安装

Apache Doris 由百度大数据部研发&#xff08;之前叫百度 Palo&#xff0c;2018 年贡献到 Apache 社区后&#xff0c; 更名为 Doris &#xff09;&#xff0c;在百度内部&#xff0c;有超过 200 个产品线在使用&#xff0c;部署机器超过 1000 台&#xff0c;单一 业务最大可达…

Mac app文件签名与公证

一、Mac app文件签名与公证 在钥匙串中创建要公证app的profile&#xff08;公证的时候会用到&#xff09; xcrun notarytool store-credentials "mac_app" --apple-id "xxxxxxxx163.com" --team-id "S24Z9326XX" --password "fnjx-qjoe-l…

PYTHON制作前后端分离的图书信息管理系统(flask+vue)

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 本次案例&#xff0c;使用 flask、vue、elements-plus、axios 制作一个基于 restful api 的前后端分离的图书信息管理案例 效果展示 &#x1f447; &#x1f447; &#x1f447; 更多精彩机密、教程&#xff0c;尽在下…

Python 的 type 函数和 isinstance 函数

type()、isinstance()都是对象类型操作函数&#xff0c;用于判定对象类型&#xff0c;用哪个函数更好哩&#xff1f; 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&#xff0c;…

数字档案馆建设指南

数字档案馆建设指南 目 录 1.总体要求 2.管理系统功能要求 3.应用系统开发和服务平台构建 4.数字档案资源建设 5.保障体系建设 1.总体要求 1.1概述 数字档案馆是指各级各类档案馆为适应信息社会日益增长的对档案信息资源管理、利用需求&#xff0c;运用现代信息技术对数字…

一只羊的奥秘,您可知羊是有四个胃的噢

很多人知道反刍动物&#xff0c;但是对于反刍动物的四个胃怎样消化&#xff0c;不太了解&#xff0c;今天小编就介绍一下&#xff0c;反刍动物为何需要四个胃消化食物。 反刍俗称倒嚼&#xff0c;是指某些动物进食经过一段时间以后将半消化的食物从胃里返回嘴里再次咀嚼。反刍主…

有效项目进度管理的 10 条规则

项目进度管理是项目中比较关键的方面之一&#xff0c;因为它将决定事情的进展方式、进展速度以及是否会取得进展。换句话说&#xff0c;它可以让你较好地控制项目&#xff0c;帮助你预测不可预测的情况&#xff0c;并使所有相关团队能够高效地协同工作。 以下是有效项目进度管…

如何在华为OD机试中获得满分?Java实现【寻找两个正序数组的中位数】一文详解!

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: Java华为OD机试真题&#xff08;2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述…

3.数据结构期末复习之栈和队列

1.栈的应用 1.括号匹配问题(还有确定他们的符号优先级) 如 2((32) * 3) /3 扫描到左括号入栈,右括号入另外一个栈,如果两个栈数量相同,则是匹配的,不保存,不然去找最少的栈,出来提示报错2.十进制转2进制 如 23转二进制10111&#xff0c;需要栈辅助 每次除2的余数倒过来写3.mai…

Vue3(一):创建vue3工程、setup、vue3响应式原理、computed和watch

Vue3&#xff1a;第一章 一、创建Vue3.0工程1.使用vue-cli创建2.使用vite创建 二、Vue3中的响应式1.拉开序幕的setup2.ref函数3.reactive函数4.vue3中响应式的原理&#xff08;1&#xff09;vue2中响应式原理&#xff08;2&#xff09;Vue3中的Proxy 5.reactive和ref的对比6.se…

性能测试如何做?超详细性能测试-测试策略总结,新人进阶之路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能测试出现的初…

YOLOv5【训练train.py逐行源码及参数调参解析】超详细解读!!!建议收藏✨✨!

之前的文章介绍了YOLOv5的网络结构&#x1f680;与目录结构源码&#x1f680;以及detect.py&#x1f680;的详细解读&#xff0c;今天带来的是YOLOv5的 train.py 代码参数逐行解读以及注释&#xff0c;废话不多说&#xff0c;让我们一起学习YOLOv5的 train.py 源码吧&#xff0…