【C++修行之路】面向对象三大特性之多态

文章目录

  • 前言
  • 认识多态
  • 构成多态的必要条件
    • 虚函数的重写
    • 虚函数重写的两个例外
  • final和override
  • 重载、覆盖、隐藏
  • 抽象类
  • 多态的原理
    • 单继承
    • 多继承
      • 重写了基类的虚函数
      • 没有重写基类的虚函数
  • 菱形继承和菱形虚拟继承的虚表
  • 补充
  • 补充·继承与多态相关问题
    • inline函数可以是虚函数吗?
    • 静态成员函数可以是虚函数吗?
    • 构造函数、赋值重载可以是虚函数吗?
    • 析构函数可以是虚函数吗?
    • 内存问题
  • 结语

前言

大家好久不见,今天我们来一起学习一下c++中的多态。

认识多态

通过之前继承的学习,我们知道以下场景下,父类中的BuyTicket()会被子类中的隐藏,如果想要调用父类的,可以通过显示调用实现。

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

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

那如果要让其实现这样的一个场景:大人是全价,小孩就半价,应该怎么实现呢?

void show2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	show2(new Person);
	show2(new Student);
	return 0;
}

我们想要让这个函数实现,如果是父类对象就打印全价,如果是子类对象就打印半价,运行程序,得到如下结果:
在这里插入图片描述

显然这不是我们想要的效果,这是我们就要引入一个叫虚函数的概念了,在父类和子类函数成员前面都加上virtual关键字,这样使两个类的同名函数构成重写的关系

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

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

void show2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	show2(new Person);
	show2(new Student);
	return 0;
}

运行程序,得到了我们想要的效果:
在这里插入图片描述
这样的实现被称为多态。

构成多态的必要条件

构成多态有两个必要条件:
一、必须通过基类的指针或者引用来调用虚函数
二、被调用的必须是虚函数,并且派生类要对基类的虚函数进行重写

注意以上两个条件缺一不可,少哪一个都无法构成多态。

虚函数的重写

上面提到要对虚函数进行重写(覆盖):派生类有一个跟基类完全相同的虚函数(返回值类型、函数名字、参数列表),称子类虚函数重写了基类的虚函数。

虚函数继承相当于继承了基类函数的声明,在派生类中重写的是基类的内容,请看以下场景:
在这里插入图片描述
上述例子很好的证明了这个结论。

虚函数重写的两个例外

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

二、析构函数重写
基类的析构函数为虚函数,派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,编译器会对析构函数的名字做特殊处理,编译后的析构函数名称统一处理为destructor。

这样我们就理解为什么析构函数要定义为虚函数了,假如有以下场景:

class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
}

在调用delete函数释放p2指向的对象资源的时候,就构不成多态,这样才能保证p1、p2正确调用析构函数。

final和override

c++对函数重写的要求比较严格,有些情况下由于疏忽会导致类似字母次序写反而无法重载,并且编译阶段不会报错,只有在程序运行时未能得到预期结果才会报错,这样会大大降低程序开发效率,因此c++提供了override和final两个关键字。
一、
final修饰虚函数,被final修饰的虚函数不可以再被重写!
二、
override检测派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错!
演示如下:
在这里插入图片描述
在这里插入图片描述

重载、覆盖、隐藏

重载

  1. 两个函数在同一个作用域
  2. 函数名相同、参数不同

重写(覆盖)

  1. 两个函数分别在基类和派生类的作用域
  2. 函数名、参数、返回值必须相同(协变例外)
  3. 两个函数必须是虚函数

重定义(隐藏)

  1. 两个函数分别在基类和派生类的作用域
  2. 函数名相同
  3. 两个基类和派生类的同名函数若不重写就是隐藏

抽象类

在虚函数的后面写上 = 0 ,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例出对象,必须重写纯虚函数,派生类才可以实例出对象,纯虚函数规范了派生类必须重写,体现出了接口继承,演示如下:
在这里插入图片描述
注意:纯虚函数可以有函数体!!!

多态的原理

单继承

先来看一个问题:在32位操作系统中运行以下代码:

//运行这段代码,会得到什么结果?
#include <iostream>
using namespace std;
class Person {
public:
	void func();
protected:
	int _a1;
};

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

结果是4,因为整形大小为4个字节,将其中的函数变成虚函数后,再计算大小

//
#include <iostream>
using namespace std;
class Person {
public:
	virtual void func();
protected:
	int _a1;
};

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

这次得到的结果是8,这是为什么呢?为什么多出来了4个字节呢?

打开监视窗口,我们会发现:
在这里插入图片描述

Person实例画的对象里多出来一个指针,这个指针指向了一个指针数组。
在这里插入图片描述
事实上,这个指针也叫虚函数表指针

多态的原理可以用这张图片来简单表示:在这里插入图片描述
通过反汇编,我们可以得知满足多态以后的函数调用,不是在编译时确定的,而是在运行后在对象中寻找的。不满足多态的函数调用在编译时就确认好了。
在这里插入图片描述

多继承

假如这个类继承了多个类的时候,是怎么完成多态的呢?我们一起来分析一下:

重写了基类的虚函数

下面程序是如何实现多态的呢?

class A
{
public:
	virtual void func1()
	{
		cout << " hello  A" << endl;
	}
};
class B
{
public:
	virtual void func1()
	{
		cout << " hello  B" << endl;
	}
};

class C : public A, public B
{
public:
	virtual void func1()
	{
		cout << " hello C " << endl;
	}
protected:
	int _a1 =1;
};

int main()
{
	C c;
	A* a = &c;
	B* b = &c;

	a->func1();
	b->func1();
	return 0;
}

我们打开监视窗口,C类中包含A、B两张虚表,但是明明在C类中重写了继承下来的方法,但两个方法的地址却不一样?
在这里插入图片描述
使用反汇编来看一下到底是为什么?
在这里插入图片描述

我们发现在上述实现多态的过程中,调用的函数应该一致,但jmp的两个地址却不同,通过反汇编我们发现b在调用的时候使用了sub命令,然后调用了func1函数,所以,这里为什么要调用sub命令呢?其实,sub ecx 本质上是修正this指针的过程,因为A先被继承,因此a指针天然就是指向对象的起始位置,但b指针指向对象的中间位置,因此要修正this指针。
在这里插入图片描述

没有重写基类的虚函数

我们知道,虚函数的函数指针要保存到一张叫虚函数表的地方,那么 派生类中没有重写父类虚函数的虚函数(新定义的虚函数)存在哪张表里呢?

我们可以通过打印虚表的方式来看一下,调试窗口有时不会显示(我也不知道为什么)

先来说一下如何打印虚表,当我们实例化对象后,在这个对象里有一个虚表指针,它指向一个函数指针数组,我们要做的就是把数组内容打印出来

将函数指针类型重定义为VFPTR,那么这个虚表指针就是一个VFPTR* 类型的指针了,我们只要在这个对象里拿出这个指针即可,这里提供一种扩展性较强的方案:

在这里插入图片描述

运行下面的测试程序

typedef void(*VFPTR)();
void printVFTable(VFPTR table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("第%d个虚函数的地址:0x%x->", i, table[i]);
		VFPTR f = table[i];
		f();
		cout << endl;
	}
	cout << endl;
}

class A
{
public:
	virtual void func1()
	{
		cout << "hello A" << endl;
	}
};
class B
{
public:
	virtual void func1()
	{
		cout << "hello B" << endl;
	}
};

class C : public A, public B
{
public:
	virtual void func1()
	{
		cout << "hello C" << endl;
	}
	virtual void Cfunc()
	{
		cout << "C func" << endl;
	}
protected:
	int _a1 =1;
};

int main()
{

	C c;
	B* b = &c;
	printVFTable(*(VFPTR**)&c);
	printVFTable(*(VFPTR**)b);
	return 0;
}

在这里插入图片描述
可见,在派生类中新定义的虚函数其实是存在第一个继承对象的虚函数表里的。

菱形继承和菱形虚拟继承的虚表

太复杂了,占个位,以后有空再来补充,实际上非常不推荐设计出菱形继承。

补充

虚表是什么阶段生成的?
编译阶段

对象中虚表指针什么时候初始化?
构造函数的初始化列表

虚表存在哪里?

int main()
{
	
	int x = 0;
	static int y1 = 0;
	int* z1 = new int;
	const char* p1 = "xxxxxxxx";
	A a;
	static int y2 = 0;
	int* z2 = new int;
	const char* p2 = "xxxxxxxx";

	printf("栈对象:%d\n", &x);
	printf("静态区对象:%d\n", &y1);
	printf("静态区对象:%d\n", &y2);
	printf("堆对象:%d\n", &z1);
	printf("堆对象:%d\n", &z2);
	printf("常量区对象:%d\n", &p1);
	printf("常量区对象:%d\n", &p2);

	printf("虚表%d\n", *(VFPTR**)&a);

	return 0;
}

在这里插入图片描述
根据上述代码,再加上虚表是所有对象共用一份的数据,因此我们推测虚表应该在静态区。

补充·继承与多态相关问题

inline函数可以是虚函数吗?

可以,但是不要忘了inline只是给编译器一个建议,采不采纳这个建议取决于编译器,由于内联函数要在编译阶段就展开,不会进入符号表等,而虚函数地址要存放到虚表中,这个行为是在运行时进行的,所以编译器就会忽略掉inline属性。

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

虚函数的地址存放在虚表里,而static静态函数不被某一个对象拥有,而被一个类拥有,因此静态成员函数没有this指针,但虚表指针在每一个实例化的对象里,也就是说虚函数调用会使用this指针,所以说静态成员函数不会构成多态,也就不可以是虚函数。

构造函数、赋值重载可以是虚函数吗?

不可以,在对象创建时,编译器对虚表指针初始化,先让虚表指针指向父类的虚函数表,父类构造完后,子类的虚表指针再指向自己的虚函数表。

虚函数表是在构造函数的 初始化列表 初始化的!

构造时就调用虚函数,那么调用构造函数就要去找虚表指针,虚表指针在构造函数初始化列表才初始化,这是一个先有蛋还是先有鸡的问题

同样的道理,赋值重载可不可以设计为虚函数呢?
语法上是可以设置为虚函数的,但是极其不推荐这样做,我们回到虚函数的本质,我们设置虚函数本身就是为了重写,但对于赋值重载来讲,我们是想先赋值父类再赋值子类而不是多态的 只执行子类不执行父类。

析构函数可以是虚函数吗?

可以,非常推荐将析构函数设计为虚函数,上面我们也提到过,编译器会把两个类的析构函数统一处理为同名函数destructor() 这样我们在释放子类指针的时候,会隐藏父类的析构函数,我们没有办法完全释放空间,演示如下:

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

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

int main()
{
	Base* pd = new Derive;
	delete pd;
	return 0;
}

因为名字相同,默认构成了重写,基类的指针就会去调用基类的析构,这样是不符合预期的,因此我推荐将析构函数设计为虚函数,这样二者构成多态,此时就可以正确的释放空间了。

内存问题

运行下述代码报错:

class Base
{
public:
	void func1()
	{}
};

class Derive : public Base
{
public:
	virtual void func2(){}
};

int main()
{
	Base* ptr = new Derive;
	delete ptr;
	return 0;
}

其实原因就是在基类指针对子类切片的时候,父类没有虚表指针,但子类有虚表指针,这样会导致父类部分的前4/8个字节被非法访问了,所以在delete的时候会报错

结语

到这里,本篇文章就结束了,希望对你理解多态有所帮助,这篇文章花费了我大量时间,如果对你学习有所帮助,请给我一个三连+关注,我们下次再见。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT这么火,我们能怎么办?

今天打开百度&#xff0c;看到这样一条热搜高居榜二&#xff1a;B站UP主发起停更潮&#xff0c;然后点进去了解一看&#xff0c;大体是因为最近AI创作太火&#xff0c;对高质量原创形成了巨大冲击&#xff01;记得之前看过一位UP主的分享&#xff0c;说B站UP主的年收入大体约等…

iptables防火墙详解

文章目录一、iptables概念1、防火墙基础1.1 防火墙概念1.2 Netfilter和iptables的区别2、Iptables的表、链结构2.1 规则链2.2 规则表2.3 规则表之间的顺序3、规则3.1 匹配条件3.2 处理动作二、iptables规则管理1、iptables规则操作1.1 iptables信息查询1.2 规则添加1.3 规则删除…

Ant Design Vue的汉化

Ant Design Vue的汉化 1. 引入依赖 import zhCN from "ant-design-vue/lib/locale-provider/zh_CN"; // 汉化 export default {data () {zhCN,} }2. 标签包裹需要汉化的组件 <a-config-provider :locale"zhCN"><a-table :row-selection"ro…

C++IO流

文章目录一、CIO流体系二、C标准IO流三、C文件IO流1.ifstream2.ofstream一、CIO流体系 C流是指信息从外部输入设备向计算机内部输入&#xff0c;从内存向外部输出设备输出的过程&#xff0c;这种输入输出的过程非常形象地被称为流的概念。IO流指的就是输入输出流。 我们平时对…

PerfEnforce Demonstration: Data Analytics with Performance Guarantees

PerfEnforce Demonstration: Data Analytics with Performance Guarantees Created by: ctur Date: April 2, 2023 2:54 PM Status: ready to start 实时响应式的扩展算法 实时响应式的扩展算法分为 1. 比例积分控制 2. 强化学习 比例积分控制方法 “We use a proportiona…

现在的年轻人真会玩,开发界面都这么时尚,不服老都不行了

文章目录一、你还在用传统的开发界面吗二、年轻人的界面1.动漫型2.偶像型3.提神型三、更换背景的操作第一步第二步第三步一、你还在用传统的开发界面吗 不比不知道&#xff0c;一比吓一跳&#xff0c;都2023年了&#xff0c;你还在用Pycharm的默认背景写代码吗&#xff1f;已经…

不同版本的JDK新特性

1.JDK9&#xff1a;模块化开发 模块化功能用的不是很多 2.JDK10&#xff1a;var局部变量推导 使用var的两个基本要求&#xff1a; 也用得不是很多 3.JDK11 (1)单文件程序 就是能够直接用java命令编译.java文件了&#xff0c;跳过了使用javac命令的步骤&#xff0c;对新人…

4年功能测试月薪9.5K,3个月时间成功进阶自动化,跳槽涨薪6k后我的路还很长...

前言 其实最开始我并不是互联网从业者&#xff0c;是经历了一场六个月的培训才入的行&#xff0c;这个经历仿佛就是一个遮羞布&#xff0c;不能让任何人知道&#xff0c;就算有面试的时候被问到你是不是被培训的&#xff0c;我还是不能承认这段历史。我是为了生存&#xff0c;…

AD14安装步骤

首先 得需要科学安软件&#xff0c;我相信你可以找到的。 第一步&#xff1a;解压完成之后哦&#xff0c;双击打开AltiumDesignerSetup14_3_15.exe 第二步&#xff1a;点击next 第三步&#xff1a;先点击我同意&#xff0c;在点击next 第四步&#xff1a;点击next 第五步&…

路径 Dijkstra 蓝桥杯 JAVA

目录题目描述&#xff1a;Dijkstra 算法 (朴素版)&#xff1a;用Dijkstra解决本题&#xff1a;题目描述&#xff1a; 小蓝学习了最短路径之后特别高兴&#xff0c;他定义了一个特别的图&#xff0c;希望找到图中的最短路径。 小蓝的图由2021 个结点组成&#xff0c;依次编号1 至…

TypeScript由浅到深(上篇)

目录 一、什么是TypeScript有什么特点&#xff1a; 二、TypeScript的编译环境&#xff1a; 三、TypeScript数据类型&#xff1a; 01_标识符的类型推导&#xff1a; 02_JS中的类型Array&#xff1a; 03_JS 中的类型Object&#xff1a; 04_函数的类型&#xff1a; 05_匿名…

C++游戏分析与破解方法介绍

1、C游戏简介 目前手机游戏直接用C开发的已经不多&#xff0c;使用C开发的多是早期的基于cocos2dx的游戏&#xff0c;因此我们这里就以cocos2d-x为例讲解C游戏的分析与破解方法。 Cocos2d-x是一个移动端游戏开发框架&#xff0c;可以使用C或者lua进行开发&#xff0c;也可以混…

SpringBoot事件的选取原理

有四个事件启动监听器&#xff1a; 事件1会被监听吗&#xff1f;答案不会 容器发布一个正在启动的事件 org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners 遍历我们注册的监听器&#xff0c;但这里有个判断条件&#xff…

众人围剿,GPT-5招惹了谁

目录千人呼吁暂停AI训练代表人物分析反对原因分析信息安全人身安全失业利益总结GPT-4 火爆全球&#xff0c;引发了人工智能大浪潮。过去的一个月&#xff0c;OpenAI、微软、谷歌加上百度不断释放王炸&#xff0c;所有人都相信&#xff0c;AI 的就是未来的生产力。俗话说&#x…

Android动画进阶

在Android中&#xff0c;实现动画的方式通常有两种&#xff1a;视图动画和属性动画。然而这两种方式只能实现一些较为简单动画&#xff0c;仅仅通过改变这些控件属性的方式实现一些复杂的动画效果是比较有难度的&#xff0c;那么我们该如何实现复杂的动画。这里介绍两种实现方式…

Android配置Jetpack-Compose环境

Android 配置 Jetpack Compose 环境 记录配置Jetpack Compose环境的一些坑~ 本文同步更新于博客&#xff1a; 链接 直接创建kotlin项目或创建java项目后再配置均可 根目录 build.gradle 配置kotlin环境构建脚本 buildscript {ext.kotlin_version 1.4.32dependencies {clas…

大模型时代,AI模型开源还能这么玩?模型空间内测邀请(含重磅福利)

‍人工智能学习与实训社区飞桨 AI Studio自2019年以来&#xff0c;持续吸纳众多开发者于平台内开源贡献、实训提升&#xff0c;分享项目经验、共享自研模型等。 随着 AI Studio 开发者规模的增长、开发者开发能力的提升&#xff0c;我们收到许多期待与建议&#xff0c;经过一段…

企业OA管理系统需具备哪些功能?

OA也就是办公自动化&#xff0c;是通过将计算机、通信等现代化技术运用到传统办公方式而形成的一种新型办公方式。OA办公管理系统能够更加高效优质的处理办公事务以及进行企业管理业务&#xff0c;实现对资源的高效利用&#xff0c;进而达到提高生产力&#xff0c;提升管理水平…

详解vue各种权限控制与管理的实现思路

一、 菜单权限 菜单权限&#xff1a;控制用户在系统中能够看到哪些菜单项菜单权限指的就是后台系统中的左侧的菜单栏&#xff0c;前端可以根据后端接口返回的权限数据结合element-ui菜单组件循环拼接而成即可&#xff0c;有什么权限就展示什么菜单通过vuex持久化插件(本地存储…

Linux系统【centos7】常用基础命令教程

今天我来介绍一下Linux系统的基础知识。 首先&#xff0c;我们需要了解Linux是什么。Linux是一种免费且开放源代码的操作系统&#xff0c;它被广泛用于服务器、移动设备和嵌入式系统。 接下来&#xff0c;我们需要了解基本的Linux命令。其中一些基本命令包括&#xff1a; 1.…