跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

C++ 多态底层实现原理详解

C++ 多态底层依赖虚指针 vptr 与虚函数表 vtable。带虚函数的类对象内存大小增加 4/8 字节用于存储 vptr。运行时通过 vptr 查找 vtable 获取虚函数地址实现动态绑定,而非编译时的静态绑定。基类与派生类拥有独立虚表,派生类重写虚函数会覆盖基类地址。虚表通常位于常量区,本质为存虚函数指针的数组。

游戏玩家发布于 2026/3/24更新于 2026/5/79 浏览
C++ 多态底层实现原理详解

前言

在 C++ 多态的语法层面,我们已经认识了虚函数、重写、纯虚函数等关键知识点。多态的底层依赖于 vptr(虚指针)与 vtable(虚函数表)。但在学习时,常有以下疑问:

  • 为什么带虚函数的类,sizeof 大小会多出 4/8 字节?
  • 基类指针指向不同派生类对象,是如何在运行时找到对应函数的?
  • 虚表、虚指针、虚函数分别存在内存哪个区域?
  • 静态绑定和动态绑定到底有什么区别?

本篇从内存布局、对象模型、汇编视角、虚表结构出发,把 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. 多态是如何实现的
  6. 动态绑定与静态绑定
  7. 虚函数表
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • VS Code 远程连接服务器后 GitHub Copilot 失效排查指南
  • Java 面试核心知识点汇总
  • HDFS 常用命令与 Java API 编程实践
  • PFC2D 借助 Python 绘制应力云图
  • 飞算 JavaAI 插件安装与代码生成实战指南
  • 大语言模型提示词编写与应用指南
  • 2024 年人工智能大模型发展回顾与展望
  • 宇树 G1 人形机器人强化学习训练配置与奖励函数解析
  • 基于 RAG 架构的本地智能问答系统构建指南
  • AI Agent Skills 设计与编写实战指南
  • 2026 年 RAG 技术演进:DeepSeek 与 Neo4j 构建企业知识图谱
  • 从人类视频到机器人跳舞:BeyondMimic 全流程解析与 rl_sar 部署实践
  • ChatLaw 袁粒:法律大模型如何助力个人维权与行业思考
  • Git Push 失败常见原因排查:权限、分支保护与冲突解决
  • Gemini Pro 提示词最佳实践:多模态与结构化设计指南
  • 使用 LangChain 快速搭建 RAG 知识库
  • CentOS 安装 LNMP 环境配置
  • nanobind C++/Python 高性能绑定实战指南
  • 基于 FPGA 的微波炉控制器设计
  • 从零开始学习 Web 安全与黑客技术入门指南

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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