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指针类型是什么,就调用哪个类中的方法。 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

visual studio code could not establish connection to *: XHR failed

vscode远程连接服务器时&#xff0c;输入密码&#xff0c;又重新提示输入密码&#xff0c;就这样循环了好几次&#xff0c;然后会报上述的错误。由于我是window系统&#xff0c;我用cmd&#xff0c;然后ssh */你的IP地址/*发现可以远程到服务器上&#xff0c;但是通过Vscode就不…

泛型相关内容

1. 什么是泛型 泛型就是定义一种模板&#xff0c;既实现了编写一次&#xff0c;万能匹配&#xff0c;又通过编译器保证了类型安全。 2. 使用泛型 1&#xff09;使用泛型时&#xff0c;把泛型参数<T>替换为需要的class类型&#xff0c;不指定时默认为Obje…

PostgreSql与Postgis安装

POstgresql安装 1.登录官网 PostgreSQL: Linux downloads (Red Hat family) 2.选择版本 3.安装 ### 源 yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm ### 客户端 yum install postgresql14 ###…

【开源】基于JAVA+Vue+SpringBoot的停车场收费系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 停车位模块2.2 车辆模块2.3 停车收费模块2.4 IC卡模块2.5 IC卡挂失模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 停车场表3.2.2 车辆表3.2.3 停车收费表3.2.4 IC 卡表3.2.5 IC 卡挂失表 四、系统实现五、核心代码…

Vue中路由守卫的详细应用

作为一名web前端开发者&#xff0c;我们肯定经常使用Vue框架来构建我们的项目。而在Vue中&#xff0c;路由是非常重要的一部分&#xff0c;它能够实现页面的跳转和导航&#xff0c;提供更好的用户体验。然而&#xff0c;有时我们需要在路由跳转前或跳转后执行一些特定的逻辑&am…

《学成在线》微服务实战项目实操笔记系列(P1~P62)【上】

《学成在线》项目实操笔记系列【上】&#xff0c;跟视频的每一P对应&#xff0c;全系列12万字&#xff0c;涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳&#xff0c;参考这篇&#xff0c;相信会带给你极大启发。同时也欢迎大家提问与讨论&#xff0c;我会尽力帮大家解…

Python环境下基于辛几何模态分解的信号分解方法

基于辛几何的分析方法是一种保护相空间几何结构的新型分析方法&#xff0c;主要用于求解动力学和控制系统中矩阵或Hamilton矩阵的特征值问题&#xff0c;用来解决在动力学和控制系统理论的2n2n矩阵或哈密顿矩阵的特征值问题&#xff0c;已应用到结构损伤信号、奇异微分方程等系…

【前端web入门第五天】01 结构伪类选择器与伪元素选择器

文章目录: 1.结构伪类选择器 1.1 nth-child(公式) 2.伪元素选择器 1.结构伪类选择器 作用:根据元素的结构关系查找元素。 选择器说明E:first-child查找第一个E元素E:last-child查找最后一个E元素E:nth-child(N)查找第N个E元素&#xff08;第一个元素N值为1) 一个列表结构…

Linux学习笔记(centOS)—— 文件系统

目录 一、Linux中的文件 打开方式 二、目录结构​ 三、相关命令 切换目录命令 列出当前目录下的文件和目录命令 一、Linux中的文件 “万物皆文件。” 图1.1 所有文件 打开方式 图形化界面左上角的位置→计算机&#xff0c;打开以后就可以看到Linux全部的文件了&#xf…

Sping Cloud Hystrix 参数配置、简单使用、DashBoard

Sping Cloud Hystrix 文章目录 Sping Cloud Hystrix一、Hystrix 服务降级二、Hystrix使用示例三、OpenFeign Hystrix四、Hystrix参数HystrixCommand.Setter核心参数Command PropertiesFallback降级配置Circuit Breaker 熔断器配置Metrix 健康统计配置Request Context 相关参数C…

力扣精选算法100道—— 连续数组(前缀和专题)

连续数组&#xff08;前缀和专题&#xff09; 目录 &#x1f6a9;了解题意 &#x1f6a9;算法原理 ❗为什么hash设置成<0,-1>键值对 ❗与和为K的子数组比较hash的键值对 &#x1f6a9;代码实现 &#x1f6a9;了解题意 我们看到给定数组里面只有0和1&#xff0c;我们…

HarmonyOS 开发学习笔记

HarmonyOS 开发学习笔记 一、开发准备1.1、了解ArkTs语言1.2、TypeScript语法1.2.1、变量声明1.2.2、条件控制1.2.3、函数1.2.4、类和接口1.2.5、模块开发 1.3、快速入门 二、ArkUI组件2.1、Image组件2.2、Text文本显示组件2.3、TextInput文本输入框组件2.4、Button按钮组件2.5…