跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++大前端java算法

C++ 虚函数、多态与对象内存布局

C++ 多态的底层实现并不复杂:带虚函数的类对象里会多出一个虚函数表指针,调用虚函数时,编译器通过对象里的虚表指针找到对应虚表,再间接调用目标函数,从而实现运行时绑定。文中还区分了静态绑定和动态绑定,说明了派生类重写后虚表槽位如何被替换,以及虚函数代码、虚表和对象内存大致各自处在什么位置。

利刃发布于 2026/6/300 浏览
C++ 虚函数、多态与对象内存布局

前言

C++ 的多态看起来是语法层面的事,落到实现上其实很直接:对象里多了一根指针,调用时多了一次间接寻址。真正容易绕的地方,不是'有没有虚函数',而是这几个问题:带虚函数的类为什么会变大,基类指针为什么能落到不同派生类的实现上,虚表和虚指针到底放在哪。

下面按对象布局、调用过程、虚表结构这三块拆开看。

虚函数和普通函数的区别

先看一个很常见的题,编译为 32 位程序时,下面代码的输出是什么?

A. 编译报错 B. 运行报错 C. 8 D. 12

class Base {
public:
    virtual void Func1() { 
        cout << "Func1()" << endl; 
    }
protected:
    int _b = 1;
    char _ch = 'x';
};

int main() {
    Base b;
    cout << sizeof(b) << endl;
    return 0;
}

结果是:

12

如果只看成员变量,int 加 char 再做内存对齐,很多人会先猜到 8。实际是 12,原因就在对象开头多了一个虚函数表指针,通常叫 _vfptr。它指向这类对象对应的虚表。只要类里有虚函数,编译器一般就会给对象塞进这么一个隐藏成员。

这也是为什么'虚函数不是占对象大小的那部分',真正占空间的是那个指针。虚函数本身还是普通函数,代码在代码段里,变化的是对象怎么找到它。

多态是怎么跑起来的

多态的关键不是'写了 virtual',而是调用发生时,编译器不能直接把地址定死。

看这段代码:

class Person {
public:
    virtual void BuyTicket() { 
        cout << "买票 - 全价" << endl; 
    }
private: 
    string _name;
};

class Student : public Person {
public:
    virtual void BuyTicket() { 
        cout << "买票 - 打折" << endl; 
    }
private: 
    string _id;
};

class Soldier : public Person {
public:
    virtual void BuyTicket() { 
        cout << "买票 - 优先" << endl; 
    }
private: 
    string _codename;
};

void Func(Person* ptr) {
    // 调用目标由 ptr 指向的实际对象决定
    ptr->BuyTicket();
}

int main() {
    Person ps; 
    Student st; 
    Soldier sr;
    Func(&ps);
    Func(&st);
    Func(&sr);
    return 0;
}

Func(Person* ptr) 里看起来都是 Person* 在调用 BuyTicket(),但真正决定执行哪一个版本的,是 ptr 指向的对象类型。ptr 指向 Person,就走 Person::BuyTicket;指向 Student,就走 Student::BuyTicket。

这里的核心动作很朴素:先从对象里拿到虚表指针,再去虚表里取对应函数地址,最后间接调用。流程比'面向对象很高级'要平淡得多,但就是它在起作用。

动态绑定和静态绑定

这两个词经常被一起讲,其实差别很简单。

  • 不满足多态条件的调用,编译时就能决定地址,这叫静态绑定。
  • 满足多态条件时,调用地址要等到运行时根据对象类型去虚表里找,这叫动态绑定。
// ptr 是指针 + BuyTicket 是虚函数,满足多态条件。
// 这里是动态绑定,运行时到 ptr 指向对象的虚函数表中确定调用函数地址
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax // BuyTicket

// BuyTicket 不是虚函数,不满足多态条件。
// 这里是静态绑定,编译器直接确定调用函数地址
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student(0EA153Ch)

这段汇编里,动态绑定的路径多了几次取值。代价不大,但它换来的是运行时分派能力。C++ 的多态本质上就是拿这点额外开销,去换灵活性。

虚函数表到底是什么

虚表可以理解成一个函数指针数组,里面放着当前类可见的虚函数入口。类对象里存的是虚表指针,不是整张表。

有几个点容易混:

  • 同类型对象通常共享同一张虚表。
  • 基类和派生类各自有自己的虚表。
  • 派生类重写了基类虚函数后,对应槽位会被替换成派生类版本的地址。
  • 派生类如果新增虚函数,虚表后面还会继续挂上新的入口。
class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
};

class Derive : public Base {
public:
    // 重写基类的 func1
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func1" << endl; }
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};

int main() {
    int i = 0;
    static int j = 1;
    int* p1 = new int;
    const char* p2 = "xxxxxxxx";
    printf("栈:%p\n", &i);
    printf("静态区:%p\n", &j);
    printf("堆:%p\n", p1);
    printf("常量区:%p\n", p2); 
    
    Base b; 
    Derive d; 
    Base* p3 = &b; 
    Derive* p4 = &d;
    
    printf("Person 虚表地址:%p\n", *(int*)p3);
    printf("Student 虚表地址:%p\n", *(int*)p4);
    printf("虚函数地址:%p\n", &Base::func1);
    printf("普通函数地址:%p\n", &Base::func5);
    return 0;
}

运行结果:

栈:010FF954
静态区:0071D000
堆:0126D740
常量区:0071ABA4
Person 虚表地址:0071AB44
Student 虚表地址:0071AB84
虚函数地址:00711488
普通函数地址:007114BF

这里能看出两件事。第一,虚表地址和普通对象数据不是一回事,它们不在同一个层面上。第二,虚表地址落在常量区这一侧,这和不同编译器的实现有关,不能拿标准去硬卡它的位置。C++ 标准并没有规定虚表必须放哪,能确认的是:虚函数代码本身在代码段,虚表是编译器维护的一张查表结构。

我一般把它记成一句话:对象里放的是'入口',代码段里放的是'实现'。多态只是把这个入口从编译期挪到了运行期。

目录

  1. 前言
  2. 虚函数和普通函数的区别
  3. 多态是怎么跑起来的
  4. 动态绑定和静态绑定
  5. 虚函数表到底是什么
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • OpenClaw 3.8 更新:ACP 溯源、备份工具和安全修复
  • 无人机检测数据集整理:11998 张图像与多格式标注
  • Web 端 3D 开发的实用技术栈梳理
  • 提升 PyTorch 训练效率的 9 个实用做法
  • 通义万相 2.1 的架构、能力与落地观察
  • 大语言模型原理、应用与演进路线
  • Seedance 2.0 实测:AI 视频从“能看”走向“能用”
  • 华为机试:素数伴侣的二分图匹配解法
  • Open3D.Art 生成模型到拓竹打印的实用流程
  • Python 3.11 新特性:性能、异常与类型系统的变化
  • CoPaw 部署与定制实操笔记
  • IntelliJ IDEA 2026.1 EAP:Java 26、Spring Boot 4 与 Gradle 9 适配
  • YOLOv8 无人机道路病害识别的工程落地思路
  • 双指针滑动窗口:4 道经典题的思路拆解
  • NWPU VHR-10 遥感目标检测与 YOLO 实践
  • 老龄化压力下护理机器人的发展与分化
  • 文心一言 4.5:中文能力实测与本地部署记录
  • 在 WSL2 上部署 OpenClaw 的实操记录
  • Oh My Open Code:把单模型 IDE 变成多模型协作系统
  • 大语言模型词表裁剪的实现思路与代码

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online