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

C++虚函数表:多态背后的魔法


C++ 多态底层机制:虚函数与虚函数表 (vtable)

1. 核心矛盾:静态绑定 vs 动态绑定

要理解虚表,首先要理解编译器面临的困境。

🅰️ 静态绑定 (Static Binding / Early Binding)

场景:普通函数(非virtual)。

  • 原理:编译器在编译阶段(按下 Build 按钮时),就根据指针的类型,把函数调用写死了。

  • 例子Father* p = new Son(); p->Say();

  • 编译器内心戏:“我看pFather*类型,不管它指向谁,我就把Father::Say的内存地址填在这里。”

  • 特点:速度极快,但死板。

🅱️ 动态绑定 (Dynamic Binding / Late Binding)

场景:虚函数(virtual)。

  • 原理:编译器在编译阶段不知道要调哪个函数,于是它生成了一段**“查表指令”。程序在运行阶段**(Run 起来后),根据指针指向的实际对象去查表,找到函数地址。

  • 特点:灵活(多态),但有微小的性能开销。


2. 幕后黑手:vtable 和 vptr

为了实现动态绑定,C++ 编译器在背后偷偷做了两件事:

① 虚函数表 (vtable) —— “类的大本营”

  • 什么是它:一个静态数组(函数指针数组)。

  • 谁拥有它每一个包含虚函数的(Class),都有一张属于自己的 vtable。

  • 存了什么:在这个类中,所有虚函数的入口地址。

    • 如果你重写了 (override),表里填的就是子类函数的地址。

    • 如果你没重写,表里填的还是父类函数的地址(复制过来的)。

② 虚表指针 (vptr) —— “对象的身份证”

  • 什么是它:一个隐藏的指针成员变量(通常占 4 或 8 字节)。

  • 谁拥有它每一个实例化的对象(Object)。

  • 存了什么:指向所属类的vtable 的首地址

  • 在哪儿:通常放在对象内存布局的最头部


3. 图解内存布局(这是最核心的)

假设我们有这样的代码:

class Base { public: virtual void A() { ... } // 虚函数 1 virtual void B() { ... } // 虚函数 2 void C() { ... } // 普通函数 (不进表) }; class Derived : public Base { public: void A() override { ... } // 重写了 A // 没有重写 B // C 是普通函数 }; Base* ptr = new Derived();

内存中的样子:

【 代码段 (Code Segment) - 静态区 】 ------------------------------------------------------- [Base 类的 vtable] | [Derived 类的 vtable] Index 0: &Base::A | Index 0: &Derived::A <-- 变了!(因为重写了) Index 1: &Base::B | Index 1: &Base::B <-- 没变!(直接继承) ------------------------------------------------------- ⬆ ⬆ | 指向 Base 表 | 指向 Derived 表 | | 【 堆区 (Heap) - 动态区 】 | -------------------- ----------------------- | Base 对象 b1 | | Derived 对象 d1 | | [vptr] -----------| | [vptr] -------------| <-- 这里的 vptr 指向 Derived 的表 | int member_base | | int member_base | -------------------- | int member_derived | -----------------------

4. 运行时的调用流程 (The Lookup Process)

当你执行ptr->A()时,发生了以下 4 步“间接跳转”:

  1. 找对象:通过ptr指针找到堆内存中的Derived对象。

  2. 找指针:读取对象头部的vptr(虚表指针)。

  3. 找表:顺着vptr找到Derived类的vtable

  4. 找函数:编译器知道A()是第一个虚函数(Index 0),所以取出vtable[0]里的地址,跳转执行。

最终执行的是:Derived::A()


5. 必须记住的 5 条铁律 (面试考点)

1. 构造函数不能是虚函数

  • 原因:虚函数调用依赖vptr。但在构造函数执行时,对象还在“娘胎”里,vptr还没初始化完成呢!你无法通过一个还没造好的指针去查表。

2. 析构函数必须是虚函数 (如果有继承)

  • 原因:防止内存泄漏。如果不是虚函数,delete base_ptr只会静态绑定调用~Base(),子类的析构根本不跑。只有设为virtual,才能查表找到~Derived()

3. 虚函数表是“类”级别的,虚指针是“对象”级别的

  • 100 个Derived对象,内存里有 100 个vptr,但它们都指向同一张Derived vtable

4. 纯虚函数 (= 0) 在表里存什么?

  • 在抽象类的 vtable 中,纯虚函数的位置通常填的是NULL或者一个会触发“Pure Virtual Function Call”异常的桩函数地址。

5. 性能开销 (Cost)

  • 空间开销:每个对象多一个指针大小(4/8 字节)。这在很多小对象(如存储数百万个Point)时也是一笔开销。

  • 时间开销:多了一次指针间接寻址 (ptr -> vptr -> table -> func)。比起直接函数调用慢一点点,但在现代 CPU 流水线优化下,通常可以忽略不计。


6. 一张图总结

概念存在位置数量关系作用
虚函数 (Virtual Func)代码区n 个允许被子类覆盖
虚函数表 (vtable)静态数据区每个类 1 张记录该类所有虚函数的实际地址
虚表指针 (vptr)对象内存头部每个对象 1 个告诉程序:“我是属于哪个类的”
Override代码逻辑-将 vtable 中的父类地址替换为子类地址

这就是 C++ 多态的全部秘密。

http://www.jsqmd.com/news/151364/

相关文章:

  • 手把手教你实现STM32单精度浮点转换
  • STM32驱动ST7789V实现GUI界面核心要点
  • hbuilderx开发微信小程序UI布局:实战项目示例
  • 提升GPU利用率的秘密武器:NVIDIA TensorRT镜像详解
  • 14:大象喝水
  • python学习day11
  • 电子电气架构 --- 新能源汽车领域有哪些新技术(上)
  • 工业现场下串口数据接收抗干扰设计:STM32CubeMX实现
  • 开源大模型+商业算力结合的最佳路径:TensorRT实践
  • Multisim14.3下载安装一文说清:整合常见疑问解答
  • STM32最小系统板驱动LED灯新手教程
  • 电子电气架构 --- 新能源汽车领域有哪些新技术(中)
  • 嵌入式系统中UART中断通信的高效设计方法
  • 创造社会价值:让更多普通人享受到AI进步红利
  • 构建高并发AI推理服务?TensorRT不可忽视的五大优势
  • LCD12864并行接口操作流程:典型时序波形分析
  • CubeMX配置看门狗提升稳定性:工业级设计建议
  • JLink驱动安装简明教程:聚焦关键配置节点
  • JLink驱动与时钟同步机制在工业控制中的联动分析:全面讲解
  • NVIDIA官方出品,必属精品:TensorRT镜像价值分析
  • 大模型应用卡顿?可能是缺少这一步:TensorRT转换优化
  • Packet Tracer官网下载全过程详解:完整指南
  • 拥抱开源生态:积极参与HuggingFace等社区协作
  • CCS安装项目应用:结合LaunchPad板卡实测
  • 下一代AI基础设施标配:GPU + TensorRT + 高速网络
  • 中小企业逆袭利器:借助TensorRT降低大模型门槛
  • 【2025最新】基于SpringBoot+Vue的企业内管信息化系统管理系统源码+MyBatis+MySQL
  • 【毕业设计】SpringBoot+Vue+MySQL 热门网游推荐网站平台源码+数据库+论文+部署文档
  • Keil5使用教程STM32:解决常见编译错误的实用指南
  • Java Web 三国之家网站系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】