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

C++ 深入理解多态:从虚函数表到底层实现

C++ 多态分为编译时多态和运行时多态。编译时多态通过重载和模板实现,运行时多态依赖虚函数表和动态绑定。基类指针指向派生类对象时,需将析构函数声明为虚函数以防止资源泄漏。override 和 final 关键字用于规范重写行为。虚函数表存储虚函数地址,位于内存常量区,派生类继承并覆盖基类虚函数地址。

孤勇者发布于 2026/3/26更新于 2026/5/98 浏览
C++ 深入理解多态:从虚函数表到底层实现

一、多态的概念

1.1 编译时多态

编译时多态主要是函数重载和函数模板,它们实现多态主要是通过传不同类型的参数。之所以称为编译时多态,是因为参数的匹配是发生在编译阶段的,这种也被称为静态绑定。

1.2 运行时多态

运行时多态可以认为是通过对象来区分行为的,而编译时多态是通过类型的。就好像同样是'叫'这个行为,传猫过去就是'喵喵',传狗过去就是'汪汪'。

二、多态的定义和使用

2.1 多态的判定标准

  1. 要是用基类的引用或者指针来调用;
  2. 被调用的函数是虚函数;
  3. 调用函数需要构成重写。

我们这里着重讲一下为什么一定要用基类的指针或者是引用调用:

因为多态的内核就是'在运行时确定'。假如使用的是基类的对象,那么在编译的时候对象的类型就是明确的基类类型,这时候就起不到运行时确定的效果了。假设是使用指针或者是引用来调用,在运行时是不知道具体指向的类型的,这时候就需要去查找虚函数表,就可以实现'运行时确认'。

2.1.1 虚函数

在函数声明之前加上 virtual 关键字,这个函数就是虚函数。

virtual void func()
2.1.2 函数重写

函数重写需要构成以下条件:

  1. 两个函数的函数名、参数、返回值全部相等;
  2. 两个函数全都是虚函数。

补充:派生类中的函数不是虚函数也是可以的,但是基类中的函数一定是虚函数,此时相当于是将基类中函数的 virtual 关键字继承下来了,此时重写后的函数相当于是将基类的函数名和参数和派生类的函数实现拼在一起了。

2.2 相关的题目

以下程序输出结果是什么? A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

class A {
public:
    virtual void func(int val = 1) {
        std::cout << "A->" << val << std::endl;
    }
    virtual void test() {
        func();
    }
};

class B : public A {
public:
    void func(int val = 0) {
        std::cout << "B->" << val << std::endl;
    }
};

int main(int argc, char* argv[]) {
    B* p = new B;
    p->test();
    return 0;
}

在 main 函数中创建了一个 B 类型的对象,并将它的指针赋值给了 B*类型的指针 p。然后使用 p 来调用 test 函数,这实际上调用的是 B 中继承自 A 的部分中的 test 函数,但是在调用过程中传递给 test 函数中 this 指针的是 B 类型的指针。

根据多态的对象决定论,实际上访问的 func 函数是派生类 B 重写后的 func 函数。但是派生类的 func 函数并没有声明是虚函数,所以实际上重写函数是由基类中的函数的声明和派生类的实现组成的,相当于:

virtual void func(int val = 1) {
    std::cout << "B->" << val << std::endl;
}

此时运行结果是 B->1,选择 B。

2.3 析构函数的重写

在继承体系中基类的指针是可以指向派生类的对象的,这时候传统的析构就出现问题了。

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

// 只有派生类的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函数,才能
// 构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。
int main() {
    A* p1 = new A;
    A* p2 = new B;
    delete p1;
    delete p2;
    return 0;
}

这时候应该怎么析构,我们显然是想将指针指向的对象全部析构掉,但是碍于指针是 A 基类类型的,如果不加以处理,将只会调用基类的析构函数,派生类并没有被有效析构,这显然是不符合我们的预期的。

这时候我们将析构函数置为虚函数,派生类和基类中的析构函数就构成了函数重写,此时再 delete 基类的指针,调用的就是派生类的析构函数,就可以将空间释放干净。

![虚函数表示意图]

可以看到:析构 p2 指向空间的时候调用了 B 的析构函数。

2.4 override 和 final 关键字

在继承体系中重写的判断条件是比较苛刻的,两个函数只要是出现一点不一样就会达不到重写的条件,这时候还不一定会报错,这是比较危险的(可能会实现不了预期效果)。这时候就引入了 override 关键字和 final 关键字,override 关键字用来检查是否构成重写,final 关键字用来声明这个函数不能被重写。

示例:

class A {
public:
    virtual ~A() {
        cout << "~A()" << endl;
    }
    virtual void func1() {}
    virtual void func2() final {}
};

class B : public A {
public:
    virtual ~B() {
        cout << "~B()->delete:" << _p << endl;
        delete _p;
    }
    virtual void func1(size_t i) override {}
    virtual void func2() {}
protected:
    int* _p = new int[10];
};

如图所示:会直接编译报错。

2.5 纯虚函数和抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现)。

包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

2.6 协变(不重要,了解即可)

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

class Person {
public:
    virtual A* BuyTicket() {
        cout << "买票 - 全价" << endl;
        return nullptr;
    }
};

class Student : public Person {
public:
    virtual B* BuyTicket() {
        cout << "买票 - 打折" << endl;
        return nullptr;
    }
};

三、多态的底层实现

3.1 虚函数表简介

在以上情景中的类的内存中,都存在一个 _vfptr 指针,这就是虚函数表,也叫虚表,指向了类中的虚函数。

![虚函数表结构]

3.2 多态的实现

在满足多态后,要调用哪个函数并不是在编译时就通过指向对象确定的,而是需要在运行时到指向虚函数地址的虚函数表内确定虚函数的地址然后再调用,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

![多态调用流程]

class Person {
public:
    virtual A* BuyTicket() {
        cout << "买票 - 全价" << endl;
        return nullptr;
    }
};

class Student : public Person {
public:
    virtual B* BuyTicket() {
        cout << "买票 - 打折" << endl;
        return nullptr;
    }
};

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

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

3.3 静态绑定和动态绑定

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

3.4 虚函数表详解

3.4.1 虚函数表的相关规则
  1. 同类型的对象的虚函数表是同一个(不直接将虚函数的函数指针存在对象中而是存在一个指针数组中就是为了节省空间);
  2. 派生类先将基类的虚函数表继承下来(但是是拷贝,指向的不是同一块空间);
  3. 构成了重写的函数会将原来的函数指针覆盖掉;
  4. 派生类的虚函数表中包含:(1) 基类的虚函数地址,(2) 派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分;
  5. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 0x00000000 标记。(这个 C++ 并没有进行规定,各个编译器自行定义的,vs 系列编译器会在后面放个 0x00000000 标记,g++ 系列编译不会放)

还有需要注意的是:

当虚函数继承了多个基类时,新增的虚函数,统一放在第一个基类的虚函数表中。

3.4.2 虚函数存放的位置问题(这里有坑)

大部分人看到这个问题可能想到的是:虚函数明显是存在虚函数里的。

实则不然,虚函数也是函数,是存在代码段中的,只不过是也在虚函数表中存了一份。

3.4.3 虚函数表存放的位置

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

验证得:大概是在常量区。

目录

  1. 一、多态的概念
  2. 1.1 编译时多态
  3. 1.2 运行时多态
  4. 二、多态的定义和使用
  5. 2.1 多态的判定标准
  6. 2.1.1 虚函数
  7. 2.1.2 函数重写
  8. 2.2 相关的题目
  9. 2.3 析构函数的重写
  10. 2.4 override 和 final 关键字
  11. 2.5 纯虚函数和抽象类
  12. 2.6 协变(不重要,了解即可)
  13. 三、多态的底层实现
  14. 3.1 虚函数表简介
  15. 3.2 多态的实现
  16. 3.3 静态绑定和动态绑定
  17. 3.4 虚函数表详解
  18. 3.4.1 虚函数表的相关规则
  19. 3.4.2 虚函数存放的位置问题(这里有坑)
  20. 3.4.3 虚函数表存放的位置
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 2025 年 3 月 CCF-GESP C++ 三级真题解析
  • Node.js 完整安装配置指南(含国内镜像配置)
  • 贪心算法实战:从柠檬水找零到数组减半与最大数拼接
  • SpringBoot 启动引导类的命名约定与核心原理
  • 大模型推理框架 llama.cpp 开发流程与常用函数解析
  • Ollama 快速部署大模型:Windows/Linux/Mac 通用教程
  • Python 拼接地图瓦片常见错误及解决方案
  • Python+Copilot 全流程开发效率提升指南:从语法纠错到项目重构
  • 除了卖课,普通人如何通过 AI 实现商业价值?
  • 基于 OpenClaw 与 GLM 模型实现免费 AI 联网搜索
  • Windows 下 OpenClaw + Ollama 全离线本地 AI 部署指南
  • 人工智能:自然语言处理在医疗领域的应用与实战
  • Vue.js 核心语法与原理详解
  • GitHub Copilot 代理配置与网络优化指南
  • OpenClaw 多智能体路由实战:飞书多机器人配置
  • OpenClaw macOS 安装与环境配置指南
  • npm 安装 OpenClaw 遇到 Git 报错的排查与解决
  • OpenClaw 跨平台安装指南:Windows、macOS 与 Linux 全方案
  • 前端防抖与节流详解:原理、区别与实战示例
  • 医学影像中刚性配准与非刚性配准的算法对比与选型

相关免费在线工具

  • 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