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

C++ 多态机制详解:虚函数与动态绑定

C++ 多态是面向对象编程的核心特性之一,允许通过统一接口处理不同类型对象。其实现依赖于基类指针或引用调用虚函数,并通过虚函数表(vtable)和动态绑定在运行时确定具体方法。文章详细阐述了多态构成条件、虚函数重写规则(含协变)、析构函数注意事项以及 override/final 关键字用法,并深入解析了 vtable 内存布局与静态/动态绑定的区别。

编程诗人发布于 2026/3/27更新于 2026/10/2289 浏览
C++ 多态机制详解:虚函数与动态绑定

多态

什么是多态?

多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),源于希腊语"多种形态"。在 C++ 中,它允许我们使用统一的接口处理不同类型的对象,显著提高了代码的灵活性和可扩展性。

核心概念

  1. 同一接口,多种形态 不同的对象可以通过相同的方法名调用,但实际执行的逻辑由对象自身的类决定。
  2. 解耦调用与实现 调用者只需关注接口(方法名和参数),无需关心具体实现,提高代码的可扩展性和可维护性。

多态的定义及实现

多态的构成条件

多态是一个继承关系下的类对象,去调用同一函数,产生了不同的行为。 实现多态还有两个必须重要条件:

  1. 必须是基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。 说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。
虚函数

类成员函数前面加 virtual 修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加 virtual 修饰。

class A {
public:
    virtual void func() {}
};
虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数 (即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

注意:在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,也可以构成重写 (因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)。

关键技术原理
  1. 虚函数表(vtable)
    • 每个包含虚函数的类自动生成虚函数表。
    • 存储该类所有虚函数的地址。
    • 创建对象时隐式添加 vptr 指针指向 vtable。
  2. 动态绑定过程 基类指针查询对象 vptr -> 访问 vtable -> 调用实际函数地址。
最佳实践指南
  1. 使用 override 关键字明确重写意图。
  2. 基类析构函数必须声明为 virtual。
  3. 接口类使用纯虚函数。
  4. 性能考量:虚函数调用比普通函数多一次指针解引用。

虚函数重写

协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

核心概念

  1. 基类虚函数返回基类类型指针 / 引用。
  2. 派生类重写函数返回派生类类型指针 / 引用。
  3. 编译器会自动处理类型转换,确保调用的一致性。

协变的条件

  1. 基类函数必须为虚函数。
  2. 返回类型必须是指针或引用(不能是值类型)。
  • 派生类返回类型必须是基类返回类型的公有派生类。
  • 函数签名的其他部分(参数、常量性)必须完全相同。
  • 析构函数的重写

    基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor,所以基类的析构函数加了 virtual 修饰,派生类的析构函数就构成重写。

    class A {
    public:
        virtual ~A() { cout << "~A()" << endl; }
    };
    
    class B : public A {
    public:
        ~B() { cout << "~B()->delete:" << _p << endl; delete _p; }
    protected:
        int* _p = new int[10];
    };
    
    int main() {
        A* p1 = new A;
        A* p2 = new B;
        delete p1;
        delete p2;
        return 0;
    }
    

    如果 ~A() 不加 virtual,那么 delete p2 时只调用的 A 的析构函数,没有调用 B 的析构函数,就会导致内存泄漏问题,因为 ~B() 中在释放资源。

    只有派生类 Student 的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函数,才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。

    override 和 final 关键字
    1. override 关键字 作用:明确标记一个函数是重写基类的虚函数,编译器会检查以下条件:
      • 基类中是否存在同名虚函数。
      • 基类和派生类的函数签名(参数、常量性、引用限定符)是否严格一致。
      • 返回类型是否符合协变规则(如果适用)。
    2. final 关键字 作用:final 有两种用法:
      • 修饰虚函数:禁止派生类重写该虚函数。
      • 修饰类:禁止该类被继承。
    纯虚函数和抽象类

    在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现 (实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

    • 纯虚函数:在基类中声明为 virtual,并且没有实现的函数,形式为 virtual void func() = 0;。
    • 抽象类:包含纯虚函数的类称为抽象类,不能实例化对象,只能作为基类被派生。
      • Shape 是一个抽象类,Circle 是派生类,重写了 draw() 函数。

    多态的使用:

    Shape* shape = new Circle();
    shape->draw(); // 输出:Drawing Circle
    

    示例:

    class Shape {
    public:
        virtual void draw() = 0; // 纯虚函数
    };
    
    class Circle : public Shape {
    public:
        void draw() override {
            cout << "Drawing Circle" << endl;
        }
    };
    

    多态的原理

    多态是如何实现的

    在 C++ 中,多态主要通过虚函数(virtual functions)和继承机制来实现。以下是多态实现的详细过程和原理:

    1. 虚函数表(vtable)
    • 概念:每个包含虚函数的类都会有一个虚函数表(vtable),它是一个存储虚函数地址的数组。当创建一个类的对象时,对象会包含一个指向其类的虚函数表的指针(vptr)。
    • 作用:vtable 是实现多态的关键。当通过基类指针或引用调用虚函数时,程序会通过 vptr 找到对应的 vtable,然后通过 vtable 找到实际的虚函数地址。
    虚函数表知识要点
    1. 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
    2. 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生对象中的基类对象成员也独立的。
    3. 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
    4. 派生类的虚函数表中包含,(1) 基类的虚函数地址,(2) 派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。
    5. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了 0x00000000 标记。(此条取决于编译器)
    6. 虚函数存储的位置(vs 下为常量区) 可以自行验证:
    class Base {
    public:
        virtual void func1() {}
    protected:
        int a = 1;
    };
    
    class Derive : public Base {
    public:
        virtual void func1() {}
    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;
        Base* p3 = &b;
        printf("虚表地址:%p\n", *(int*)p3);
        return 0;
    }
    

    //通过比较虚表地址,找到其最近的内存片段来确定

    2. 虚函数的声明
    • Base 类的 show() 函数被声明为虚函数,Derived 类重写了这个虚函数。

    在类中使用 virtual 关键字声明函数为虚函数。 Base 为基类,Derived 为继承类。

    class Base {
    public:
        virtual void show() {
            cout << "Base show()" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        void show() override {
            // override 表示重写基类的虚函数
            cout << "Derived show()" << endl;
        }
    };
    
    3. 多态的实现过程
    • 当创建 Derived 类的对象 d 时,对象中会包含一个 vptr,指向 Derived 类的 vtable。
    • Derived 类的 vtable 中存储了 Derived 类重写的虚函数 show() 的地址。
    • ptr 是一个指向 Base 类的指针,但它指向的是 Derived 类的对象。
    • 当调用 ptr->show() 时,程序会通过 ptr 找到对象的 vptr,然后通过 vptr 找到 Derived 类的 vtable,最终调用 Derived 类的 show() 函数。
    • 这就是多态的运行时绑定(动态绑定)过程。

    通过基类指针或引用调用虚函数:

    Base* ptr = &d;
    ptr->show();
    

    创建对象:

    Derived d;
    
    动态绑定与静态绑定
    • 对不满足多态条件 (指针或者引用 + 调用虚函数) 的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
    • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。

    目录

    1. 多态
    2. 什么是多态?
    3. 多态的定义及实现
    4. 多态的构成条件
    5. 虚函数
    6. 虚函数的重写/覆盖
    7. 关键技术原理
    8. 最佳实践指南
    9. 虚函数重写
    10. 协变
    11. 析构函数的重写
    12. override 和 final 关键字
    13. 纯虚函数和抽象类
    14. 多态的原理
    15. 多态是如何实现的
    16. 1. 虚函数表(vtable)
    17. 虚函数表知识要点
    18. 2. 虚函数的声明
    19. 3. 多态的实现过程
    20. 动态绑定与静态绑定

    更多推荐文章

    查看全部
    • AI 智能填表助手:基于大模型的 Web 表单自动填写工具
    • Ubuntu 24.04 下使用 Docker Compose 本地部署 Whisper 服务
    • 开源软件管理实战指南:从问题诊断到高效运维
    • Python 基础介绍与 12 个核心开发库详解
    • Spring Boot 自动配置原理与@EnableAutoConfiguration 详解
    • Windows 本地运行 DeepSeek 的 3 个简单步骤
    • QClaw 接入微信:AI Agent 从“聊天”迈向“执行”的实战观察
    • OpenClaw 接入飞书机器人并集成 Ollama 本地大模型实战
    • OpenClaw WebUI Chat 工作流程及核心组件解析
    • 数据结构基础:二叉树与堆的实现详解
    • WSL 安装配置指南:Ubuntu 子系统部署与常用命令
    • 持续集成选型:Jenkins 与 GitLab CI 对比
    • Hibernate 多对一映射重复字段定义问题解决方案
    • 非标自动化教学系列课程
    • 命令行下利用 MCP 协议与大模型交互的工具:MCPHost 实战
    • SpringBoot @Scheduled 与 Quartz 区别及分布式定时任务选型
    • Spring Boot 数据可视化与图表集成实战
    • 网络安全入门教程:从零基础到精通的详细指南
    • 30 岁程序员转行大模型:优势分析、技术路径与实战指南
    • JetBrains IDE 集成 LeetCode 插件配置与使用指南

    相关免费在线工具

    • 加密/解密文本

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