c++多态(2)-- 虚函数

我们在多态(1)中说到,多态就是使用父类指针访问子类函数,可以使得代码更加的简便。并且举了一个喂食动物的例子加以说明,我们使用代码进行展示。

enum class _ANIMALS_TYPE {
	CAT,
	DOG,
	ANIMAL_COUNT
};

class Animal {
public:
	Animal(_ANIMALS_TYPE  type, int age);
	void eat()const;
private:
	_ANIMALS_TYPE  type;   // 动物类型
	int age;               // 动物年龄
};

class CAT : public Animal {
public:
	CAT(_ANIMALS_TYPE  type, int age);
	void eat()const;
};

class DOG : public Animal {
public:
	DOG(_ANIMALS_TYPE  type, int age);
	void eat()const;
};

/*
  喂养动物的时候,实现多态。  -->  使用父类指针(Animal指针)指向子类对象。(Cat和Dog)
*/
void feedAnimal(const Animal* animal) {
	// 调用动物吃的功能
	animal->eat();
}

int main(void) {
	CAT cat(_ANIMALS_TYPE::CAT,5);
	DOG dog(_ANIMALS_TYPE::DOG, 6);

	/* 传入子类对象指针 */
	feedAnimal(&cat);
	feedAnimal(&dog);

	system("pause");

	return 0;
}

Animal::Animal(_ANIMALS_TYPE type, int age)
{
	this->type = type;
	this->age = age;
}

void Animal::eat()const
{
	cout << "动物吃食物" << endl;
}

CAT::CAT(_ANIMALS_TYPE type, int age) : Animal(type,age)
{
}

void CAT::eat() const
{
	cout << "猫猫吃猫粮" << endl;
}

DOG::DOG(_ANIMALS_TYPE type, int age):Animal(type,age)
{
}

void DOG::eat() const
{
	cout << "狗狗吃狗粮" << endl;
}

代码分析:   

1.  代码中我们使用c++新增的枚举类型,表示我们养的动物是什么,上面养了猫和狗。 

2.  我们定义了两个类猫和狗,都继承了动物类,并且重写了吃的方法(吃自己想吃的食物)。

3.  然后我们可以定义了一个全局函数(也可以创建一个人类,封装这个喂食的方法),用来喂食我们所养的动物。

4.  我们使用了多态的思想,使用Animal类的指针作为参数,然后我们传入参数(相应动物类的对象),通过父类指针去访问子类中继承的方法。(此处就是使用animal访问dog和cat中重写的eat函数)。

问题:   

1.  虽然上面使用多态的特性合情合理,但是当我们运行代码的时候出现了问题,发现使用父类指针访问子类对象中重写的eat()方法时,会发现输出的时:  "动物吃食物",也就是说使用父类指针调用eat()方法时并没有调用子类中重写的函数,而是调用的父类中的方法。  

原因: 

上面的问题的原因是由于指针调用函数的机制造成的。 

指针调用函数的机制:    指针调用函数,是根据指针的类型进行调用的,就是Animal类的指针调用函数时,是调用Animal类中的函数,同理CAT和DOG类的指针,调用的是其类中的方法。

上面我们虽然使用父类指针指向了子类对象,而且在子类中重写了eat()函数,但是由于父类指针的类型是Animal类型的,调用eat()函数的时候,也是调用的是Animal类中的函数,所以会打印出"动物吃食物"。 

虚函数 

有上面的问题,多态就是空谈,c++使用虚函数解决了上面的问题。 

语法: 

在父类的函数声明(只在函数声明前加,函数定义前是不需要加的)中加上virtual,那么这个函数就是虚函数了。

enum class _ANIMALS_TYPE {
	CAT,
	DOG,
	ANIMAL_COUNT
};

class Animal {
public:
	Animal(_ANIMALS_TYPE  type, int age);
	virtual void eat()const;
private:
	_ANIMALS_TYPE  type;   // 动物类型
	int age;               // 动物年龄
};

class CAT : public Animal {
public:
	CAT(_ANIMALS_TYPE  type, int age);
	void eat()const;
};

class DOG : public Animal {
public:
	DOG(_ANIMALS_TYPE  type, int age);
	void eat()const;
};

/*
  喂养动物的时候,实现多态。  -->  使用父类指针(Animal指针)指向子类对象。(Cat和Dog)
*/
void feedAnimal(const Animal* animal) {
	// 调用动物吃的功能
	animal->eat();
}

int main(void) {
	CAT cat(_ANIMALS_TYPE::CAT,5);
	DOG dog(_ANIMALS_TYPE::DOG, 6);

	/* 传入子类对象指针 */
	feedAnimal(&cat);
	feedAnimal(&dog);

	system("pause");

	return 0;
}

Animal::Animal(_ANIMALS_TYPE type, int age)
{
	this->type = type;
	this->age = age;
}

void Animal::eat()const
{
	cout << "动物吃食物" << endl;
}

CAT::CAT(_ANIMALS_TYPE type, int age) : Animal(type,age)
{
}

void CAT::eat() const
{
	cout << "猫猫吃猫粮" << endl;
}

DOG::DOG(_ANIMALS_TYPE type, int age):Animal(type,age)
{
}

void DOG::eat() const
{
	cout << "狗狗吃狗粮" << endl;
}

 代码分析:  

1.  上面的代码和这里的代码是类似的,我们只是在此处代码上加上了virtual关键字,说明此时父类的eat()函数就是虚函数。 

2.   当我们加上virtual的时候,再去运行代码,输出就不一样了。输出: "猫猫吃猫粮","狗狗吃狗粮"。也就说明,我们在喂食的函数中,使用animal的指针访问的eat()函数,是其子类重写的函数。  

原因:  

1.   上面的例子就可以看出虚函数的存在让多态有了意义(可以使用父类指针指向子类继承的函数) 

2.   那为什么虚函数就能够让多态有意义呢? 

其实是,如果父类存在虚函数,那么编译器就会在对象的内存的开头添加一个虚指针(vptr),这个虚指针指向一个虚函数表(vtable),这个虚函数表中存放了所有虚函数的地址。(按照声明的顺序存放)

这样当我们使用指针访问函数的时候,编译器会找虚函数表中是否存在这个函数,如果存在就通过虚函数表的地址找到函数的位置(也就是调用虚函数表中的指针)。 

3. 当子类继承了存在虚函数的父类 

 我们知道,子类继承父类的时候,父类中的数据也会被继承,包括虚指针,并且虚函数表中的内容也会复制过来

如果不对父类中继承来的属性和核函数,那么内存就是这样的,但是当我们对父类继承来虚函数进行重写,那么就会发生改变,编译其会将虚函数表进行修改。

如图,我们在子类中重写了eat()方法,编译器就会自动修改虚函数表中的数据,原来从父类继承过来的虚函数表,地址是指向父类的eat()函数的,当我们在CAT类中重写了eat()函数时,编译器就会将对应函数修改成CAT类中eat()函数所在的地址。 

4. 怎样去调用? 

我们前面说到,指针调用函数是根据自己的类型调用类型中相符的函数,当加上虚函数之后,在指针访问虚函数的时候,会根据虚指针,查找要访问的虚函数的地址,根据虚函数表的地址来找到相应的函数。 

我们前面说过,使用父类指针指向子类对象是,可以理解为使用子类中继承了父类的成员中的数据去创建一个父类对象,其内部的值和子类中这些成员的值是一样的(虚指针也是)。在加上虚函数的时候,就又多拷贝一个虚指针(因为虚指针在最上面)。 

 当我们使用指针去访问虚函数的时候,自然就会去虚函数表中寻找函数的位置,但是此时虚函数表中的eat()函数的位置,已经被编译器修改成了子类的eat()函数的地址,所以这时候去调用eat()函数就是子类中重写的函数。

5. 使用工具查看类的内存模型 

前面说过:  项目右键 -> 属性 -> c/c++ -> 命令行 -> 写上/d1 reportSingleClassLayout类名,重新编译之后就能显示出类的内存模型来了。 

6. 子类继承以及重写

 父类的虚函数,子类继承之后就也是虚函数了。

在子类重写虚函数的时候,函数声明前可以加virtual也可以不加,因为加不加都是虚函数重写(继承过来就是虚函数) 

多继承的虚函数 

其实和单继承是类似的,只是根据继承的顺序,拷贝属性。 

如果,假设我们上面的CAT类又继承了一个Animal2的类,类内部有一个属性name,有一个虚函数virtual void play()const;

看上面的图,其实和前面的是类似的。 多继承了一个类的属性

纯虚函数和抽象类

纯虚函数 

纯虚函数就是当我们在父类实现虚函数时,并不会有太大的作用这样的话如果在父类实现的话就会白白占用资源(即使是空实现)。

就比如说,我们代码中的eat()函数,在Animal类实现的话其实没什么用,因为吃东西其实是针对具体的实际动物(例如: 猫和狗之类的),所以我们在猫类和狗类中去实现eat()函数更加有意义。

而且我们又希望实现多态,这时候我们就可以将父类的虚函数设置为纯虚函数,就可以了。 

 

上面的eat()函数就是纯虚函数,形如: virtual void eat() const = 0; 

如果把父类声明为纯虚函数,那么它的子类:
1)要么对纯虚函数进行完整的实现  (常用)
2)继续将其设置为纯虚函数
3)啥也不写(和(2)一样,但是写上更好) 

 抽象类

存在纯虚函数的类就是抽象类抽象类是不能定义对象的。 

 1. 子类继承和实现

如果父类中存在纯虚函数,那么子类继承过来这个函数也是纯虚函数,也就是说如果子类不对继承过来的纯虚函数进行实现,那么它也是一个抽象类,不能创建对象。

父类的纯虚函数被子类继承需要在子类中进行实现,否则子类也是一个抽象类。 

当然子类中也可以拥有自己的纯虚函数。 

总结:   

1.  使用虚函数时候才能真正的实现多态。 

2.  如果想要使用父类指针调用子类重写的函数,就需要在父类中的函数声明前加上virtual关键字。 

3.  只有在继承(父类和子类的关系)中,才能使用多态(父类指针指向子类对象,得有父类和子类) 。

4.  父类中存在虚函数,子类继承之后也是虚函数。而且,只有在子类中重写了虚函数之后,编译器才会去修改对应的虚函数表中的内容。 不然子类中的虚函数表就是复制父类中的。

5.  有纯虚函数的类称为抽象类,抽象类不能定义对象,同理,子类继承之后,也是纯虚函数。 需要进行实现。 

6. 简单来理解指针调用函数的情况,加上virtual指针指向谁(子类对象)就调用谁(对应类)中的方法不加virtual指针类型是什么,就调用哪个类中的方法。 

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

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

相关文章

2024.2.6

1.现有无序序列数组为23,24,12,5,33,5347&#xff0c;请使用以下排序实现编程 函数1:请使用冒泡排序实现升序排序 函数2:请使用简单选择排序实现升序排序 函数3:请使用快速排序实现升序排序 函数4:请使用插入排序实现升序排序 #include<stdio.h> #include<string.h&g…

Linux操作系统基础(一):操作系统概述

文章目录 操作系统概述 一、计算机分类 二、计算机组成 三、操作系统概述 四、操作系统分类 操作系统概述 一、计算机分类 计算机一般分为个人计算机&#xff08;笔记、台式机&#xff09;与 企业级服务器&#xff08;1U、2U、机柜、塔式、刀片&#xff09;两种形式。 二…

日本的便宜服务器有哪些?

年底之际&#xff0c;无非是云服务器优惠的黄金时期&#xff0c;对于个人用户和独立开发者来说&#xff0c;无论你是搭建个人网站还是个人博客&#xff0c;现在都是行动的好时机。那么&#xff0c;对于这时要入手日本服务器的用户&#xff0c;该怎么找便宜厂商呢&#xff1f;这…

Shell脚本系列| SSH分发公钥方法 - expect脚本的使用

ssh原理&#xff1a;在SSH安全协议的原理中&#xff0c; 是一种非对称加密与对称加密算法的结合。用于确保远程登录和其他网络服务的会话安全&#xff0c;通过非对称加密、会话加密、多重验证机制等手段&#xff0c;保护数据传输的机密性和完整性。 ssh登录有2种方法&#xff1…

P2957

题目描述 The cows enjoy mooing at the barn because their moos echo back, although sometimes not completely. Bessie, ever the excellent secretary, has been recording the exact wording of the moo as it goes out and returns. She is curious as to just how mu…

Qt Windows和Android使用MuPDF预览PDF文件

文章目录 1. Windows MuPDF编译2. Android MuPDF编译3. 引用 MuPDF 库4. 解析本地PDF文件 1. Windows MuPDF编译 使用如下命令将MuPDF的源码克隆到本地 git clone --recursive git://git.ghostscript.com/mupdf.git直接用VS&#xff0c;打开 mupdf/platform/win32/mupdf.sln …

基于Skywalking开发分布式监控(二)

续上篇&#xff0c;上一篇主要是讲了为啥选skywalking&#xff0c;以及怎么有针对性改造SW Agent&#xff0c;现在我们继续看看如何构建自定义Trace跟踪链 要对SW Agent插件做适当剪裁&#xff0c;原来包括customize插件在内SW 8.9有100多个插件&#xff0c;如果没有作用也就罢…

【网络技术】【Kali Linux】Nmap 嗅探(一)简单扫描

一、实验环境 本次实验进行简单的Nmap扫描&#xff0c;实验使用 Kali Linux 虚拟机和 Ubuntu Linux 虚拟机完成&#xff0c;主机操作系统为 Windows 11&#xff0c;虚拟化平台选择 Oracle VM VirtualBox&#xff0c;如下图所示。 二、实验步骤 1、相关配置 Kali Linux 虚拟机…

windows中的apache改成手动启动的操作步骤

使用cmd解决安装之后开机自启的问题 services.msc 0. 这个命令是打开本地服务找到apache的服务名称 2 .通过服务名称去查看服务的状态 sc query apacheapache3.附加上关掉和启动的命令&#xff08;换成是你的服务名称&#xff09; 关掉命令 sc stop apacheapache启动命令 …

ChatGPT 3.5与4.0:深入解析技术进步与性能提升的关键数据

大家好&#xff0c;欢迎来到我的博客&#xff01;今天我们将详细比较两个引人注目的ChatGPT版本——3.5和4.0&#xff0c;通过一些关键数据来深入解析它们之间的差异以及4.0版本的技术进步。 1. 模型规模与参数 ChatGPT 3.5&#xff1a; 参数数量&#xff1a;约1.7亿个模型层数…

07-Java桥接模式 ( Bridge Pattern )

Java桥接模式 摘要实现范例 桥接模式&#xff08;Bridge Pattern&#xff09;是用于把抽象化与实现化解耦&#xff0c;使得二者可以独立变化 桥接模式涉及到一个作为桥接的接口&#xff0c;使得实体类的功能独立于接口实现类&#xff0c;这两种类型的类可被结构化改变而互不影…

CSS之盒子模型

盒子模型 01-选择器 结构伪类选择器 基本使用 作用&#xff1a;根据元素的结构关系查找元素。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IE…

12 选择排序和堆排序

选择排序 基本思想 每一次从待排序的数据元素中选出最小(或最大)的一个元素&#xff0c;存放在序列的起始位置&#xff0c;直到全部待排序的数据元素排完 直接选择排序 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素若它不是这组元素的最后一个(第一个)元素…

relectron框架——打包前端vue3、react为pc端exe可执行程序

文章目录 ⭐前言⭐搭建Electron打包环境&#x1f496; npm镜像调整&#x1f496; 初始化项目&#x1f496; 配置index.js ⭐打包vue3⭐打包react⭐总结⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享关于使用electronjs打包前端vue3、react成exe可执行程序。…

【开源】JAVA+Vue+SpringBoot实现房屋出售出租系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 房屋销售模块2.2 房屋出租模块2.3 预定意向模块2.4 交易订单模块 三、系统展示四、核心代码4.1 查询房屋求租单4.2 查询卖家的房屋求购单4.3 出租意向预定4.4 出租单支付4.5 查询买家房屋销售交易单 五、免责说明 一、摘…

Vulnhub-Empire靶机-详细打靶流程

渗透思路 1.确认靶机IP地址2.端口服务扫描3.敏感目录扫描4.ffuf命令在这个目录下&#xff0c;继续使用ffuf工具扫描 5.ssh私钥爆破1.将私钥写进sh.txt中2.将私钥转换为可以被john爆破的形式3.通过John爆破 6.ssh私钥登陆7.icex64提权8.arsene提权 1.确认靶机IP地址 ┌──(roo…

【WebSocket】微信小程序原生组件使用SocketTask 调用星火认知大模型

直接上代码 微信开发者工具-调试器-终端-新建终端 进行依赖安装 npm install base-64 npm install crypto-js 然后顶部工具栏依次点击 工具-构建npm // index.js const defaultAvatarUrl https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQ…

android studio下开发flutter

文章目录 1. 配置环境 https://flutter.cn/docs/get-started/install2. android studio下开发flutter 1. 配置环境 https://flutter.cn/docs/get-started/install 2. android studio下开发flutter 打开Android Studio -> File -> Settings -> Plugins 搜索Dart插件 …

Golang 基础 Go Modules包管理

Golang 基础 Go Modules包管理 在 Go 项目开发中&#xff0c;依赖包管理是一个非常重要的内容&#xff0c;依赖包处理不好&#xff0c;就会导致编译失败&#xff0c;本文将系统介绍下 Go 的依赖包管理工具。 我会首先介绍下 Go 依赖包管理工具的历史&#xff0c;并详细介绍下…

第4章——深度学习入门(鱼书)

第4章 神经网络的学习 本章的主题是神经网络的学习。这里所说的“学习”是指从训练数据中自动获取最优权重参数的过程。本章中&#xff0c;为了使神经网络能进行学习&#xff0c;将导入损失函数这一指标。而学习的目的就是以该损失函数为基准&#xff0c;找出能使它的值达到最…