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.rhkb.cn/news/386926.html

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

相关文章

打造重庆市数字化教育“新名片”,广阳湾珊瑚中学凭实力“出圈”!

分布于教学楼连廊顶部的智能照明设备,根据不同的时间和场景需求自动调节灯光亮度和开关状态;安装于各个教室内的智能黑板、学校同步时钟、学生互动设备,在极简以太全光网的赋能下,为师生提供丰富的教学体验与学习支持......行走于重庆市广阳湾珊瑚中学,像是与充满科技感的“校园…

病理AI领域的基础模型汇总|顶刊专题汇总·24-07-26

小罗碎碎念 本期文献主题&#xff1a;病理AI领域的最新基础模型 今天的推文是一期生日特辑&#xff0c;定时在下午六点二十一分发表&#xff08;今天农历六月二十一&#xff0c;哈哈&#xff09;&#xff0c;算是自己给自己的24岁生日礼物&#xff0c;希望24岁这一年&#xff0…

ollama本地部署大语言模型记录

目录 安装Ollama更改模型存放位置 拉取模型GemmaMistralQwen1.5(通义千问)codellama 部署Open webui测试性能知识广度问题1问题2 代码能力总结 最近突然对大语言模型感兴趣 同时在平时的一些线下断网的CTF比赛中&#xff0c;大语言模型也可以作为一个能对话交互的高级知识检索…

SSRF中伪协议学习

SSRF常用的伪协议 file:// 从文件系统中获取文件内容,如file:///etc/passwd dict:// 字典服务协议,访问字典资源,如 dict:///ip:6739/info: ftp:// 可用于网络端口扫描 sftp:// SSH文件传输协议或安全文件传输协议 ldap://轻量级目录访问协议 tftp:// 简单文件传输协议 gopher…

【JavaScript】函数声明和函数表达式的区别

文章目录 一、函数声明1. 定义方式2. 作用域提升&#xff08;Hoisting&#xff09;3. 块级作用域 二、函数表达式1. 定义方式2. 作用域提升&#xff08;Hoisting&#xff09;3. 自引用 三、其他区别1. 函数名2. 可读性和代码组织3. 使用场景 四、总结函数声明函数表达式 在Java…

【大模型系列】Video-LaVIT(2024.06)

Paper&#xff1a;https://arxiv.org/abs/2402.03161Github&#xff1a;https://video-lavit.github.io/Title&#xff1a;Video-LaVIT: Unified Video-Language Pre-training with Decoupled Visual-Motional TokenizationAuthor&#xff1a;Yang Jin&#xff0c; 北大&#x…

Java面试八股之@Qualifier的作用

Qualifier的作用 Qualifier 是 Spring 框架中的一个非常有用的注解&#xff0c;它主要用于解决在依赖注入过程中出现的歧义问题。当 Spring 容器中有多个相同类型的 Bean 时&#xff0c;Qualifier 可以帮助指明应该使用哪一个具体的 Bean 进行注入。 Qualifier 的作用&#x…

外设购物平台

目 录 一、系统分析 二、系统设计 2.1 系统功能设计 2.2 数据库设计 三、系统实现 3.1 注册功能 3.2 登录功能 3.3 分页查询所有商品信息功能 3.4 分页条件&#xff08;精确、模糊&#xff09;查询商品信息功能 3.5 购物车功能 3.6 订单管理功能 四、项…

【Opencv】模糊

消除噪声 用该像素周围的平均值代替该像素值 4个函数 blur():最经典的 import os import cv2 img cv2.imread(os.path.join(.,dog.jpg)) k_size 7 #窗口大小&#xff0c;数字越大&#xff0c;模糊越强 img_blur cv2.blur(img,(k_size,k_size)) #窗口是正方形&#xff…

云计算实训16——关于web,http协议,https协议,apache,nginx的学习与认知

一、web基本概念和常识 1.Web Web 服务是动态的、可交互的、跨平台的和图形化的为⽤户提供的⼀种在互联⽹上浏览信息的服务。 2.web服务器&#xff08;web server&#xff09; 也称HTTP服务器&#xff08;HTTP server&#xff09;&#xff0c;主要有 Nginx、Apache、Tomcat 等。…

C#使用csvhelper实现csv的操作

新建控制台项目 安装csvhelper 33.0.1 写入csv 新建Foo.cs namespace CsvSut02;public class Foo {public int Id { get; set; }public string Name { get; set; } }批量写入 using System.Globalization; using CsvHelper; using CsvHelper.Configuration;namespace Csv…

[数据集][目标检测]金属罐缺陷检测数据集VOC+YOLO格式8095张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;8095 标注数量(xml文件个数)&#xff1a;8095 标注数量(txt文件个数)&#xff1a;8095 标注…

使用Process Explorer和Dependency Walker排查dll动态库加载失败的问题

目录 1、问题描述 2、如何调试Release版本的代码&#xff1f; 3、使用Process Explorer查看exe主程序加载的dll库列表&#xff0c;发现mediaplay.dll没有加载起来 4、使用Dependency Walker查看rtcmpdll.dll的库依赖关系和接口调用情况&#xff0c;定位问题 4.1、使用Depe…

Javascript面试基础6【每日更新10】

Gulp gulp是前端开发过程中一种基于流的代码构建工具&#xff0c;是自动化项目的构建利器;它不仅能对网站资源进行优化&#xff0c;而且在开发过程中很多重复的任务能够使用正确的工具自动完成 Gulp的核心概念:流 流&#xff0c;简单来说就是建立在面向对象基础上的一种抽象的…

多微信聚合神器:高效沟通,一个界面全搞定!

大家都知道&#xff0c;频繁的来回切换微信&#xff0c;不仅浪费时间&#xff0c;还容易错过重要的信息。 今天&#xff0c;我要向大家推荐一款多微信管理神器——个微管理系统&#xff0c;助你实现统一管理&#xff0c;聚合聊天&#xff0c;让沟通变得更加高效。 1、网页扫码…

基于MindIE实现通义千问Qwen推理加速

一、昇腾开发者平台申请镜像 登录Ascend官网昇腾社区-官网丨昇腾万里 让智能无所不及 二、登录并下载mindie镜像 #登录docker login -u XXX#密码XXX#下载镜像docker pull XXX 三、下载Qwen的镜像 使用wget命令下载Qwen1.5-0.5B-Chat镜像&#xff0c;放在/mnt/Qwen/Qwen1.5-…

【无标题】Git(仓库,分支,分支冲突)

Git 一种分布式版本控制系统&#xff0c;用于跟踪和管理代码的变更 一&#xff0e;Git的主要功能&#xff1a; 二&#xff0e;准备git机器 修改静态ip&#xff0c;主机名 三&#xff0e;git仓库的建立&#xff1a; 1.安装git [rootgit ~]# yum -y install git 2.创建一个…

【策略工厂模式】记录策略工厂模式简单实现

策略工厂模式 1. 需求背景2. 代码实现2.1 定义基类接口2.2 排序策略接口定义2.3 定义抽象类&#xff0c;实现策略接口2.4 具体的排序策略实现类2.5 实现策略工厂类2.6 控制类 3. 启动测试4. 总结 1. 需求背景 现在需要你创建一个策略工厂类&#xff0c;来根据策略实现各种排序…

【JAVA】记录一次前端无能造成的 线上bug

有一个需求是 当方式切换 垫资时 清空 当前所选细单商品 但是前端的奇葩 操作是&#xff0c;只是在页面上清空 细单。 不请求 后台删除 细单 让前端 必须 清空同时 请求后台 删除细单 但是 该前端 技术不行&#xff0c; 嫌麻烦 不做 只好 后台 判断该类型时 进行删除操作…

AutoMQ 开源可观测性方案:夜莺 Flashcat

01 引言 在现代企业中&#xff0c;随着数据处理需求的不断增长&#xff0c;AutoMQ [1] 作为一种高效、低成本的流处理系统&#xff0c;逐渐成为企业实时数据处理的关键组件。然而&#xff0c;随着集群规模的扩大和业务复杂性的增加&#xff0c;确保 AutoMQ 集群的稳定性、高可…