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

C++ 多态原理详解:虚函数表与动态绑定

讲解 C++ 多态原理,包括虚函数表指针(vfptr)的作用、虚函数表的本质及存储位置。阐述了静态绑定与动态绑定的区别:不满足多态条件时在编译期确定地址,满足条件时在运行时通过虚表查找地址。介绍了派生类虚函数表的生成逻辑,即继承基类虚表并覆盖重写函数,同时包含自身新增虚函数。最后验证了虚函数表通常存储在代码段(常量区)。

蜜桃汽水发布于 2026/3/23更新于 2026/7/129K 浏览
C++ 多态原理详解:虚函数表与动态绑定

4 多态的原理

4.1 虚函数表指针

我们以一道题来引入多态的原理。

下面编译为 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;
}

按照我们之前的知识,这题答案应该选:C。但我们不妨多留一个心眼:这题如果是考察内存对齐,为什么要加一个虚函数呢?是不是没有这么简单。

我们来看下运行结果:为什么?Base 类中除了 _b 和 _ch 成员,还多一个 _vfptr 成员放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v 代表 virtual,f 代表 function)。一个含有虚函数的类中至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

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

虚函数表其实是一个数组,该数组中存放着该类中所有虚函数的地址。虚函数表本质是一个函数指针数组,而 _vfptr 则是指向这个数组的指针。通过图片我们也可以看到:虚函数表中放着虚函数 Func1() 和 Func2() 的地址,因为 Func3() 不是虚函数,并没有放进去。

4.2 多态的原理

认识到了虚表指针的存在,我们就可以进一步来了解多态的原理啦。

我们结合具体的样例来学习。

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

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

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

void Func(Person* ptr) {
    ptr->BuyTicket();
}

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

上述代码中有三个类,每个类都有一个虚表指针。

可以看到,三个类中虚函数表的 BuyTicket() 函数指针的地址都是不同的。

多态是怎么做到指向谁就去调用谁的呢?在编译阶段,编译器检查语法,看满不满足多态的条件。如果满足多态,在编译这段指令时,底层不再是编译时通过调用对象确定函数的地址,而是变成:在运行时,到指向对象的虚函数表中去找对应虚函数的地址,进行调用。

这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

对 Func() 函数的 ptr 来说,不论传递的是父类对象还是子类对象,在它眼里都是父类对象,不同的是子类需要进行切片,ptr 看到的是子类切片后剩下的父类对象。但是没关系,如果满足多态条件,ptr 会进入这个父类的虚函数表中查找对应的虚函数的地址,找到谁就调用谁。

满足多态时的汇编代码:

前面的 mov 指针简单来说就是:找到 _vfptr 指针,再找到对应的虚函数表,再找到对应的函数指针,最后将指针给 eax 寄存器,寄存器去 call 函数地址。

下面,我将父类的 virtual 去掉,他们就不满足多态的条件了,再来看看他们的汇编代码。

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

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

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

两句代码搞定,ptr 是父类的指针,直接调用父类的 BuyTicket() 函数,与指向的对象无关。

4.3 动态绑定和静态绑定

  • 对不满足多态条件(指针或者引用 + 调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就叫做动态绑定。
// 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)

从运行效率上来说,静态绑定更高一点,毕竟只有两句指令。

4.4 虚函数表

  • 这一点我们前面已经讲过了。
  • 同一个类的对象虚函数表共用,不同类型对象虚表各自独立。

基类对象的虚函数表中存放基类所有虚函数的地址。

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

int main() {
    Base b1;
    Base b2;
    Base b3;
    return 0;
}

这也解释了为什么虚函数不放在对象中,而是放在一个数组之中,因为不同的对象好共享。如果不把虚函数地址放在虚函数表中,而是放在对象之中,那么每个对象都要存一份,太过冗余。像这样放在一个公共的地方,无论有几个虚函数,都只需多 4 个字节来存储指针就行。

  • 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但需要注意的是,这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。

  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

  • 派生类的虚函数表包含:基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址。

什么意思呢?

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;
};

现在基类 Base 中有两个虚函数,派生类 Derive 中重写了 func1(),并且有一个自己的虚函数 func3()。

派生类的虚函数表生成逻辑是这样的:先将基类的虚函数表拷贝一份看有无完成重写/覆盖。派生类 Derive 重写了 func1 函数,就会用重写的 func1 将基类的 func1 进行覆盖,func2 并没有完成重写,不管最后再加上自己的虚函数。

  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个 0x00000000 标记。(这个 C++ 并没有明确规定,各个编译器自行定义的,VS 系列编译器会在后面放个 0x00000000 标记,g++ 系列编译器不会放)

  • 虚函数存在哪?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段 (常量区) 的,只是虚函数的地址又存到了虚表中。

  • 虚函数表存在哪?这个问题严格来说并没有标准答案,C++ 标准并没有规定,我们写下面的代码可以对比验证一下。VS 下是存在代码段(常量区)。

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;
}

运行结果:

可以看到,虚表的地址和常量区的最接近。我们可以大致判定 VS 下虚函数表是放在代码段的。

目录

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

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

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

更多推荐文章

查看全部
  • 数据结构复习:Java 顺序表与 ArrayList 详解
  • MoonTV:基于 Next.js 的开源跨平台影视聚合播放器
  • Python 和 Java 中的浅拷贝与深拷贝详解
  • DeepFace + OpenCV 实时情绪分析实战
  • ASP.NET Core EF Core + MySQL 从数据库生成实体类实战指南
  • 基于代理服务与 AI 分析的亚马逊电商数据高效爬取方案
  • 两年转行网络安全:真实经验总结与学习路径建议
  • NVIDIA DGX Spark 部署 Stable Diffusion 3.5 与 ComfyUI 实战指南
  • Spring Data JPA 原理与实战:Repository 接口深度解析
  • RAG 与 LLM 微调对比:结合业务场景落地指南
  • Python Flask 框架核心原理与开发实践
  • Python 体育数据爬虫:基于 Playwright 与异步技术的高性能采集
  • 2 个原因解答:为什么网络安全缺口大,招聘却很少?
  • 大模型加速落地汽车领域,车企探索智能化新路径
  • C语言运算符优先级与结合性详解
  • yshopmall 开源电商 SaaS 解决方案解析
  • ZeroClaw: 基于 Rust 的轻量级 AI 助手网关,与 OpenClaw 对比分析
  • SkyWalking 全链路监控实战:Spring Cloud、Dubbo、RocketMQ 及 ShardingSphere 集成
  • AR 技术在电力配电运维中的应用与解决方案
  • 深入解析 Spring AOP 原理

相关免费在线工具

  • 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

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online