c++如何理解多态与虚函数

目录

  • **前言**
  • **1. 何为多态**
    • 1.1 **编译时多态**
      • 1.1.1 函数重载
      • 1.1.2 模板
    • **1.2 运行时多态**
      • **1.2.1 虚函数**
      • **1.2.2 为什么要用父类指针去调用子类函数**
  • **2. 注意**
    • **2.1 基类的析构函数应写为虚函数**
    • **2.2 构造函数不能设为虚函数**
  • **本文参考**

前言

在学习 c++ 的虚函数这一块时,总有许多疑惑,诸如:

  • 多态有什么用?
  • 为何要用父类指针去调用子类函数?
  • 编译时多态与运行时多态有何区别?
  • … …

如果你跟我一样有这些疑惑,那么本文非常适合你。

  • 阅读本文之前你至少理解什么是 继承。
  • 本文从概念、语法层面讲解多态与虚函数,不会讲解在 c++ 中,它的底层是如何实现的。
  • 本文重点在解决上述几个问题,不会过多设计其 c++ 语法

1. 何为多态

多态,比较宽泛的定义为:

对于同一行为,不同的对象有不同的表现

比如 “买门票” :同样是买门票这一行为,但 普通人全价,学生半价,儿童免费。

将其定义放在程序中来看,相当于:同一函数,不同对象调用将返回不同结果。

说到这里,如果你没了解过 “运行时多态”,那么你可能第一反应是:函数重载。
没错,重载 也是多态的一种 ,它属于 编译时多态


1.1 编译时多态

在 c++ 中,“编译时”(静态)、“运行时”(动态)这两个词常常会被提起。

编译时多态,在编译时就能确定对象的行为,调用的是哪个函数。这通常通过 函数重载模板 等机制实现。

因为本文重点不在这里,所以编译时多态只是简单介绍

1.1.1 函数重载

在 C++ 中,编译器通过 函数签名 来区分不同的函数。

函数签名:由函数名称、参数列表(包括参数类型、参数顺序)组成。

也就是说,对于同名函数:

  • 如果仅仅是返回值类型不同,那么他们将被视为同一函数
  • 如果参数列表不同(包括参数类型、参数顺序),那么他们将被视为不同函数

1.1.2 模板

template <typename T>
void fun(T t);

那么在编译时,编译器就会推导出 T 的实际类型,使得模板实例化,生成相应的代码。

它允许程序员编写与类型无关的代码。


1.2 运行时多态

运行时多态性 允许程序在运行时根据对象的实际类型来调用相应的方法,而不是根据编译时引用的类型。

在 C++ 中,运行时多态常见于类的继承中:

通过父类的指针或引用,调用父类和子类中的同名函数时,根据所指向对象的类型,确定应调用哪个函数。

读完这句话,你可能有两个疑惑:

  1. 如何实现上述提到的运行时多态?(只是语法层面)
  2. 为什么要用父类的指针去调用子类的函数?直接通过对应的子类,自己调用自己的成员函数不行吗?

下面来一一解答:


1.2.1 虚函数

在一个类的成员函数前加上 virtual 关键字,那么这个函数被称为 虚函数,它能被子类重写,是实现运行时多态的重要手段。

  • 重写:在子类中定义一个与父类的虚函数名称相同的函数
  • 纯虚函数:只有声明,没有定义的虚函数,常在函数末尾加上 ‘= 0’ 来标识。它要求所有的子类都必须重写此方法
  • 有父类:
class Father
{
public:virtual void vfun() {  }  // 虚函数// virtual pvfun() = 0; -> 纯虚函数
};
  • 其子类为:
class Son1 : public Father
{
public:void vfun() { cout << "Son1::vfun()" << endl; }		// 重写了 Father::vfun()
};class Son2 : public Father
{
public:void vfun() { cout << "Son2::vfun()" << endl; }		// 重写了 Father::vfun()
};
  • 下面通过父类指针调用虚函数 vfun()

父类指针可以用子类指针初始化,反之不一定成立。具体原因与 c++ 对象内存布局 有关,这里不展开

int main()
{Father* f0 = new Father();Father* f1 = new Son1();Father* f2 = new Son2();f0->vfun();f1->vfun();f2->vfun();    return 0;
}
  • 运行程序:

在这里插入图片描述
可以看到,使用父类指针去调用虚函数,那么在运行时,可以根据指针所指的实际对象,调用对应的函数。也就是说,通过 virtual 关键字,我们实现了运行时多态。

倘若把 Father::vfun() 的 virtual 关键字去掉,那么运行结果为
在这里插入图片描述
对比来看,去掉 virtual 后,即便父类指针指向不同类型,但是调用的函数仍然是父类的函数。
因此,从这个结果来看,也证实了 virtual 是实现运行时多态的重要手段。

那么,它有何用?解决下面的问题,那么这个问题也迎刃而解。


1.2.2 为什么要用父类指针去调用子类函数

【以王者荣耀游戏为例】
王者荣耀是一款 5v5 竞技游戏,其中有许多英雄,每个英雄 (hero) 有自己的价格 (_price),当你买了某个英雄时 (buy),那么你的金币 (money) 将会减少对应的数量。

下面用程序简单模拟这个过程:
创建基类 Hero:有虚函数 buy(),其有四个派生类都重写了基类的虚函数buy():LiBai、HuaMuLan、HanXin、GuanYu
在这里插入图片描述

为了代码简洁,就不添加 _price 成员。

int your_money = 1000;class Hero 
{
public:virtual void buy() = 0;
};class LiBai : public Hero
{
public:void buy() { your_money -= 20; cout << "Buying LiBai" << endl; }
};class HuaMuLan : public Hero
{
public:void buy() { your_money -= 60; cout << "Buying HuaMuLan" << endl; }
};class HanXin : public Hero
{
public:void buy() { your_money -= 40; cout << "Buying Hanxin" << endl; }
};class GuanYu : public Hero
{
public:void buy() { your_money -= 70; cout << "Buying GuanYu" << endl; }
};

下面用一个全局方法来模拟买英雄这一行为,如果不采用父类指针,那么我们就需要多个重载函数:

void buy(LiBai* x) 	  { x->buy(); }
void buy(HuaMuLan* x) { x->buy(); }
void buy(HanXin* x)   { x->buy(); }
void buy(GuanYu* x)   { x->buy(); }

但是采用父类指针,只需要写一个:

void buy(Hero* x) { x->buy(); }

而且,倘若有一天出了新英雄 ChuangPu

class ChuangPu : public Hero
{
public:void buy() { your_money -= 1000; cout << "Buying ChuangPu" << endl; }
};

对于不采用父类指针的代码,除了添加上述代码,还需要加入函数:

void buy(ChuangPu* x) { x->buy(); }

但是采用父类指针的代码不需要修改全局函数 buy。

这还仅仅只是针对一个全局方法,倘若你的代码有许多类似的函数,那么修改代码的工作量很大

因此你也能看出:使用多态,能增加程序的可扩展性,即当程序需要修改或增加功能时,需要改动或增加的代码较少

说完这些,下面来看一些注意事项:


2. 注意

2.1 基类的析构函数应写为虚函数

我们知道,当一个对象的生命周期结束时,那么在回收这块内存时会先调用它的析构函数,以防内存泄漏。
现有如下的两个类:

Father
~Father()
Son
int* _s
Son(int)
~Son()

如果不将基类 Father 的虚构函数设为 虚函数:

class Father
{
public:~Father(){cout << "~Father()" << endl;}
};class Son : public Father
{
public:Son(int n) : _s{ new int(n) } { }~Son(){delete _s;cout << "~Son()" << endl;}private:int* _s;
};

现在通过父类指针,用子类初始化:

int main()
{Father* s = new Son(1);delete s;return 0;
}

那么程序运行结果为:
在这里插入图片描述
是的,子类的析构函数没有被调用。

这是由于 delete 操作内部调用了 s 的析构函数,但是 s 的类型为 Father*,并且其析构函数不是虚函数,因此只会调用父类的析构函数。具体原因与 c++ 虚函数的底层实现有关(虚函数表),本文不涉及

那么将父类的析构函数设为虚函数,在运行得:
在这里插入图片描述
子类的析构函数也调用了。


2.2 构造函数不能设为虚函数

在上面的例子中,倘若你将 Son 类的构造函数设为虚构函数,编译代码时会报错:
在这里插入图片描述
其原因之一在于:调用时机的问题。
构造函数是在对象被创建时调用的,当对象被创建成功后,内存分配了,它的类型才能被确定。
但虚函数的调用是在运行时根据对象的实际类型来确定的,而上面提到,对象类型的确定发生在构造函数被调用之后。
如果将构造函数设为虚函数,不就相当于创建对象后才能调用构造函数嘛。两者矛盾。

当然,更具体的原因还是涉及到虚函数的底层实现:虚函数表


本文参考

  1. C++ 一篇搞懂多态
  2. C++——来讲讲虚函数、虚继承、多态和虚函数表

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

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

相关文章

C++ | Leetcode C++题解之第275题H指数II

题目&#xff1a; 题解&#xff1a; class Solution { public:int hIndex(vector<int>& citations) {int n citations.size();int left 0, right n - 1;while (left < right) {int mid left (right - left) / 2;if (citations[mid] > n - mid) {right m…

全球“微软蓝屏”事件:IT基础设施韧性与安全性的考验

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

git配置环境变量

一.找到git安装目录 打开此git安装目录下的bin文件&#xff0c;复制此文件路径 二.配置环境变量 2.1 右键点击此电脑的属性栏 2.2 点击高级系统配置 2.3 点击环境变量 2.4 按图中步骤进行配置 三.配置完成 win r 输入cmd打开终端 终端页面中输入 git --version 如图所示…

20240725java的Controller、DAO、DO、Mapper、Service层、反射、AOP注解等内容的学习

在Java开发中&#xff0c;‌controller、‌dao、‌do、‌mapper等概念通常与MVC&#xff08;‌Model-View-Controller&#xff09;‌架构和分层设计相关。‌这些概念各自承担着不同的职责&#xff0c;‌共同协作以构建和运行一个应用程序。‌以下是这些概念的解释&#xff1a;‌…

Redis的两种持久化方式---RDB、AOF

rdb其实就是一种快照持久化的方式&#xff0c;它会将Redis在某个时间点的所有的数据状态以二进制的方式保存到硬盘上的文件当中&#xff0c;它相对于aof文件会小很多&#xff0c;因为知识某个时间点的数据&#xff0c;当然&#xff0c;这就会导致它的实时性不够高&#xff0c;如…

【游戏制作】使用Python创建一个美观的贪吃蛇游戏,附完整代码

目录 前言 项目运行结果 项目简介 环境配置 代码实现 主体结构 主要功能详解 界面和菜单 控制蛇的移动 食物生成和碰撞检测 游戏结束 运行游戏 总结 前言 贪吃蛇游戏是一款经典的电脑游戏&#xff0c;许多人都曾经玩过。今天我们将使用Python和ttkbootstrap库来实…

Mysql注意事项(一)

Mysql注意事项&#xff08;一&#xff09; 最近回顾了一下MySQL&#xff0c;发现了一些MySQL需要注意的事项&#xff0c;同时也作为学习笔记&#xff0c;记录下来。–2020年05月13日 1、通配符* 检索所有的列。 不建议使用 通常&#xff0c;除非你确定需要表中的每个列&am…

51单片机-第四节-定时器

一、定时器&#xff1a; 1.介绍&#xff1a; 单片机内部实现的计时系统。 作用&#xff1a;代替长时间Daley&#xff0c;提高cpu效率。 数量&#xff1a;至少2个&#xff0c;T0&#xff0c;T1&#xff0c;T2等。其中T0&#xff0c;T1为所有51单片机共有&#xff0c;T2等为不…

爬虫提速!用Python实现多线程下载器!

✨ 内容&#xff1a; 在网络应用中&#xff0c;下载速度往往是用户体验的关键。多线程下载可以显著提升下载速度&#xff0c;通过将一个文件分成多个部分并行下载&#xff0c;可以更高效地利用带宽资源。今天&#xff0c;我们将通过一个实际案例&#xff0c;学习如何用Python实…

typecho仿某度响应式主题Xaink

新闻类型博客主题&#xff0c;简洁好看&#xff0c;适合资讯类、快讯类、新闻类博客建站&#xff0c;响应式设计&#xff0c;支持明亮和黑暗模式 直接下载 zip 源码->解压后移动到 Typecho 主题目录->改名为xaink->启用。 演示图&#xff1a; 下载链接&#xff1a; t…

【proteus经典项目实战】51单片机用计数器中断实现100以内的按键计数并播放音乐

一、简介 一个基于8051微控制器的计数器系统&#xff0c;该系统能够通过按键输入递增计数&#xff0c;并且能够在达到100时归零。该系统将使用计数器中断和外部中断来实现其功能。 51单片机因其简单易用和成本效益高&#xff0c;成为电子爱好者和学生的首选平台。通过编程单片…

最新风车IM即时聊天源码及完整视频教程2024年7月版

堡塔面板 试验性Centos/Ubuntu/Debian安装命令 独立运行环境&#xff08;py3.7&#xff09; 可能存在少量兼容性问题 不断优化中 curl -sSO http://io.bt.sy/install/install_panel.sh && bash install_panel.sh 1.宝塔环境如下: Nginx 1.20 Tomcat 8 MySQL 8.0 R…

构造+有序集合,CF 1023D - Array Restoration

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 1023D - Array Restoration 二、解题报告 1、思路分析 先考虑合法性检查&#xff1a; 对于数字x&#xff0c;其最左位置和最右位置 之间如果存在数字比x小&#xff0c;则非法 由于q次操作&#xff0c;第q…

GPT-4o mini:AI技术的平民化革命

目录 引言一、GPT-4o mini简介二、性能表现三、技术特点四、价格与市场定位五、应用场景六、安全性与可靠性七、未来展望八、代码示例结语 引言 在人工智能的浪潮中&#xff0c;大模型技术一直是研究和应用的热点。然而&#xff0c;高昂的成本和复杂的部署常常让许多企业和开发…

基于DMASM镜像的DMDSC共享存储集群部署

DMv8镜像模式共享存储集群部署 环境说明 操作系统&#xff1a;centos7.6 服务器&#xff1a;2台虚拟机 达梦数据库版本&#xff1a;达梦V8 安装前准备工作 参考文档《DM8共享存储集群》-第11、12章节 参考文档《DM8_Linux服务脚本使用手册》 1、系统环境(all nodes) 1…

学生党蓝牙耳机什么牌子的比较好?四大高性价比蓝牙耳机推荐

作为学生党&#xff0c;如果在有限的预算内选到一款合适自己的蓝牙耳机&#xff0c;那我们的生活和学习会增加很多的乐趣和便利&#xff0c;那面对市面上百元到千元不等的蓝牙耳机&#xff0c;学生党蓝牙耳机什么牌子的比较好&#xff1f;身为一名蓝牙耳机重度依赖者&#xff0…

AES算法分析:加密解密

✨主题简介 &#x1f510; 随着信息安全的重要性日益凸显&#xff0c;数据加密成为保护隐私和敏感信息的关键手段。本期我们将带你用Python实现AES加密解密&#xff0c;掌握这一强大的数据保护技术&#xff01; &#x1f4da;内容介绍 &#x1f50d; AES&#xff08;Advanced…

结构型设计模式-组合模式

一、组合模式 对于这个图片肯定会非常熟悉&#xff0c;上图我们可以看做是一个文件系统&#xff0c;对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树&#xff0c;当我们找到某个叶子节点后&#xff0c;就可以对叶子节点进行相关的操作。可以将…

开始尝试从0写一个项目--前端(三)

器材管理板块 添加器材管理导航 src\views\home\Home.vue src\router\index.js src\views\equipment\Equipment.vue <template><div>hello!</div></template> 测试 搜索导航分页查询 src\views\equipment\Equipment.vue <template><div&…

Discourse 备份和恢复中有关附件的问题

下面的这个问题是在官方论坛上网友问的内容是&#xff1a; 我想问一下&#xff0c;备份和附件分别挂载了不同的S3 备份的时候会把附件的S3里面的内容也都一起备份了吗&#xff1f;如果不选择包含上传的图片和附件&#xff0c;那么恢复备份的时候&#xff0c;附件用的S3里面的内…