news 2026/8/8 5:50:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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++ 多态的全部秘密。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/2 17:40:56

电子电气架构 --- 新能源汽车领域新技术(中)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

作者头像 李华
网站建设 2026/7/29 11:37:44

移植开源软件Notepad--(NDD)到鸿蒙PC:环境搭建与配置

背景与概述 Notepad-- 是一个功能强大的开源文本编辑器&#xff0c;支持多种编程语言的语法高亮、插件扩展等功能。随着OpenHarmony生态向PC端扩展&#xff0c;将Notepad–移植到OpenHarmony PC环境上&#xff0c;不仅能够丰富鸿蒙生态的应用种类&#xff0c;还能为开发者提供…

作者头像 李华
网站建设 2026/7/28 7:40:53

创造社会价值:让更多普通人享受到AI进步红利

创造社会价值&#xff1a;让更多普通人享受到AI进步红利 在今天的AI时代&#xff0c;一个训练得再出色的模型&#xff0c;如果无法快速响应用户请求、动辄几秒甚至十几秒的延迟&#xff0c;那它本质上仍停留在实验室阶段。真正决定AI能否走进日常生活、被普通大众使用的关键&am…

作者头像 李华
网站建设 2026/7/31 3:46:12

STM32最小系统板驱动LED灯新手教程

从零点亮第一盏灯&#xff1a;STM32最小系统板驱动LED实战指南 你有没有过这样的经历&#xff1f;买了一块几十块钱的“蓝丸板”&#xff0c;插上电脑却不知道从何下手&#xff1b;翻遍资料&#xff0c;看到满屏的术语——GPIO、时钟使能、HAL库、SWD下载……一头雾水。别急&a…

作者头像 李华
网站建设 2026/8/4 2:23:41

超详细版ssd1306寄存器功能解析入门

从寄存器开始&#xff0c;真正读懂SSD1306 OLED驱动芯片你有没有过这样的经历&#xff1f;手里的OLED屏接上MCU&#xff0c;调用几行库函数&#xff0c;屏幕亮了——但一旦出问题&#xff0c;就只能靠“换线、换电源、重启”三板斧硬扛。为什么图像翻转&#xff1f;为什么亮度忽…

作者头像 李华
网站建设 2026/7/28 9:12:27

Keil5使用教程:多模块工程管理的最佳实践案例

Keil5实战指南&#xff1a;如何用多模块工程管理打造专业级嵌入式项目你有没有遇到过这样的场景&#xff1f;改一行LED驱动代码&#xff0c;Keil却把整个工程重新编译一遍&#xff0c;耗时三分钟起步&#xff1b;团队协作开发&#xff0c;两个人同时修改main.c&#xff0c;Git合…

作者头像 李华