跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 多态底层实现原理详解:虚函数表与对象模型

C++ 多态通过虚函数表和虚指针实现运行时绑定。带虚函数的类对象包含虚函数表指针,导致内存大小增加。基类和派生类拥有独立的虚函数表,派生类重写虚函数会覆盖表中地址。动态绑定在运行时根据对象实际类型查找虚表确定函数地址,静态绑定则在编译时确定。虚函数表通常存储在代码段常量区。

怪力乱神发布于 2026/3/24更新于 2026/10/1883 浏览
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

正常来说是 1+5 然后内存对齐为 8,可是其运行结果是 12,为什么会这样呢? 我们监视窗口调试一下:

在这里插入图片描述

咦?这个 _vfptr 是什么东西?没错,像是上方提到的,对象中的这个指针我们叫做虚函数表指针(v 代表 virtual,f 代表 function)。一个含有虚函数的类中至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

二、多态的原理
多态是如何实现的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

从底层的角度 Func 函数中 ptr->BuyTicket(),是如何作为 ptr 指向 Person 对象调用 Person::BuyTicket,ptr 指向 Student 对象调用 Student::BuyTicket 的呢?通过上图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。第一张图,ptr 指向的 Person 对象,调用的是 Person 的虚函数;第二张图,ptr 指向的 Student 对象,调用的是 Student 的虚函数。

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) {
    // 这里可以看到虽然都是 Person 指针 Ptr 在调用 BuyTicket
    // 但是跟 ptr 没关系,而由 ptr 指向的对象决定的。
    ptr->BuyTicket();
}
int main() {
    // 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
    // 多态也会发生在多个派生类之间。
    Person ps; 
    Student st; 
    Soldier sr;
    Func(&ps);
    Func(&st);
    Func(&sr);
    return 0;
}
动态绑定与静态绑定
  • 对不满足多态条件 (指针或者引用 + 调用虚函数) 的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
// ptr 是指针+BuyTicket 是虚函数满足多态条件。
// 这里就是动态绑定,编译在运行时到 ptr 指向对象的虚函数表中确定调用函数地址
ptr->BuyTicket();
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 不是虚函数,不满足多态条件。
// 这里就是静态绑定,编译器直接确定调用函数地址
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student(0EA153Ch)
虚函数表
  • 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以,基类和派生类有各自独立的虚函数表。
  • 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
  • 派生类的虚函数表中包含,(1) 基类的虚函数地址,(2) 派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 0x00000000 标记。(这个 C++ 并没有进行规定,各个编译器自行定义的,vs 系列编译器会再后面放个 0x00000000 标记,g++ 系列编译不会放)
  • 虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
  • 虚函数表存在哪的?这个问题严格说并没有标准答案 C++ 标准并没有规定,我们写下面的代码可以对比验证一下。vs 下是存在代码段 (常量区)

在这里插入图片描述

在这里插入图片描述

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() {
    Base b; 
    Derive d;
    return 0;
}
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

显然,虚表地址在常量区。

目录

  1. 一、虚函数和普通函数的区别
  2. 二、多态的原理
  3. 多态是如何实现的
  4. 动态绑定与静态绑定
  5. 虚函数表

更多推荐文章

查看全部
  • Python 调用 ChatGPT API 入门与实战
  • SmolVLA 模型 TensorRT 加速实战:ONNX 导出与部署优化
  • 微软发布 SPREADSHEETLLM:大模型精准理解与推理 Excel
  • Mac Intel 芯片安卓模拟器安装与使用指南
  • Windows 系统安装与配置 Neo4j 图数据库指南
  • Git 版本控制快速入门详解
  • Neo4j 知识讲解与在线工具使用教程
  • Trae IDE 安装与使用教程
  • Java 免费自学网站推荐
  • JDK21 下载与安装指南
  • Java 架构师面试题解析
  • VSCode GitHub Copilot 安装与使用指南
  • GTC Taipei 2025 医疗领域前瞻:AI 代理与医疗生态变革
  • 基于 VeRL 框架的 GSPO 算法在 Atlas 800T A2 服务器上实践
  • Spring Boot 自定义注解的几种典型用法
  • 从 MVP 到千万级并发:AI 在前后端开发中的差异化落地
  • 基于 YOLOv8/v11 与 LLM 的 Web 目标检测及人脸表情识别系统
  • JavaSE 封装详解
  • Minecraft Java 版服务器本地搭建与内网穿透教程
  • OpenClaw 本地安装教程:Node.js 环境配置与一键部署

相关免费在线工具

  • 加密/解密文本

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

  • Gemini 图片去水印

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

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online