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

C++ 抽象类与多态原理:从纯虚函数到虚表机制

C++ 抽象类通过纯虚函数定义接口规范,派生类必须重写才能实例化。多态底层依赖虚表指针和虚函数表实现动态绑定。虚表存储虚函数地址,位于代码段。对象包含虚表指针指向虚表。基类指针调用虚函数时,运行时根据实际对象类型查找虚表确定函数地址。静态绑定在编译时确定,动态绑定在运行时确定。析构函数建议设为虚函数以正确释放资源。

奇形怪状发布于 2026/2/10更新于 2026/10/243.7K 浏览
C++ 抽象类与多态原理:从纯虚函数到虚表机制

C++ 抽象类与多态原理:从纯虚函数到虚表机制

前言

在 C++ 多态体系中,纯虚函数与抽象类是实现'接口规范'的核心工具,而虚函数表(vtable)与虚表指针(vfptr)则是多态底层实现的关键。本文将基于实战代码,从纯虚函数与抽象类的定义、使用场景入手,逐步拆解多态的底层原理——包括虚表指针的存在性、虚函数表的结构与存储位置,最终帮你打通'多态怎么用'到'多态如何实现'的认知链路。

一、纯虚函数与抽象类:强制接口规范的'契约'

在实际开发中,我们经常需要定义一个'只规定行为,不提供具体实现'的类。C++ 通过纯虚函数和抽象类实现这种'接口契约'。

1.1 纯虚函数:没有实现的'接口声明'

在虚函数的声明后加=0,该函数即为纯虚函数。纯虚函数无需在基类中实现(语法上允许实现,但无实际意义,因为会被派生类重写),其核心作用是'强制派生类必须重写该函数'。

1.2 抽象类:包含纯虚函数的'不可实例化类'

包含纯虚函数的类称为抽象类,它有两个关键特性:

  • 无法直接实例化对象(编译器会报错);
  • 派生类若未重写基类的所有纯虚函数,自身也会成为抽象类,同样无法实例化。

有了上面的知识储备,我们来看下代码示例吧:

#include <iostream>
using namespace std;

// 抽象类:包含纯虚函数 Drive()
class Car {
public:
    // 纯虚函数:只声明接口,不提供实现
    virtual void Drive() = 0;
};

// 派生类 Benz:重写纯虚函数,成为'具体类'
class Benz : public Car {
public:
    // 必须重写 Drive(),否则 Benz 也是抽象类
    virtual void Drive() {
        cout << "Benz-舒适" << endl;
    }
};

// 派生类 BMW:重写纯虚函数,成为'具体类'
class BMW : public Car {
public:
       {
        cout <<  << endl;
    }
};

{
    
    
    
    
    Car* pBenz =  Benz;
    pBenz->(); 
    
    Car* pBMW =  BMW;
    pBMW->(); 
    
     pBenz;
     pBMW;
     ;
}
virtual
void
Drive
()
"BMW-操控"
int main()
// 抽象类无法实例化对象
// Car car;
// 用抽象类指针指向派生类对象(多态核心用法)
new
Drive
// 多态调用:输出'Benz-舒适'
new
Drive
// 多态调用:输出'BMW-操控'
delete
delete
return
0

二、多态的底层原理:虚表指针与虚函数表

当我们用基类指针调用派生类的虚函数时,编译器如何'知道'该调用哪个类的函数?答案藏在虚表指针(vfptr) 和虚函数表(vtable) 中——这是 C++ 实现动态绑定(运行时多态)的核心机制。

2.1 虚表指针(vfptr):对象中的'导航器'

首先通过下面这个题目来验证一下虚表指针的存在。下面编译为 32 位程序的运行结果是什么(D)。 A. 编译报错 B. 运行报错 C. 8 D. 12

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

int main() {
    Base b;
    // 除了我们能看到的_b 和_ch,其实有虚函数的类就会有一个虚函数表指针 (32 位下 4 字节,64 位下 8 字节)
    // 因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
    cout << sizeof(b) << endl; // 32 位:4+4+1->12
    // 输出结果:32 位环境下为 12 字节,64 位环境下为 16 字节
    return 0;
}

关键结论:

  • 只要类中包含虚函数(或继承自含虚函数的类),该类的对象就会额外存储一个虚表指针;
  • 虚表指针通常位于对象内存的最前端(不同编译器可能有差异),其作用是'指向该类的虚函数表';
  • 同类型的对象共用一张虚函数表,但每个对象都有独立的虚表指针(指向同一张虚表)。

2.2 多态的实现原理

从底层的角度 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;
};

void Func(Person& ptr) {
    // 这里可以看到虽然都是 Person 指针 Ptr 在调用 BuyTicket
    // 但是跟 ptr 没关系,而是由 ptr 指向的对象决定的。
    ptr.BuyTicket();
}

int main() {
    // 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
    // 多态也会发生在多个派生类之间。
    Person ps;
    Student st;
    Func(ps);
    Func(st);
    
    // 这三个的虚函数表是一样的,同类型的对象共用一虚表
    // Person p1;
    // Person p2;
    // Person p3;
    return 0;
}

2.3 虚函数表(vtable):存储虚函数地址的'数组'

虚函数表(简称'虚表')是编译器为每个含虚函数的类生成的一张'虚函数指针数组',数组中存储的是该类所有虚函数的地址。其结构与生成规则如下:

  • 基类虚表:存储基类所有虚函数的地址(如 Base 类的虚表存储 Func1 和 Func2 的地址);
  • 派生类虚表:
    • 首先继承基类虚表的所有内容;
    • 若派生类重写了基类的虚函数,会用派生类自身的虚函数地址'覆盖'基表中对应的位置;
    • 派生类新增的虚函数,其地址会追加到虚表的末尾;
  • 虚表结尾标记:部分编译器(如 VS)会在虚表末尾添加 0x00000000 作为结束标记(g++ 无此标记,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:会覆盖虚表中 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;
}

2.4 动态绑定与静态绑定

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

当用基类指针调用虚函数时,编译器会按以下步骤完成'动态绑定'(运行时确定调用的函数):

  • 获取虚表指针:从基类指针指向的对象中,取出虚表指针(vfptr);
  • 查找虚表:通过虚表指针找到该对象所属类的虚函数表(vtable);
  • 定位函数地址:在虚表中找到目标虚函数对应的地址(若派生类重写过,此处就是派生类函数地址);
  • 调用函数:通过找到的函数地址,调用对应的虚函数。

以之前的'买票'场景为例,流程如下:

// 基类指针指向派生类对象
Person* ptr = new Student;
// 动态绑定流程:
// 1. 从 ptr 指向的 Student 对象中,取出 vfptr;
// 2. 通过 vfptr 找到 Student 类的虚表;
// 3. 在虚表中找到 BuyTicket 对应的地址(Student::BuyTicket 的地址);
// 4. 调用该地址对应的函数,输出'买票 - 打折'。

三、关键问题辨析与总结

3.1 虚函数存在哪里?虚表又存在哪里?

  • 虚函数:与普通函数一样,编译后是一段机器指令,存储在代码段;虚表中仅存储虚函数的'地址',而非函数本身;
  • 虚表:本质是'存储虚函数地址的指针数组',在 VS 等编译器中存储在代码段(常量区)(因内容不可修改),C++ 标准未强制规定存储位置,不同编译器可能有差异。
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;
    printf("Base 虚函数表地址:%p\n", *((int*)&b));
    printf("Derive 虚函数表地址:%p\n", *((int*)&d));
    printf("虚函数地址:%p\n", &Base::func1);
    printf("普通函数地址:%p\n", &Base::func5);
    return 0;
}

运行结果: 栈:010FF954 静态区:0071D000 堆:0126D740 常量区:0071ABA4 Person 虚表地址:0071AB44 Student 虚表地址:0071AB84 虚函数地址:00711488 普通函数地址:007114BF

3.2 实战注意事项:

  • 抽象类只能作为基类指针 / 引用使用,不可直接实例化;
  • 抽象类无法实例化,派生类必须重写所有纯虚函数才能成为'具体类';
  • 虚表指针会增加对象的内存开销(32 位下 4 字节,64 位下 8 字节);
  • 虚函数调用比普通函数多一次'地址查找',存在微小性能损耗,但通常可忽略(多态带来的灵活性远大于性能损失)。

四、多态考察的一些常见问题 (重点,面试高频题)

1. 面向对象的三大特性 (这里重点讲讲什么是多态)

  • 封装:将数据和操作数据的方法封装在一起,隐藏对象的内部实现细节,仅对外暴露公共接口,降低耦合性 (eg:迭代器的实现)
  • 继承:子类继承父类的属性和方法,子类可以直接使用父类的非私有成员和方法,实现代码的复用,同时子类可以对父类的方法进行扩展和重写
  • 多态:多态分为静态多态 (编译时多态) 和动态多态 (运行时多态),静态多态比如函数的重载,模板。我们可以通过传不同的参数达到不同的效果。动态时多态就要求有继承关系,用基类的指针或者引用去调用虚函数,且派生类对基类的虚函数完成了重写,根据基类的指针指向的对象去调用对应的方法,做到不同对象执行不同逻辑。(eg.动物叫声的例子,不同的对象发出不同的叫声)

2. 什么是重载,重写 (覆盖),重定义 (隐藏)?

  • 重载:同一类中 (同一作用域中),函数名相同,参数列表(参数类型、个数、顺序)不同,返回值类型可以相同也可以不同
  • 重写 (覆盖):子类继承父类后 (不同作用域),对父类的虚函数进行重新实现,函数名、参数列表、返回值类型(协变情况除外)完全相同。
  • 重定义 (隐藏):(不同作用域) 子类中定义了与父类同名 (只需要同名就可以) 的非虚函数,隐藏父类的该函数。
特性定义示例
重载同一类中,方法名相同,参数列表(参数类型、个数、顺序)不同,与返回值类型无关类中 add(int a, int b) 和 add(double a, double b)
重写(覆盖)子类继承父类后,对父类的虚函数进行重新实现,方法名、参数列表、返回值类型(协变情况除外)完全相同父类 Animal 的虚函数 makeSound(),子类 Dog 重写为 void makeSound() { cout << "汪汪" << endl; }
重定义(隐藏)子类中定义了与父类同名的非虚函数,隐藏父类的该函数父类有 func(),子类也定义 func(),子类对象调用 func() 时执行子类的,父类对象调用执行父类的

3. 多态的实现原理? 答: 多态通过 虚函数表(vtable)和虚表指针(vptr) 实现。每个包含虚函数的类都有一个虚函数表,表中存储着该类所有虚函数的地址。每个对象都有一个虚表指针,指向所属类的虚函数表 (相同类型的对象指向同一张虚函数表)。当通过父类指针或者引用调用虚函数时,程序会根据指向的实际对象类型,通过其虚表指针找到虚函数表,再找到对应的虚函数地址 (可以通过虚函数指针) 并调用,从而实现运行时的多态。

4. inline 函数可以是虚函数吗?inline 属性和虚函数属性能同时存在吗? 答:可以是虚函数,从语法上看,inline 函数可以声明为虚函数,但实际上编译器会忽略 inline 属性 (inline 一般展开是不需要地址的),将其当作普通虚函数处理。因为虚函数要放在虚表中去,两者机制冲突,也就是说 inline 属性和虚函数属性是不同时存在的。

5. 静态成员可以是虚函数吗? 答:不能,静态成员函数属于类,不属于某个对象,没有 this 指针,而虚函数的调用需要通过对象的虚函数指针来实现,所以静态成员函数不能是虚函数。

6. 构造函数可以是虚函数吗? 答:不可以。因为对象中的虚函数指针是在构造函数初始化列表阶段才初始化的,所以构造函数不能是虚函数。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

  • 析构函数可以是虚函数。
  • 场景:当存在继承关系,且需要通过父类指针删除子类对象时,为了确保子类的析构函数被调用,需要将父类的析构函数声明为虚函数。例如:父类 Base,子类 Derived,若用 Base* p = new Derived(); delete p;,如果 Base 的析构函数不是虚函数,只会调用 Base 的析构函数,导致子类资源未释放;若为虚函数,则会先调用 Derived 的析构函数,再调用 Base 的析构函数。

8. 对象访问普通函数快还是虚函数更快? 答:首先如果是普通对象调用的话两者是一样快的,但如果是基类的指针或者引用去调用,且构成了多态调用,则调用的普通函数更快,运行时调用虚函数需要到虚函数表中去查找,有一定开销。

调用场景普通函数调用机制虚函数调用机制性能差异根源
普通对象调用编译时直接绑定函数地址,直接跳转执行编译时直接绑定函数地址,直接跳转执行无差异
基类指针/引用多态调用编译时直接绑定函数地址,直接跳转执行运行时通过 vptr 找 vtable,再找函数地址执行虚函数多了查表的运行时开销

9. 虚函数表是在什么阶段生成的,存在哪里的? 答:虚函数表是在编译阶段生成的,一般情况下是存在**代码段 (常量区) 的。

10. C++ 菱形继承的问题?虚继承的原理? 答:菱形继承会导致数据冗余和二义性的问题,虚继承则是通过虚基类指针和虚基表 (不要把虚函数表,虚函数指针和虚基表,虚基类指针搞混了) 实现的中间基类在继承时顶层基类时声明为虚继承,这样可以保证顶层基类的成员只会有一份,解决了数据冗余和二义性的问题。

11. 什么是抽象类?抽象类的作用? 答:包含纯虚函数(形如 virtual void func() = 0;)的类,无法实例化对象。抽象类的作用是 作为接口规范,强制子类必须重写实现纯虚函数

结语

通过抽象类,我们能定义清晰的接口契约;通过虚表机制,C++ 实现了'运行时动态绑定'的多态能力。理解这两者,不仅能正确使用多态,更能在复杂场景中(如框架设计、模块扩展)写出更灵活、可维护的 C++ 代码。

目录

  1. C++ 抽象类与多态原理:从纯虚函数到虚表机制
  2. 前言
  3. 一、纯虚函数与抽象类:强制接口规范的“契约”
  4. 1.1 纯虚函数:没有实现的“接口声明”
  5. 1.2 抽象类:包含纯虚函数的“不可实例化类”
  6. 二、多态的底层原理:虚表指针与虚函数表
  7. 2.1 虚表指针(vfptr):对象中的“导航器”
  8. 2.2 多态的实现原理
  9. 2.3 虚函数表(vtable):存储虚函数地址的“数组”
  10. 2.4 动态绑定与静态绑定
  11. 三、关键问题辨析与总结
  12. 3.1 虚函数存在哪里?虚表又存在哪里?
  13. 3.2 实战注意事项:
  14. 四、多态考察的一些常见问题 (重点,面试高频题)
  15. 结语

更多推荐文章

查看全部
  • 基于 AI 辅助开发的在线图书借阅系统实现
  • Python GUI 可视化设计工具 tkinter-helper 介绍
  • RAG 检索增强生成:概念、原理与实战
  • 循环队列(Circular Queue)详解
  • 金仓数据库 SQL 防火墙原理与防护实践
  • GitHub Copilot 调用第三方模型API
  • GLM-4v-9b 实战:基于 llama.cpp GGUF 在消费级 GPU 部署多模态模型
  • 通义万相 2.1 视频生成模型在蓝耘智算平台的部署与应用
  • 前端安全:别让你的应用变成黑客的游乐场
  • 【火】Spatial Joy 2025 全球 AR&AI 赛事:开发者要的资源、玩法、避坑攻略都在这
  • AI+SDD 重构复杂业务研发范式与实操指南
  • 前端跨子域通讯深度解析:核心方案与避坑指南
  • GESP C++ 一级认证真题与解析:单选题 1-15
  • Midjourney 官方网址查询与中文访问方案
  • AI 语言模型详解:原理、架构与应用实践
  • LeetCode 经典算法题 Python 实战解析:树、链表与 DP
  • Google AI Studio 使用指南与 Gemini 开发集成
  • 基于 AI API 的海龟汤互动游戏开发实践
  • AI 数学基础:Tokenization 如何将文本转换为数字
  • 基于 STM32 的智慧农业大棚系统设计

相关免费在线工具

  • 加密/解密文本

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