C++面试冲刺笔记1:虚函数的基本工作原理

C++面试冲刺笔记1:虚函数的基本工作原理

前言

​ 笔者最近开始投简历,出于应对之后快速的面试流程需求,这里准备的是将常见的C++八股文进行总结,从而方便自己进行学习,检查和评估。

什么是虚函数

​ 虚函数,本质上还是函数,为什么是虚的呢?虚函数本质上是由 virtual 修饰的成员函数,但“虚”的真正含义指的是:它的调用行为并非在编译阶段决定,而是在运行阶段通过动态绑定机制决定。举一个例子感受一下:

class BaseClass {
public:virtual void aVirtualFunction() = 0; // 这是一个最极端的纯虚函数  
};class DerivedClass : public BaseClass {
public:void aVirtualFunction() override; // 这是子类的一个实现
};

​ 如果你熟悉,你就会说:很简单,当我们写上代码:

BaseClass* pSon = new DerivedClass();
pSon->aVirtualFunction();

​ 的时候,我们就会高兴的发现,尽管我们在书写的时候写的是BaseClass*,但是实际上,由于我们的赋值对象是BaseClass的派生类DerivedClass,这个时候,aVirtualFunction被瞧瞧的替换成了子类的实现而不是父类的实现。**这就是一种运行时的多态!**大类中的子类行为可以被共同表达为一个抽象的行为,而子类则各自为政,具象化这个抽象的父类行为(或者是覆盖父类的行为,这就是类的覆盖了,我们后面讨论类的时候慢慢聊)。

虚函数的实现本质

​ 我们现在聊一聊虚函数的运作本质是如何支持的。很简单,写过嵌入式C比较大型项目的朋友都知道,我们经常搞函数回调,也就是说,在运行的时候动态的跳转道给定的地址执行代码。在C++中,我们可以联想到——上述的这些本质上可以看作是类中的一个动态的存储着函数指针的成员变量,发生继承的时候,我们就把子类的指针赋值给我们的对象上,这样,发生调用的时候,我们就会直接调用这个函数指针指向的函数。很好!实际上大致的确如此,我们把这些潜在的虚函数排列成一张表格。也就是经典的虚函数表。虚函数表是用一个虚函数表指针指向的。所以说,指向虚函数表成员的大小就是一个指针的大小,这是毋庸置疑的。**这就是你调试一个带有虚函数的类的时候,你看到调试栏中的_vptr或者_vtable**对象了。

免责声明:

  • _vptr 是编译器生成的,名称和布局是实现相关的(如 MSVC 和 GCC 不一样),也就是说,不同的编译器对此的实现不一致,所以别乱搞Hack把代码的可移植性搞丢了
  • 不是标准定义的,因此依赖它做 portable 编程是不可取的,但了解它对理解原理很重要。

一些其他重要的事情

  • 派生类中重写时,可以省略 virtual,虚性自动继承。

  • 可以用 override 强制编译器检查函数签名是否匹配,避免拼写或参数错误

  • 纯虚函数=0)表示“子类必须重写”,容许定义抽象类,也就是无法实例化的接口 。这个小trick可以用在库设计中,强迫客户程序员重写父类的代码

  • 虚析构函数用来确保通过基类指针删除子类对象时,析构链能正确触发。所以,任何带有虚函数的类,都最好将自己的析构函数写上大大的virtual,否则会翻车。

    struct A {~A() { std::cout << "A析构\n"; }
    };
    struct B : A {~B() { std::cout << "B析构\n"; }
    };A* p = new B();
    delete p; // 只会调用A的析构!
    

    这个时候你需要做的是:

    struct A {virtual ~A() { std::cout << "A析构\n"; }
    };
    
  • 静态函数不能是虚函数:它不属于实例,没 this 指针,无法参与动态绑定

到这里就结束?No No No

​ 那可太没意思了兄弟们,大伙都知道我这个人的性子,写着点是没有啥值得说的,否则就成了博客灌水了。我们还有很多其他的话题。

多重继承下的虚函数表结构(主次 vtable)

​ 我们知道,C++是一个允许多重继承的语言,请看下图:

class HolyShitComplexClass : 	public BaseA, // is BaseApublic BaseB, // is BaseBpublic BaseC, // is Also BaseC
{...
};

​ 这个所谓的HolyShitComplexClass是三个类的子集,我们这个时候会问,欸!这么多的父类,我们的虚函数表怎么排列呢?答案是这样的,按照我们继承的顺序,依次排放我们的虚函数表指针,这也就是说,在多重继承中,每个基类子对象通常都有独立的 vptr 和对应的 vtable(主表与次表),使得各继承路径可独立动态绑定

[vptr_A][A_fields][vptr_B][B_fields][vptr_C][C_fields][Derived_fields]

​ 你看,这样排列的,我要是重写了A中的一个虚函数的行为,咱们就改A函数表中对应的函数指针指向,其他的如法炮制!

虚函数与非虚函数混合时的内存布局

​ 更多的时候,我们往往是类中同时含有虚函数和非虚数据成员,笔者的CCIMXDesktop项目中就有大量的这样的例子:

#ifndef APPCARDWIDGET_H
#define APPCARDWIDGET_H#include <QWidget>class DesktopToast;
class QLabel;
namespace Ui {
class AppCardWidget;
}/*** @brief AppCardWidget is a lightweight widget used to post messages to a DesktopToast.** This is an abstract base class representing an application card UI component.* It is responsible for handling pre-launch work and posting messages via toast notifications.*/
class AppCardWidget : public QWidget {Q_OBJECTpublic:Q_DISABLE_COPY(AppCardWidget);AppCardWidget() = delete;/*** @brief Constructs an AppCardWidget.* @param toast Pointer to the DesktopToast object used to show messages.* @param parent Optional parent widget.*/explicit AppCardWidget(DesktopToast* toast, QWidget* parent = nullptr);~AppCardWidget();/*** @brief Set the current icon for the app card.** This function allows derived classes to customize the app card icon* by providing a QPixmap.** @param icons The pixmap to be used as the icon.*/virtual void setCurrentIcon(const QPixmap& icons);/*** @brief Abstract method to invoke pre-launch operations.** Derived classes should implement this to perform necessary* preparations before the system starts or the app card becomes active.*/virtual void invoke_preLaunch_work() = 0;/*** @brief operate_comment_label*/virtual void operate_comment_label() = 0;/*** @brief invoke_textlabel_stylefresh*/void invoke_textlabel_stylefresh();protected:/*** @brief Abstract method to post messages to the bound DesktopToast.** Derived classes implement this to send notifications or status updates* through the toast system.*/virtual void postAppCardWidget() = 0;/*** @brief setHelperFunction: plainly set the text for shown* @param what*/virtual void setHelperFunction(const QString& what);virtual void setupSelfTextLabelStyle(QLabel* selfTextLabel) = 0;DesktopToast* binding_toast; ///< Pointer to the toast widget used for posting messages.Ui::AppCardWidget* ui; ///< UI object generated from the Qt Designer form.public:/*** @brief Event filter to handle user interaction events.* @param watched The QObject being watched.* @param event The event being filtered.* @return true if the event was handled, otherwise false.*/bool eventFilter(QObject* watched, QEvent* event) override;
};
#endif // APPCARDWIDGET_H

​ 你看,这个类中,我们就混合了虚函数和非虚函数,那么问题来了,我们的编译器有没有比较具体的排列呢?有的。

  • 通常 vptr 放在对象首部(首成员);
  • 紧接其后是非虚成员,保持自然对齐;
  • 所有虚函数都在单一 vtable 中按声明顺序排列 。
+---------------------+--------------------------------------------+
| 内存区域            | 内容描述                                   |
+=====================+============================================+
|      虚表指针       | 指向 AppCardWidget 的虚函数表               |
+---------------------+--------------------------------------------+
|                     |                                            |
|     成员变量        | DesktopToast* binding_toast                |
|                     | (绑定的 toast 对象指针)                     |
|                     +--------------------------------------------+
|                     | Ui::AppCardWidget* ui                      |
|                     | (UI 组件指针)                              |
+---------------------+--------------------------------------------+
|     继承部分        | QWidget 基类的所有成员数据                 |
+---------------------+--------------------------------------------+
|     Qt 特性         | Q_OBJECT 宏添加的元对象系统数据            |
|                     +--------------------------------------------+
|                     | Q_DISABLE_COPY 宏禁用拷贝功能              |
+---------------------+--------------------------------------------+虚表指针指向的是->
+-----+---------------------------------------------+-----------+
| 偏移 | 函数签名                                   | 类型      |
+=====+=============================================+===========+
|  0  | ~AppCardWidget()                           | 析构函数  |
+-----+---------------------------------------------+-----------+
|  1  | setCurrentIcon(const QPixmap&)             | 虚函数    |
+-----+---------------------------------------------+-----------+
|  2  | invoke_preLaunch_work()                    | 纯虚函数  |
+-----+---------------------------------------------+-----------+
|  3  | operate_comment_label()                    | 纯虚函数  |
+-----+---------------------------------------------+-----------+
|  4  | postAppCardWidget()                        | 纯虚函数  |
+-----+---------------------------------------------+-----------+
|  5  | setHelperFunction(const QString&)          | 虚函数    |
+-----+---------------------------------------------+-----------+
|  6  | setupSelfTextLabelStyle(QLabel*)           | 纯虚函数  |
+-----+---------------------------------------------+-----------+
|  7  | eventFilter(QObject*, QEvent*)             | 虚函数    |
+-----+---------------------------------------------+-----------+

CRTP 静态多态

​ 这个就谈不上虚函数的内容了,但是放在这里原因很简单,我们谈论到运行时多态的时候,必然要跟CRTP 静态多态做做对比。

​ 在很多时候,我们可能实际上,不太需要的是运行时多态,什么意思?

BaseClass* pSon = new DerivedClass();
pSon->aVirtualFunction();

​ 这个代码显然我们并不需要运行时多态,理由非常简单,因为我们知道pSon在这个逻辑流中指向的是一个具体的子类DerivedClass,而不是其他奇奇怪怪的东西。基于这个理念,我们发现一些场景中完全不需要所谓的运行时多态,我们在写代码的时候就预料到这里一定不会出现运行才能裁决的事情,啥叫运行时裁决呢?

void DesktopMainWindow::invoke_appcards_init() {/* sequencely invoke the work */showToast("AppCards Are Initing...");// each_app_cards是一个父类,这个父类表达这个对象是一个AppCards,但是具体是啥,如何提前派发初始化的工作,由子类自己的invoke_preLaunch_work裁决。for (const auto& each_app_cards : std::as_const(this->app_cards)) {each_app_cards->invoke_preLaunch_work();}showToast("AppCards Init Finished!");
}

​ 这个卡片可能实从用户提供的插件动态库,将来的我创建的,但是现在我完全不知道他们的具体的行为,但是我可以保证他们肯定至少是AppCards,这个时候就要到运行的时候检索类对象的元信息找出她到底是谁,然后调用具体的类对invoke_preLaunch_work的实现

​ 所以,CRTP是啥呢?答案是:Curiously Recurring Template Pattern,奇异递归模板模式。这个玩意的基本起手框架是这样的。。。

template <typename Derived>
class Base {
public:void interface() {// 调用派生类实现的方法static_cast<Derived*>(this)->implementation();}// 可提供默认实现(可选)void implementation() {std::cout << "Base implementation\n";}
};class Derived : public Base<Derived> {
public:void implementation() {std::cout << "Derived implementation\n";}
};

​ 这很有意思,我们实际上利用模板,在编译的时候就转发给了我们指定的编译器确定的Derived类。这就是CRTP。我们可以看到,这里在语法上,Base和Derived可以说是毫无关系,我们在代码编写的时候才会耦合到一起。

  • 这个可是和传统虚函数的动态多态不同,CRTP 在 编译期就完成了多态行为的分发(通过 static_cast 强转为派生类),没有虚函数表、没有间接跳转;
  • 不会因为虚函数造成 cache miss 或影响内联优化;
  • 可以实现“接口继承”或“行为注入”。

​ 所以CRTP在现代C++中还是有不少的身影的(我没用到,因为我评估我的项目是否可以有潜在的提升的时候发现真用不上)。

​ 感谢GPT,他给了我一个总结表格:

特性CRTP(静态多态)虚函数(动态多态)
多态分发时机编译期运行时
是否使用 vtable
性能开销无额外开销,可内联优化存在虚函数调用开销
类型灵活性编译期固定,类型必须已知支持运行时多态
扩展性模板层级难以抽象出接口更适合框架级接口

🧱 CRTP 限制和注意事项

​ CRTP是存在问题的,我们一个一个了解:

  • 编译复杂度增加:模板膨胀、编译错误难排查,我想大家都被该死的模板报错折磨过(好像新clang对模板的报错稍微友善了点?但是排查模板的报错属于是非常的不友好了)
  • 不可跨 DLL 边界使用:CRTP 依赖编译期展开,这就是因为它本身就是一个依赖模板的静态多态技术,属于是代价了。
  • 无法通过指针存储基类对象:除非用 Base<Derived>* 这样的具体类型;
  • 类型绑定固定:CRTP 的“接口”只能绑定一个特定派生类,缺乏动态扩展能力;

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

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

相关文章

Spring Boot 事务失效问题详解:原因、场景与解决方案

在 Spring Boot 开发中&#xff0c;事务管理是保证数据一致性和完整性的核心机制。然而&#xff0c;许多开发者在使用 Transactional 注解时&#xff0c;可能会遇到事务失效的问题&#xff0c;导致数据异常或业务逻辑错误。本文将深入分析 Spring Boot 中事务失效的常见原因&am…

Python-文件操作-StringIO和BytesIO-路径操作-shutil模块-csv,ini序列化和反序列化-argparse使用-学习笔记

序 欠4年前的一份学习笔记&#xff0c;献给今后的自己。 文件操作 冯诺依曼体系架构CPU由运算器和控制器组成 运算器&#xff0c;完成各种算数运算、逻辑运算、数据传输等数据加工处理 。 控制器&#xff0c;控制程序的执行 存储器&#xff0c;用于记忆程序和数据&#xff0c;例…

LLM的表征做减法的是什么,自然语言是一个矩阵,怎么进行减法的

LLM的表征做减法的是什么,自然语言是一个矩阵,怎么进行减法的 有个假设:就是最后一个词语融合了前面词语的信息 减法操作主要用于提取模型内部表征中的"诚实性"概念向量。具体来说,这是通过对比诚实和不诚实场景下的模型隐藏状态实现的。 import torch from t…

Java创建型模式---单例模式

单例模式基础概念单例模式是一种创建型设计模式&#xff0c;其核心思想是确保一个类仅有一个实例&#xff0c;并提供一个全局访问点来获取这个实例。在 Java 中实现单例模式主要有以下关键点&#xff1a;私有构造函数 - 防止外部通过new关键字创建实例静态实例变量 - 类内部持有…

详解Kafka重平衡机制详解

Kafka 的重平衡机制&#xff08;Rebalance&#xff09;是确保消费者组内成员动态变化&#xff08;如新成员加入、现有成员退出或崩溃、订阅主题分区数变化&#xff09;时&#xff0c;分区所有权能合理、公平地重新分配的核心机制。其目标是保证所有分区都有消费者处理&#xff…

代码详细注释:文件IO在用户管理系统中的应用实践:C语言实现用户名查重与密码确认与支持日志记录的终端用户认证解决方案的注册登录系统

代码/* 作业增强版注册登录系统 - 带日志和安全性增强功能 */ #include <stdio.h> // 标准输入输出函数(printf, scanf等) #include <stdlib.h> // 标准库函数(exit, malloc等) #include <string.h> // 字符串处理函数(strcmp, strcspn等) #inc…

Go与JS无缝协作:Goja引擎实战之错误处理最佳实践

引言&#xff1a;当Go邂逅JavaScript 在现代软件开发中&#xff0c;跨语言协作已成为提升效率的关键。想象一下&#xff1a;用Go的高性能处理核心逻辑&#xff0c;同时用JavaScript的灵活性实现动态规则——这不再是梦想。Goja&#xff0c;这个纯Go语言实现的JavaScript引擎&am…

继承与多态:面向对象编程的两大支柱

引言&#xff1a;为什么必须掌握继承与多态&#xff1f; 在Java开发中&#xff0c;继承与多态是构建可扩展、易维护系统的基石&#xff1a; 继承&#xff1a;实现代码复用&#xff0c;建立清晰的类层次结构多态&#xff1a;提升代码灵活性&#xff0c;实现"编写一次&#…

2025使用VM虚拟机安装配置Macos苹果系统下Flutter开发环境保姆级教程--上篇

前言 我们在学习Flutter开发的过程中&#xff0c;永远都跳不过去的一个问题就是如何在MAC下开发并打包Flutter工程项目&#xff0c;但MAC开发首先要解决的问题就是我们一般技术人员的电脑都是WINDOWS操作系统&#xff0c;专门配置一台MAC的话成本又是不得不考虑的因素&#xf…

250708-Svelte项目从Debian迁移到无法联网的RHEL全流程指南

&#x1f4cc; 背景 在 Debian 上使用以下命令创建了一个 Svelte 项目&#xff1a; npm install -g sv npx sv create my-svelte-demo cd my-svelte-demo npm install npm run dev现在需要将该项目迁移到一台 无法联网的 RHEL 9.4 服务器 上运行&#xff0c;出现如下报错&…

力扣 hot100 Day39

118. 杨辉三角 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 class Solution { public:vector<vector<int>> generate(int numRows) {vector<vector<int>> res(numRows);for (int i 0; i < numRows; i) {res[i].resi…

HuggingFists: 无代码处理复杂PDF

有过使用LLM搭建RAG或其它类知识系统的朋友一定会对文档数据的复杂多样性有着深刻的理解。各行各业的磁盘中都沉睡了数年到数十年的各类文档信息&#xff0c;包括&#xff1a;Doc、Docx、PPT、PDF、XLS、PNG、JPEG等各类格式。利用LLM激活这些数据价值的首要工作就是能够正确的…

Vue 3 框架实现理念、架构与设计哲学深度解析

第一部分&#xff1a;Vue 3 的起源&#xff1a;架构演进与设计哲学 Vue 3 的诞生并非一次简单的版本迭代&#xff0c;而是一场深刻的架构革命。它的出现是前端技术演进、应用规模扩张以及对更高性能和可维护性追求的必然结果。要全面理解 Vue 3 的各项实现理念&#xff0c;必须…

SQL Server使用存储过程导出数据到Excel实现方式

在SQL Server数据库管理中,存储过程作为预编译的T-SQL语句集合,能显著提升数据操作效率与安全性。将数据导出到Excel的需求广泛存在于报表生成、数据迁移等场景。本文详细解析四种通过存储过程实现数据导出的技术方案,涵盖代码实现、适用场景及优化策略,为不同业务需求提供…

OpenGL 2. 着色器

#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include <stdexcept>// 函数声明 void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow* window); void checkShaderCom…

【c++】容器扩容导致的类实例资源被错误释放

BUG记录 表现为新实例被存入前&#xff0c;容器内部的旧实例的析构被意外调用 因为 std::vector 在容量不足时&#xff0c;会自动扩容&#xff0c;把旧元素「搬」到新内存&#xff0c;然后析构旧内存上的那些对象。然后由于LKMotorController 类里没有正确处理移动语义&#xf…

TypeScript 集成

下面&#xff0c;我们来系统的梳理关于 Vue TypeScript 深度集成 的基本知识点&#xff1a;一、TypeScript 与 Vue 集成概述 1.1 为什么需要 TypeScript 类型安全&#xff1a;编译时类型检查&#xff0c;减少运行时错误代码智能&#xff1a;强大的IDE智能提示和自动补全可维护…

npm proxy

背景 前端项目下载依赖时经常会出现timeout的情况&#xff0c;此时有三种解决方案。 切换镜像源。 适用于对依赖版本要求不严格的情况。延长超时时间。设置npm proxy。一些生产环境对依赖版本有着严格要求&#xff0c;并且指定了依赖的下载地址&#xff08;如下图&#xff09;&…

TVS管工作原理是什么?主要的应用场景都有哪些?

什么是TVS管&#xff1f; TVS&#xff08;Transient Voltage Suppressors&#xff09;&#xff0c;即瞬态电压抑制器&#xff0c;也被称为雪崩击穿二极管&#xff0c;是一种二极管形式的高效能保护器件&#xff0c;常用来防止端口瞬间的电压冲击造成后级电路的损坏。 TVS 有单…

分布式微服务系统架构第156集:JavaPlus技术文档平台日更-Java线程池使用指南

title: java线程池使用 author: 哪吒 date: 2023-06-15点击勘误issues&#xff0c;哪吒感谢大家的阅读Java线程池使用指南1. 线程池基础使用1.1 创建线程池的方式方式一&#xff1a;使用Executors工具类&#xff08;不推荐&#xff09;// 1. 固定大小线程池 ExecutorService fi…