当前位置: 首页 > news >正文

C++虚函数操作指南

    • 1 什么是虚函数?
      • 1.1 虚函数的使用规则
      • 1.2 用 C++ 运行虚函数的示例
      • 1.3 协变式返回类型
    • 2 在 C++ 中使用虚函数的优点
      • 2.1 代码更为灵活、更为通用
      • 2.2 代码可复用
      • 2.3 契约式设计
    • 3 虚函数的局限性
      • 3.1 性能
      • 3.2 设计问题
      • 3.3 调试,容易出错
    • 4 虚函数的替代方案
      • 4.1 仅使用数据成员
      • 4.2 变体
      • 4.3 函数式编程
      • 4.4 静态多态
    • 5 用 Incredibuild 加速您的 C++ 虚函数
    • 6 总 结
      • 6.1 什么是 C++ 的虚函数?
      • 6.2 虚函数存在哪些问题?
      • 6.3 在 C++ 中,虚函数有何替代方案?

1 什么是虚函数?

虚函数是基类中声明的成员函数,且使用者期望在派生类中将其重新定义。那么,在 C++ 中,什么是虚函数呢?在 C++ 中,通常将虚函数用于实现运行时多态,该特性由 C++ 提供,适用于面向对象编程。我们将在下文更为详细地讨论运行时多态。不论函数调用所使用的指针或引用类型如何,虚函数最为重要的工作是确保函数调用正确。

1.1 虚函数的使用规则

C++ 虚函数必须遵循几个关键规则:

  • [✔] 在基类中使用 virtual 关键词来声明函数
  • [✔] 虚函数不能为静态函数
  • [✔] 为实现运行时多态,应使用指针或引用来访问虚拟函数
  • [✔] 对于基类和派生类而言,此类函数的原型应该相同(允许使用协变式返回类型,我们将在下文进行讨论)
  • [✔] 如果基类中含有虚函数,则应该使用虚拟析构函数,防止析构函数调用错误

1.2 用 C++ 运行虚函数的示例

虚函数在 C++ 中的运行情况:

class Pet {
public:virtual ~Pet() {}virtual void make_sound() const = 0;
};class Dog: public Pet {
public:virtual void make_sound() const override {std::cout << "raf raf\n";}
};class Cat: public Pet {
public:virtual void make_sound() const override {std::cout << "mewo\n";}
};int main() {Cat mitzi;Dog roxy;Pet *pets[] = {&mitzi, &roxy};for(auto pPet: pets) {pPet->make_sound();}
}

解释一下上述示例。
Pet 这是一个通用基类。但是我们仍然希望存在一个 make_sound 函数,这样,我们就能在不知道 pet 类型的情况下,在 pet 上调用 make_sound。仅在进行编译时,我们才能知道 pet 类型。因此,我们在基类中声明虚函数 make_sound,用 =0 来将其表示为由派生类实现的纯虚函数。
然后,再由 DogCat 来真正实现该函数。实现函数期间,我们添加关键词override,这样,编译器就能确保函数签名与基类中的签名相匹配。
main 中,我们可以在 Pet 指针上调用 make_sound,而无需在编译时知道该指针指向哪种 pet。我们会在运行时,根据实际存在的对象,实现所需函数。
我们必须要强调,这是一个非常简单的示例。我们也有其他解决方案应对这一简单示例(例如,为 pet’s sound 持有数据成员,并避免使用虚函数)。但我们想要展示虚函数的实现过程,因此不对其他解决方案进行额外展示。通常情况下,会使用虚函数为派生类中的不同行为建模,而相应行为不能用简单数据成员来建模。

1.3 协变式返回类型

我们提到过,若要实现虚函数,派生类函数的签名必须与基类中的签名相匹配。 唯一允许的区别是在返回类型上,只要派生类的返回类型是基类返回的派生类型即可。让我们看看下面的示例:

class PetFactory {
public:virtual ~PetFactory() {}virtual Pet* create() const = 0;
} class DogFactory: public PetFactory {
public:virtual Dog* create() const override {return new Dog();}
}; class CatFactory: public PetFactory {
public:virtual Cat* create() const override {return new Cat();}
};int main() {std::vector<Pet*> pets;DogFactory df;CatFactory cf;PetFactory* petFactory[] = {&df, &cf};for(auto factory: petFactory) {pets.push_back(factory->create());}for(auto pPet: pets) {pPet->make_sound();}for(auto pPet: pets) {delete pPet;}
}

在上述示例中,PetFactory 创建函数仅能知道它可以返回 Pet*,但使用协变式返回类型,DogFactoryCatFactory 则能知道更为具体的内容,这种虚函数的实现方式仍然行之有效。

2 在 C++ 中使用虚函数的优点

现在,如果您已经花费时间研究过 C++,可能会注意到,不需要由虚函数来重新定义派生类中的基函数。但存在这样的巨大区别,使得虚函数不可或缺:虚函数覆写基类函数,从而实现运行时多态。从本质上讲,多态指一个函数或对象以不同方式执行的能力,具体情况视使用方式而定。这属于面向对象编程的关键特性——结合其他众多特性,使得 C++ 作为编程语言而有别于 C 语言。

2.1 代码更为灵活、更为通用

这是贯穿所有多态程序的主要优点:根据运行时已知的调用对象,通过允许以不同方式执行函数调用,能使程序更为灵活而通用。如此一来,运行时多态便能从真正意义上使您的代码反映现实——特别是各场景中的对象(或人、动物、形状)并不总是以相同方式执行。

2.2 代码可复用

通过使用虚函数,我们可以将只应实现一次的通用操作和不同子类中可能有所不同的具体细节区分开来。试想以下示例:如果我们希望实现prism 类层次结构,则需要在各派生类中分别计算基面积,但可以使用派生类实现基面积计算,从而在基类中实现体积函数。实现代码如下:

class Prism {double height;
public:virtual ~Prism() {}virtual double baseArea() const = 0;double volume() const {return height * baseArea();}// ...
};class Cylinder: public Prism {double radius;
public:double baseArea() const override {return radius * radius * std::numbers::pi}// ...
};

2.3 契约式设计

术语“契约式设计”指如果代码设置有执行设计的契约,会比只通过文档来执行设计要好得多。虚函数,特别是纯虚函数,因其决定了在派生类中以不同方式重新实现特定操作的设计决策,可将其视为契约式设计工具。

3 虚函数的局限性

虚函数功能极为强大,但它们并非毫无缺点。开始使用虚函数前,您应该注意以下事项:

3.1 性能

无论是在运行时性能还是在内存方面,虚函数成本都要比普通函数高

内存部分通常冗余,取决于实现方式,但最为常见的是每个对象都有一个额外内部指针。这并不是什么大问题,除非我们有数以百万计的小对象,这些小对象的额外指针可能会引起内存问题。

函数的运行时性能成本不是一次跳转而是两次跳转,或者如果可以内联函数,性能成本就是两次跳转而不是零次跳转。虚函数需要跳转到虚函数表,再跳转到函数本身。这种额外跳转增加了 CPU 指令缓存中指令未准备就绪的概率,因此,这两次跳转并非唯一成本。

最后,如果您需要实现多态,与其他替代方案相比,性能方面的额外成本通常也在情理之中。然而,若要将第一个虚函数添加到类中,通常需要考虑额外成本。

3.2 设计问题

继承,特别是虚函数,会引起设计问题。继承层次结构设计糟糕可能会导致类膨胀和类之间关系异常。

从构造函数和析构函数调用虚函数的规则也会影响您的设计。从构造函数和析构函数调用的任何虚函数都不是多态函数,这样一来,有时需要将操作从构造函数转移到 init 虚拟函数。

为避免糟糕设计,应切记继承和多态并非是应对任何问题的最佳解决方案。

3.3 调试,容易出错

讽刺的是,虚函数面临的挑战之一是缺乏弹性

由于需要遵循调用流程,调试虚函数调用可能会变得稍显混乱。一般来说,遵循函数调用并不十分困难,但根据对象类型,在遵循隐藏调度方面,仍然需要进行额外工作。调试器会自行纠正错误,但决定断点位置可能会变得更加困难。

至于更容易出错,在某些情况下,不应调用虚函数的基类实现;而在某些情况下,应在开始时调用,有时也在结束时调用。由于忘记调用基类实现,或是在错误的地方、不需要的时候调用,使用虚函数极其容易出错。

可将其视为契约式设计工具。

4 虚函数的替代方案

4.1 仅使用数据成员

第一种替代方案是尝试并对基于简单数据成员的不同行为进行建模。如果不同类型的唯一区别是 sound,那就将其转换为数据成员,在构造时进行初始化,这样就没有问题了。但在许多情况下,行为更加复杂,需要不同的实现方式。

4.2 变体

另一种方案是使用 std::variantstd::visit,特别是待支持的不同类型已知,且列表不会太长时,二者可能相关。您可以点击此处和此处关于该方案的信息。

4.3 函数式编程

您可以传递待执行的操作,将其作为函数对象的 lambda,或者作为旧有 C 样式的函数指针,随后对其进行建模,而无需在类层次结构中对不同操作进行建模。通过该方法,您能将数据模型和可能想要执行的操作区分开来,这带来了极高灵活性。

4.4 静态多态

静态多态是一种基于模板的方法,用于获取多态动态,但基于编译时已知的实际想要使用的类型。例如,您可能希望代码同时支持 UDPConnectionTCPConnection,但在编译时,您可能想要知道使用 UDPConnectionTCPConnection 的具体流程。基于模板的静态多态可以实现更佳性能。
一些替代技术可能会导致项目编译时间变长。我们认为,特别是当您使用 Incredibuild 来加速构建时,这不会影响您的决策设计。首先选择合适的设计方案,然后使用正确工具来缩减编译时间即可。

5 用 Incredibuild 加速您的 C++ 虚函数

如果您想在不严重拖累编译速度和构建进程的情况下,从虚拟函数中受益,您就需要强大的计算能力作为后援。
Incredibuild 能够做到这一点。通过在虚拟机在本地网络上分配编译任务,Incredibuild 从根本上加快了 C++ 的编译速度。此外,Incredibuild 能与时下主流编译器和构建系统无缝集成,包括 Visual Studio、Qt CreatorClang
如此一来,虚函数便能具备极高的灵活性和效率,而无需花费时间来等待代码编译。首先选择合适的设计方案,然后使用正确工具来缩减编译时间即可。

6 总 结

6.1 什么是 C++ 的虚函数?

虚拟函数是基类中声明的成员函数,将在派生类中重新定义。在 C++ 中,使用虚函数来实现运行时多态。

6.2 虚函数存在哪些问题?

在运行时性能和内存使用方面,相比于普通函数,虚拟函数会造成更多影响。此外,虚拟函数会产生基于继承层次结构的设计问题,导致类膨胀和关系异常。最后,虚拟函数由于存在函数调用问题,往往难以进行调试。由于调用顺序的不可预测性,使用虚拟函数更容易引发错误。

6.3 在 C++ 中,虚函数有何替代方案?

是的,为了实现更好的设计或者是获得更佳的性能,您可能要考虑一些替代方案。但鉴于 C++ 程序员普遍使用虚函数,您应将其视为工具包内的一项工具,必要时加以使用。
如果您选择了另一种替代方案,比如基于模板的静态多态,切勿让较长的编译时间影响您的设计方案。确保选用合适的工具来加速构建进程,如果您没有使用 Incredibuild,请了解我们的解决方案,看看 Incredibuild 在减少编译时间方面可实现的惊人效果。

相关文章:

C++虚函数操作指南

1 什么是虚函数&#xff1f;1.1 虚函数的使用规则1.2 用 C 运行虚函数的示例1.3 协变式返回类型2 在 C 中使用虚函数的优点2.1 代码更为灵活、更为通用2.2 代码可复用2.3 契约式设计3 虚函数的局限性3.1 性能3.2 设计问题3.3 调试&#xff0c;容易出错4 虚函数的替代方案4.1 仅…...

Mybatis-Plus分页插件

引言&#xff1a;MyBatis Plus自带分页插件&#xff0c;只要简单的配置即可实现分页功能 1.添加Configuration配置类 Configuration MapperScan("com.atguigu.mybatisplus.mapper") //可以将主类中的注解移到此处public class MybatisPlusConfig {Beanpublic Mybatis…...

Selenium Webdriver options的实用参数设置

1、关闭Chrome浏览器受自动控制的提示 options.add_experimental_option(useAutomationExtension, False) options.add_experimental_option(excludeSwitches, [enable-automation])2、关闭是否保存密码的弹窗 options.add_experimental_option("prefs", { "c…...

代码随想录算法训练营第七天|454.四数相加II 、 383. 赎金信 、 15. 三数之和 、18. 四数之和

454.四数相加II 454.四数相加II介绍给你四个整数数组 nums1、nums2、nums3 和 nums4 &#xff0c;数组长度都是 n &#xff0c;请你计算有多少个元组 (i, j, k, l) 能满足&#xff1a;思路因为是存放在数组里不同位置的元素&#xff0c;因此不需要考虑去重的操作&#xff0c;而…...

详解抓包原理以及抓包工具whistle的用法

什么是抓包? 分析网络问题业务分析分析网络信息流通量网络大数据金融风险控制探测企图入侵网络的攻击探测由内部和外部的用户滥用网络资源探测网络入侵后的影响监测链接互联网宽频流量监测网络使用流量(包括内部用户&#xff0c;外部用户和系统)监测互联网和用户电脑的安全状…...

【C++】反向迭代器

文章目录一、什么是反向迭代器二、STL 源码中反向迭代器的实现三、reverse_iterator 的模拟实现四、vector 和 list 反向迭代器的实现一、什么是反向迭代器 C 中一共有四种迭代器 – iterator、const_iterator、reverse_iterator 以及 const_reverse_iterator&#xff0c;其中…...

(蓝桥真题)扫描游戏(计算几何+线段树二分)

题目链接&#xff1a;P8777 [蓝桥杯 2022 省 A] 扫描游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 样例输入&#xff1a; 5 2 0 1 1 0 3 2 4 3 5 6 8 1 -51 -33 2 样例输出&#xff1a; 1 1 3 4 -1 分析&#xff1a;先考虑如何对物件进行排序&#xff0c;首先&…...

面试官:什么是双亲委派模型?如何打破它?

本文已经收录进 JavaGuide(「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。) 参加过校招面试的同学,应该对这个问题不陌生。一般提问 JVM 知识点的时候,就会顺带问你双亲委派模型(别扭的翻译。。。)。 就算是不准备面试,学习双亲委派模型对于我…...

自建服务器系列- DDNS配置

1、环境说明 光猫桥接路由器拔号的模式 2、DDNS是什么 对于DHCP方式获得的IP&#xff0c;无论对于局域网内来说&#xff0c;还是外网来说&#xff0c;都会有使得IP地址每隔一段时间变化一次&#xff0c;如果想要通过恒定不变的地址访问主机&#xff0c;就需要动态域名解析。…...

vue中使用axios简单封装用法,axios报错the request was rejected because no multipart boundar

在这里插入代码片## 创建实例 //这个写法作为我错误的记录&#xff0c;可以不看暂时 transformRequest: [(data: any) > {if (!data) {data {}}return qs.stringify(data)}]在我的项目里面&#xff0c;初始化配置里面进行handers的修改&#xff0c;例如&#xff1a;例如将…...

Leetcode.1220 统计元音字母序列的数目

题目链接 Leetcode.1220 统计元音字母序列的数目 Rating &#xff1a; 1730 题目描述 给你一个整数 n&#xff0c;请你帮忙统计一下我们可以按下述规则形成多少个长度为 n的字符串&#xff1a; 字符串中的每个字符都应当是小写元音字母&#xff08;a, e, i, o, u&#xff09;…...

深入元空间

元空间是干嘛的&#xff1f;元空间存储的是类的相关信息&#xff0c;就是类的运行时表达。包括&#xff1a;Class文件类的结构和方法常量注解代码优化JDK1.8分界在1.8版本之前&#xff0c;类的meta信息、类变量、字符串常量池都存储在永久代。1.8版本以后&#xff0c;类变量、实…...

前端技术和框架

一、各种技术概述 1.HTML &#x1f9e8;HTML中文称为超文本标记语言&#xff0c;从语义上来说&#xff0c;它只是一种是一种标识性的语言&#xff0c;并不是一种编程语言。 <p>这是一段话</p>通过这个标签可以表示文本的一个段落。而且其中还有还有图片标签、视…...

02从零开始学Java之Java到底是个啥?

博主简介我是壹壹哥(孙玉昌)&#xff0c;十年软件开发授课经验&#xff0c;CSDN博客专家、阿里云专家博主、掘金优秀创作者、infoQ专家博主&#xff1b;关注壹壹哥(孙玉昌)&#xff0c;带你玩转Java&#xff0c;轻松实现从入门到放弃&#xff0c;哦不&#xff0c;到熟悉&#x…...

KEIL5中头文件路劲包含问题

方式1&#xff1a;1.Keil中添加头文件相对路劲的方法在c/c配置中添加路劲&#xff0c;最终是将添加的绝对路径转化为相对路径&#xff1b;注意&#xff1a;相对路径的当前位置指.uvproj文件所在位置在C/C配置中的include paths”中添加工程所用的所有头文件的路径&#xff1b;2…...

机智云目前我用过最便捷的物联网快速开发方案

GE211 MINI DTU上手来看&#xff0c;是一款尺寸比较小巧的模块&#xff0c;适合放置在几乎所有白色家电中&#xff0c;通过ph2.0端子&#xff08;注意不要买错&#xff09;引出了5v、gnd、tx、rx。可以说是非常方便了。下面正式开始我们的接入流程&#xff1a;首先注册一个机智…...

MySQL基础篇1

第1章 数据库介绍 1.1 数据库概述 什么是数据库&#xff1f; 数据库就是存储数据的仓库&#xff0c;其本质是一个文件系统&#xff0c;数据按照特定的格式将数据存储起来&#xff0c;用户可以对数据库中的数据进行增加&#xff0c;修改&#xff0c;删除及查询操作。 数据库分两…...

AQS 源码解读

一、AQS AQS 是 AbstractQueuedSynchronizer 的简称&#xff0c;又称为同步阻塞队列&#xff0c;是 Java 中的一个抽象类。在其内部维护了一个由双向链表实现的 FIFO 线程等待队列&#xff0c;同时又提供和维护了一个共享资源 state &#xff0c;像我们平常使用的 ReentrantLo…...

使用 DataLoader 加载数据报错‘expected sequence of length 4 at dim 1 (got 0)’

使用 transformer 将字符串转为 id 序列&#xff0c;字符串为中英文混杂形式&#xff0c; 运行中出现报错&#xff1a;expected sequence of length 4 at dim 1 (got 0) 发现是在encoder_plus转换时&#xff0c;将输入的文本根据max_length截断了&#xff0c;导致[MASK]等字段…...

第十四届蓝桥杯第三期模拟赛B组C/C++原题与详解

文章目录 一、填空题 1、1 找最小全字母十六进制数 1、1、1 题目描述 1、1、2 题解关键思路与解答 1、2 给列命名 1、2、1 题目描述 1、2、2 题解关键思路与解答 1、3 日期相等 1、3、1 题目描述 1、3、2 题解关键思路与解答 1、4 乘积方案数 1、4、1 题目描述 1、4、2 题解关…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

AI Agent与Agentic AI:原理、应用、挑战与未来展望

文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例&#xff1a;使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例&#xff1a;使用OpenAI GPT-3进…...

CentOS下的分布式内存计算Spark环境部署

一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架&#xff0c;相比 MapReduce 具有以下核心优势&#xff1a; 内存计算&#xff1a;数据可常驻内存&#xff0c;迭代计算性能提升 10-100 倍&#xff08;文档段落&#xff1a;3-79…...

Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器

第一章 引言&#xff1a;语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域&#xff0c;文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量&#xff0c;支撑着搜索引擎、推荐系统、…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明

AI 领域的快速发展正在催生一个新时代&#xff0c;智能代理&#xff08;agents&#xff09;不再是孤立的个体&#xff0c;而是能够像一个数字团队一样协作。然而&#xff0c;当前 AI 生态系统的碎片化阻碍了这一愿景的实现&#xff0c;导致了“AI 巴别塔问题”——不同代理之间…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)

🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

Python如何给视频添加音频和字幕

在Python中&#xff0c;给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加&#xff0c;包括必要的代码示例和详细解释。 环境准备 在开始之前&#xff0c;需要安装以下Python库&#xff1a;…...

Device Mapper 机制

Device Mapper 机制详解 Device Mapper&#xff08;简称 DM&#xff09;是 Linux 内核中的一套通用块设备映射框架&#xff0c;为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程&#xff0c;并配以详细的…...

Spring Security 认证流程——补充

一、认证流程概述 Spring Security 的认证流程基于 过滤器链&#xff08;Filter Chain&#xff09;&#xff0c;核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤&#xff1a; 用户提交登录请求拦…...