【c++进阶】再谈虚函数


关注我,学习c++不迷路:

个人主页:爱装代码的小瓶子
专栏如下:

  1. c++学习
  2. Linux学习

后续会更新更多有趣的小知识,关注我带你遨游知识世界

期待你的关注。


文章目录

  • 深入探索C++虚函数:从编译器视角看多态的“幕后魔法”
    • 1. 一个奇怪的现象:类的大小变了?
    • 2. 揭秘核心机制:虚函数表(vtable)与虚表指针(vptr)
      • 2.1 什么是虚函数表 (vtable)?
      • 2.2 什么是虚表指针 (vptr)?
      • 2.3 动态绑定的全过程
    • 3. 继承中的虚表变换(重写的本质)
    • 4. 经典面试题:为什么析构函数必须是虚函数?
      • 4.1 场景复现
      • 4.2 运行结果与分析
      • 4.3 修正方案
    • 5. 纯虚函数与抽象类
      • 特点对比表
    • 6. 避坑指南:那些关于虚函数的“反直觉”陷阱
      • 6.1 构造函数中调用虚函数
      • 6.2 默认参数是静态绑定的
    • # 总结

深入探索C++虚函数:从编译器视角看多态的“幕后魔法”

前言:在上一篇文章中,我们聊了C++多态的基本概念,知道了“同一接口,多种实现”的妙处。但你是否好奇过,编译器在底层到底做了什么手脚?为什么加了一个virtual关键字,程序就能在运行时“聪明”地找到正确的函数?
今天,小瓶子带大家钻进编译器的“肚子里”,不再只谈语法,而是从内存布局、虚表指针(vptr)和虚函数表(vtable)的角度,彻底搞懂虚函数的原理。这部分内容也是大厂面试中考察C++深度的重灾区哦!

1. 一个奇怪的现象:类的大小变了?

在深入原理之前,我们先看一段看似简单的代码。请大家猜猜,下面两个类的大小(sizeof)分别是多少?(假设在64位系统下)

#include<iostream>usingnamespacestd;// 这是一个普通的空类classA{public:voidfunc(){cout<<"A::func"<<endl;}};// 这是一个带虚函数的空类classB{public:virtualvoidfunc(){cout<<"B::func"<<endl;}};intmain(){cout<<"sizeof(A) = "<<sizeof(A)<<endl;cout<<"sizeof(B) = "<<sizeof(B)<<endl;return0;}

结果揭晓:

  • sizeof(A)=1
  • sizeof(B)=8(32位系统下是4)

为什么?

  • 类A虽然有成员函数,但成员函数是不占对象内存空间的(它们存在代码段)。空类为了占位,编译器会给它分配1字节。
  • 类B也是“空”的,只有一个函数,但因为加了virtual,编译器悄悄地在对象内部塞了一个指针

这个指针,就是我们今天的主角——虚表指针(Virtual Pointer, 简称 vptr)

2. 揭秘核心机制:虚函数表(vtable)与虚表指针(vptr)

对于初学者来说,这确实是理解C++多态最“劝退”的地方。但别急,我们把它们拆解开来看。

2.1 什么是虚函数表 (vtable)?

当编译器发现一个类中包含虚函数(或者是继承自包含虚函数的基类)时,它会为这个类(注意是类,不是对象)生成一张表。
这张表本质上是一个函数指针数组,数组里存放着该类所有虚函数的地址。

特性虚函数表 (vtable)
归属属于,该类的所有对象共享同一张表
存储位置通常在只读数据段(.rodata)或代码段
生成时间编译期确定
内容存放该类中实际有效的虚函数地址

2.2 什么是虚表指针 (vptr)?

既然类有了表,那对象怎么找到这张表呢?
编译器会在实例化对象时,在对象的内存布局的最前面(通常是开头,取决于编译器实现)自动添加一个指针,指向该类的虚函数表。这个指针就是vptr

内存布局示意图:

对象 obj 的内存布局: +----------------------+ | vptr (8 bytes) | ----------> +----------------------------+ +----------------------+ | Base::vtable | | member var 1 | +----------------------------+ +----------------------+ | [0] &Base::func1 | | member var 2 | | [1] &Base::func2 | +----------------------+ +----------------------------+

2.3 动态绑定的全过程

当我们使用基类指针调用虚函数时,编译器并没有像调用普通函数那样直接把函数地址写死(静态绑定),而是插入了一段“查表”的代码。

Base*ptr=newDerived();ptr->func();

这段代码在运行时经历了以下步骤(这是多态生效的关键!):

  1. 取指针:通过ptr找到对象的首地址。
  2. 找vptr:读取对象首地址处的vptr
  3. 查vtable:通过vptr找到该对象所属类(Derived)的vtable
  4. 调函数:从vtable中取出对应的函数地址(例如第0个位置),然后跳转执行。

这就是为什么多态叫“动态绑定”——直到程序运行起来,顺藤摸瓜找到了虚表,才知道具体执行哪个函数。

3. 继承中的虚表变换(重写的本质)

当我们进行派生时,编译器是如何处理这张表的呢?这里体现了override(重写)的本质。

假设我们有如下关系:

classBase{public:virtualvoidfunc1(){cout<<"Base::func1"<<endl;}virtualvoidfunc2(){cout<<"Base::func2"<<endl;}};classDerived:publicBase{public:// 重写了 func1virtualvoidfunc1(){cout<<"Derived::func1"<<endl;}// 没重写 func2};

编译器构建Derived的虚表时,遵循以下规则:

  1. 拷贝:先将基类Base的虚表内容拷贝一份过来。
  2. 覆盖:如果派生类重写了某个虚函数(如func1),则用派生类自己的函数地址覆盖掉表中原有的基类函数地址。
  3. 追加:如果派生类定义了新的虚函数,则将其地址添加到表的末尾。

小结:所谓的“重写”,在底层其实就是替换了虚表中对应位置的函数指针

4. 经典面试题:为什么析构函数必须是虚函数?

这绝对是面试中出现频率Top 3的问题!

4.1 场景复现

如果我们用基类指针指向派生类对象,然后delete这个指针:

classBase{public:// 注意:这里没有加 virtual~Base(){cout<<"Delete Base"<<endl;}};classDerived:publicBase{public:~Derived(){cout<<"Delete Derived"<<endl;}};intmain(){Base*p=newDerived();deletep;// 灾难发生了!return0;}

4.2 运行结果与分析

输出:

Delete Base

问题Derived的析构函数根本没执行!如果Derived里申请了堆内存,这里就造成了内存泄漏

原因:因为~Base()不是虚函数,编译器进行的是静态绑定。编译器看到pBase*类型,就直接生成了调用Base::~Base()的指令,完全不管它实际指向的是什么对象。

4.3 修正方案

只要将基类的析构函数加上virtual

virtual~Base(){cout<<"Delete Base"<<endl;}

此时delete p会变成动态绑定,通过vptr找到Derived的析构函数先执行,然后编译器会自动插入代码调用基类的析构函数,保证清理顺序正确(先子后父)。

金科玉律只要一个类可能被继承,且可能通过基类指针删除派生类对象,其析构函数必须声明为virtual

5. 纯虚函数与抽象类

有时候,基类并不知道该怎么实现某个函数(比如“图形”类的“计算面积”函数),这时候就可以使用纯虚函数

classShape{public:// 纯虚函数,也就是接口定义virtualdoublegetArea()=0;};

特点对比表

特性普通虚函数纯虚函数
语法virtual void f() {}virtual void f() = 0;
实例化类可以被实例化包含纯虚函数的类称为抽象类不能实例化
目的提供默认实现,允许覆盖强制派生类必须实现(提供接口规范)
虚表内容存放函数地址在某些编译器实现中,该位置可能存放nullptr或报错函数

6. 避坑指南:那些关于虚函数的“反直觉”陷阱

虽然虚函数很强大,但也有它无能为力,甚至会“坑”你的时候。

6.1 构造函数中调用虚函数

千万不要在构造函数或析构函数中调用虚函数!

classBase{public:Base(){func();// 危险!}virtualvoidfunc(){cout<<"Base::func"<<endl;}};classDerived:publicBase{public:virtualvoidfunc(){cout<<"Derived::func"<<endl;}};

当你执行Derived d;时,会先调用Base的构造函数。此时,Derived的部分还没有初始化,对象的类型在编译器眼里暂时还是Base
因此,Base构造函数里调用的func()永远是Base::func(),多态失效了。

6.2 默认参数是静态绑定的

classBase{public:virtualvoidshow(intx=10){cout<<"Base: "<<x<<endl;}};classDerived:publicBase{public:virtualvoidshow(intx=20){cout<<"Derived: "<<x<<endl;}};intmain(){Base*p=newDerived();p->show();// 猜猜输出什么?}

输出结果:Derived: 10

原因:这确实很反直觉!

  • 函数执行是动态的(调用了Derived的函数体)。
  • 默认参数是静态的(编译期根据指针类型Base*决定的)。
    所以你得到了一个“缝合怪”:Derived的逻辑,加上Base的默认参数。切记:绝不要重新定义继承而来的默认参数值。

# 总结

今天我们从编译器的角度重新审视了虚函数,是不是感觉清晰了很多?不再是死记硬背语法,而是理解了底层的流动。

这篇文章我们主要讲了:

  • 内存变化:包含虚函数的类会多出一个vptr指针,导致对象大小增加。
  • 底层机制:多态是通过vptr(查表指针)和vtable(函数地址表)配合实现的动态绑定。
  • 重要规则:基类析构函数必须是virtual,以防内存泄漏。
  • 避坑细节:构造函数中多态失效,以及默认参数静态绑定的陷阱。

C++的魅力就在于此,越钻研底层,越能体会到设计的精妙。如果你觉得这篇文章对你有帮助,别忘了三连支持一下小瓶子哦!


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

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

相关文章

I2C通信协议工业级设计要点:核心要点

工业级I2C通信设计实战&#xff1a;从信号完整性到系统容错的全链路优化 你有没有遇到过这样的场景&#xff1f; 一个本该稳定运行的工业传感器网络&#xff0c;突然开始频繁丢包&#xff1b;某台设备上的温度读数卡死不动&#xff0c;重启后又恢复正常&#xff1b;更糟的是&a…

Proteus 8.9环境搭建教程:全面讲解安装细节

从零搭建Proteus 8.9仿真环境&#xff1a;手把手带你避开每一个坑你是不是也曾在安装Proteus时被各种“找不到许可”、“服务无法启动”、“MCU不运行”的报错搞得焦头烂额&#xff1f;明明按照网上的教程一步步来&#xff0c;结果一打开软件就弹窗警告——别急&#xff0c;这并…

杰理芯片SDK开发-AD697N添加按键触摸提示音功能教程

前言 到现在为止也开发了许多杰理TWS蓝牙耳机项目SDK的案子&#xff0c;在调试案子时不断的向前辈们学习到了很多关于蓝牙TWS耳机专业的知识。想在这里做一个学习汇总&#xff0c;方便各位同行和对杰理芯片SDK感兴趣的小伙伴们学习&#xff1b; 本章详细讲解杰理AD697N芯片按键…

1.13草花互动面试

1. 浏览器输入网址到服务器的完整流程&#xff08;从 DNS 解析到页面渲染&#xff09;怎么答&#xff1a;“当我在浏览器输入一个网址&#xff08;比如 https://www.example.com&#xff09;并回车后&#xff0c;整个过程大致是这样的&#xff1a;DNS 解析&#xff1a;浏览器把…

Cortex-M ISR响应延迟优化完整示例

如何让 Cortex-M 的中断快到“无感”&#xff1f;——ISR 响应延迟优化实战全解析在嵌入式系统的世界里&#xff0c;“快”从来不是目的&#xff0c;而是生存的底线。你有没有遇到过这样的场景&#xff1a;电机控制环路突然失稳、音频播放咔哒作响、通信数据包莫名丢失……排查…

芯片验证工程师的写代码能力不是第一位

很多人以为验证工程师就是搭环境、跑仿真。但这只是表面工作。验证的核心在于发现问题&#xff0c;而不是证明设计正确。举个实际的例子&#xff1a;某个FIFO模块在正常读写测试下运行完美&#xff0c;覆盖率也达到了100%。但有个验证工程师在review代码时问了一句&#xff1a;…

IAR软件编译选项设置深度剖析与优化建议

深入IAR编译器&#xff1a;从配置到实战的性能调优全解析在嵌入式开发的世界里&#xff0c;一个常被忽视却至关重要的环节是——编译器不是“翻译机”&#xff0c;而是系统性能的塑造者。许多工程师习惯性地把代码写完后点击“Build”&#xff0c;看到绿色对勾就认为万事大吉。…

断言:让芯片设计工程师又爱又恨

断言(Assertion)&#xff0c;说白了&#xff0c;它就是设计工程师在代码里埋下的一个个”判断点”&#xff0c;时刻监控着信号是不是符合预期。什么是断言&#xff1f;举个最简单的例子&#xff1a;assert property ((posedge clk) (req |-> ##[1:2] ack));这段代码的意思是…

JFlash烧录固件的完整指南与调试技巧

JFlash烧录实战&#xff1a;从连接失败到量产自动化的深度通关指南你有没有遇到过这样的场景&#xff1f;凌晨两点&#xff0c;产线停摆&#xff0c;几十块板子卡在“Cannot connect to target”的报错界面上&#xff1b;又或者&#xff0c;明明烧录成功了&#xff0c;程序却死…

尾调用搞懂了,JS性能直接起飞?前端人别再被面试官问懵了!

尾调用搞懂了&#xff0c;JS性能直接起飞&#xff1f;前端人别再被面试官问懵了&#xff01;尾调用搞懂了&#xff0c;JS性能直接起飞&#xff1f;前端人别再被面试官问懵了&#xff01;为啥每次面试都被问“尾调用优化”&#xff1f;尾调用到底是个啥玩意儿手把手看代码&#…

程序员如何在技术变革中保持竞争力

程序员如何在技术变革中保持竞争力 关键词:程序员、技术变革、竞争力、持续学习、技能多元化 摘要:随着科技的飞速发展,技术变革日新月异,程序员面临着前所未有的挑战。本文旨在探讨程序员在技术变革中保持竞争力的有效方法。通过对背景的介绍,明确了文章的目的、读者群体…

FileMasterPro v1.2.5:全能多功能文件管理工具

FileMasterPro v1.2.5 是专为 Windows 系统打造的专业文件管理工具&#xff0c;集成极速搜索、加密保险箱、智能整理、批量重命名及重复文件查重等核心功能&#xff0c;兼顾安全性与便捷性&#xff0c;轻松解决个人及办公场景中的海量文件管理难题。快速搜索与结果筛选作为高效…

C#热更原理:为何原生不支持DLL替换?

先把问题摆在桌面上: 做 Unity / .NET 游戏热更新的时候,大家老会说一句: “C# 原生不支持运行时替换 DLL,所以得上 ILRuntime / HybridCLR / Lua 等方案。” 听多了你可能会问: 为啥 C# 就不能像脚本语言那样,想换逻辑就把 DLL 替换了? 反正 DLL 不就是一堆字节吗,我重…

Winhance v26.01.12 便携版:Windows 系统优化工具

Winhance v26.01.12 便携版是专为 Win10/Win11 打造的专业 Windows 系统优化工具&#xff0c;无需重装系统就能解决电脑卡顿、系统冗余等问题&#xff0c;帮助用户实现系统瘦身与性能提升&#xff0c;让新旧电脑都能拥有流畅运行体验&#xff0c;是 Windows 系统优化领域的实用…

2026年安徽省职业院校技能大赛(高职组) 电子数据取证与分析(学生赛)样题任务书

2026年安徽省职业院校技能大赛&#xff08;高职组&#xff09;电子数据取证与分析&#xff08;学生赛&#xff09;赛项电子数据取证技术与应用技能竞赛样题模块一&#xff1a;计算机数据分析&#xff08;35 分&#xff09;1.对 Windows 计算机镜像进行分析&#xff0c;用户硬盘…

Go进阶之协程

1.协程的概念:1.1基本概念:1).进程:进程是应用启动的实例.每个进程都有自己独立的内存空间.不同的进程通过进程间的通信方式来通信.2).线程:线程从属于进程.每个进程至少包含一个线程.线程是CPU调度的基本单位.多个线程之前共享进程资源并通过共享内存等线程间的通信方式通信.3…

抗干扰PCB工艺设计:工业电子一文说清

工业电子抗干扰PCB设计&#xff1a;从原理到实战&#xff0c;一文讲透在工厂车间里&#xff0c;一台PLC控制器突然死机&#xff0c;产线被迫停摆。排查数小时后发现&#xff0c;并非软件出错&#xff0c;也不是元器件损坏——而是PCB板上的一个地平面被割裂&#xff0c;导致ADC…

2026年安徽省职业院校技能大赛(高职组) 电子数据取证与分析(学生赛)赛项规程

2026年安徽省职业院校技能大赛&#xff08;高职组&#xff09; 电子数据取证与分析&#xff08;学生赛&#xff09;赛项规程一、赛项名称二、竞赛目标三、竞赛方式与内容五、竞赛规则软件列表&#xff1a;五、赛场预案六、赛项安全七、竞赛须知八、申诉与仲裁需要拿奖可以私信博…

Vue.js 前端开发实战 ( 电子版 ) —— 黑马

点击这里 | Vue.js 前端开发实战 ( 上 ) —— 黑马 | ⚡️⚡️⚡️ 点击这里 | Vue.js 前端开发实战 ( 下 ) —— 黑马 | ⚡️⚡️⚡️ 最后结语 Github: https://github.com/Parker-Cui Gitee: https://gitee.com/cui_pe_ng_fei Juejin: https://juejin.cn/user/2276467567…

基于真实项目的KeilC51与MDK双环境部署教程

一套能跑通的 Keil C51 与 MDK 共存方案&#xff1a;从踩坑到实战你有没有遇到过这种情况&#xff1a;手头同时在做两个项目&#xff0c;一个是老款 8051 单片机控制板&#xff0c;另一个是基于 STM32 的智能网关。想用 Keil 开发&#xff0c;却发现装了 MDK 后 C51 找不到了&a…