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

news2024/11/16 21:33:47

目录

  • **前言**
  • **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.coloradmin.cn/o/1948939.html

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

相关文章

Tableau入门|数据可视化与仪表盘搭建

原视频链接&#xff08;up:戴戴戴师兄&#xff09;&#xff0c;文章为笔者的自学笔记&#xff0c;用于复习回顾&#xff0c;原视频下方有原up整理的笔记&#xff0c;更加直观便捷。因为视频中间涉及的细节较多&#xff0c;建议一边操作&#xff0c;一边学习。 整体介绍 可视化…

生成式AI:对话系统(Chat)与自主代理(Agent)的和谐共舞

生成式AI&#xff1a;对话与行动的和谐共舞 我们正站在一个令人激动的时代门槛上——生成式AI技术飞速发展&#xff0c;带来了无限的可能性。一个关键问题浮现&#xff1a;AI的未来是对话系统&#xff08;Chat&#xff09;的天下&#xff0c;还是自主代理&#xff08;Agent&am…

非凸T0算法,如何获取超额收益?

什么是非凸 T0 算法&#xff1f; 非凸 T0 算法基于投资者持有的股票持仓&#xff0c;利用机器学习等技术&#xff0c;短周期预测&#xff0c;全自动操作&#xff0c;抓取行情波动价差&#xff0c;增厚产品收益。通过开仓金额限制、持仓时长控制等&#xff0c;把控盈亏风险&…

【Ant Design Pro】快速上手

初始化 初始化脚手架&#xff1a;快速开始 官方默认使用 umi4&#xff0c;这里文档还没有及时更新&#xff08;不能像文档一样选择 umi 的版本&#xff09;&#xff0c;之后我选择 simple。 然后安装依赖。 在 package.json 中&#xff1a; "start": "cross-e…

java-数据结构与算法-02-数据结构-05-栈

文章目录 1. 栈1. 概述2. 链表实现3. 数组实现4. 应用 2. 习题E01. 有效的括号-Leetcode 20E02. 后缀表达式求值-Leetcode 120E03. 中缀表达式转后缀E04. 双栈模拟队列-Leetcode 232E05. 单队列模拟栈-Leetcode 225 1. 栈 1. 概述 计算机科学中&#xff0c;stack 是一种线性的…

学习Numpy的奇思妙想

学习Numpy的奇思妙想 本文主要想记录一下&#xff0c;学习 numpy 过程中的偶然的灵感&#xff0c;并记录一下知识框架。 推荐资源&#xff1a;https://numpy.org/doc/stable/user/absolute_beginners.html &#x1f4a1;灵感 为什么 numpy 数组的 shape 和 pytorch 是 tensor 是…

WordPress 后台开发技巧:向文章发布页右侧添加自定义菜单项

案例图片 这个案例向你介绍了如何在文章发布页的右侧边栏增加一个新的自定义菜单项。具体用它实现什么功能&#xff0c;就看你的需要了。 代码 function add_custom_menu_item() { add_meta_box(custom_menu_item, 这里是菜单项名称, display_custom_menu_item, post, side, …

昇思MindSpore学习入门-高阶自动微分

mindspore.ops模块提供的grad和value_and_grad接口可以生成网络模型的梯度。grad计算网络梯度&#xff0c;value_and_grad同时计算网络的正向输出和梯度。本文主要介绍如何使用grad接口的主要功能&#xff0c;包括一阶、二阶求导&#xff0c;单独对输入或网络权重求导&#xff…

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙 文章目录 代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙17.太平洋大西洋水流问题一、DFS二、BFS三、本题总结 82…

解析capl文件生成XML Test Module对应的xml工具

之前一直用的CAPL Test Module来写代码&#xff0c;所有的控制都是在MainTest()函数来实现的&#xff0c;但是有一次&#xff0c;代码都写完了&#xff0c;突然需要用xml的这种方式来实现&#xff0c;很突然&#xff0c;之前也没研究过&#xff0c;整理这个xml整的一身汗&#…

【1】CPU飙升到200%以上问题汇总

原链接 【1】CPU飙升到200%以上问题汇总 CPU飙升到200%以上是生成中常见的问题 注意&#xff1a; 1. linux的cpu使用频率是根据cpu个数和核数决定的 2. top&#xff0c;然后你按一下键盘的1&#xff0c;这就是单个核心的负载&#xff0c;不然是所有核心的负载相加&#xff0c;…

Golang | 腾讯一面

go的调度 Golang的调度器采用M:N调度模型&#xff0c;其中M代表用户级别的线程(也就是goroutine)&#xff0c;而N代表的事内核级别的线程。Go调度器的主要任务就是N个OS线程上调度M个goroutine。这种模型允许在少量的OS线程上运行大量的goroutine。 Go调度器使用了三种队列来…

基于STM32瑞士军刀--【FreeRTOS开发】学习笔记(二)|| 堆 / 栈

堆和栈 1. 堆 堆就是空闲的一块内存&#xff0c;可以通过malloc申请一小块内存&#xff0c;用完之后使用再free释放回去。管理堆需要用到链表操作。 比如需要分配100字节&#xff0c;实际所占108字节&#xff0c;因为为了方便后期的free&#xff0c;这一小块需要有个头部记录…

2024年7月25日(Git gitlab以及分支管理 )

分布式版本控制系统 一、Git概述 Git 是一种分布式版本控制系统,用于跟踪和管理代码的变更。它是由Linus Torvalds创建的,最 初被设计用于Linux内核的开发。Git允许开发人员跟踪和管理代码的版本,并且可以在不同的开 发人员之间进行协作。 Github 用的就是Git系统来管理它们的…

JVM面试题之内存区域、类加载篇

文章目录 引言JVM是什么&#xff1f;1. JVM内存划分2. 对象如何在JVM中创建2.1 内存分配2.2 创建对象步骤 3. JVM类加载流程3.1 双亲委派 总结 引言 Java开发人员在面试中基本都会被问到关于JVM的问题。想要成为高级的开发人员&#xff0c;了解和学习Java运行的原理和JVM是必不…

webpack插件给所有的:src文件目录增加前缀

1.webpack4的版本写法 class AddPrefixPlugin {apply(compiler) {compiler.hooks.compilation.tap(AddPrefixPlugin, (compilation) > {HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(AddPrefixPlugin,(data, cb) > {// 使用正则表达式替换所有包含 /st…

阿里云服务器安装Anaconda后无法检测到

前言 问题如标题所言&#xff0c;就是conda -V验证错误&#xff0c;不过后来发现其实就是虽然安装时&#xff0c;同意了写入环境变量&#xff0c;但是其实还没有写入&#xff0c;需要手动写入。下面也会重复一遍安装流程。 安装 到[Anaconda下载处](Download Now | Anaconda)查…

基于微信小程序+SpringBoot+Vue的流浪动物救助(带1w+文档)

基于微信小程序SpringBootVue的流浪动物救助(带1w文档) 基于微信小程序SpringBootVue的流浪动物救助(带1w文档) 本系统实现的目标是使爱心人士都可以加入到流浪动物的救助工作中来。考虑到救助流浪动物的爱心人士文化水平不齐&#xff0c;所以本系统在设计时采用操作简单、界面…

通过IEC104转MQTT网关对接阿里云、华为云、亚马逊AWS、ThingsBoard、Ignition、Zabbix

随着工业互联网的快速发展&#xff0c;传统电力系统中的IEC 104协议设备正逐步向更加开放、灵活的物联网架构转型。MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;作为一种轻量级的消息传输协议&#xff0c;因其低带宽消耗、高可靠性和广泛的支持性&#xf…