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

C++ 虚函数与纯虚函数:深入理解多态机制

C++ 多态依赖虚函数与纯虚函数实现。虚函数允许派生类重写基类行为,运行时绑定;纯虚函数定义接口,使类成为抽象类无法实例化。底层通过虚函数表(vtable)和虚指针(vptr)完成动态分发。使用基类指针删除派生类对象时,必须声明虚析构函数以防内存泄漏。构造函数中调用虚函数无效,且需注意函数签名匹配及性能开销。掌握这些机制是编写健壮 C++ 面向对象代码的关键。

ArchDesign发布于 2026/3/22更新于 2026/9/950 浏览
C++ 虚函数与纯虚函数:深入理解多态机制

C++ 虚函数与纯虚函数:深入理解多态机制

在 C++ 面向对象编程中,多态是核心特性之一,而虚函数与纯虚函数则是实现这一特性的基石。理解它们的本质区别、底层原理以及常见陷阱,对于编写健壮、可扩展的代码至关重要。

一、虚函数的本质与定义

虚函数允许派生类重写基类的行为,并在运行时根据对象的实际类型调用对应的版本。这是动态多态的基础。

1.1 声明语法

虚函数必须在基类中使用 virtual 关键字声明:

class Base {
public:
    virtual void func() {
        // 默认实现
    }
};
1.2 核心特性
  • 运行时绑定:调用关系在程序运行期间确定,而非编译时。
  • 重写规则:派生类重写的函数必须与基类虚函数的函数名、参数列表完全一致(协变返回类型除外)。
  • 继承性:一旦基类声明为虚函数,派生类中的重写函数自动成为虚函数,无需再次添加 virtual 关键字(但建议保留以增强可读性)。
1.3 基础示例

下面是一个简单的交通工具示例,展示了基类指针如何指向不同派生类对象并调用对应方法:

#include <iostream>
#include <string>
using namespace std;

// 基类:交通工具
class Vehicle {
public:
    virtual void run() {
        cout << "交通工具正在行驶" << endl;
    }
};

// 派生类:汽车
class Car : public Vehicle {
public:
    void run() override {
        cout <<  << endl;
    }
};


  :  Vehicle {
:
    {
        cout <<  << endl;
    }
};

{
    
    Vehicle* v1 =  ();
    Vehicle* v2 =  ();

    
    v1->();
    v2->();

     v1;
     v2;
     ;
}
"汽车在公路上飞驰"
// 派生类:飞机
class
Plane
public
public
void run() override
"飞机在蓝天上翱翔"
int main()
// 基类指针指向派生类对象
new
Car
new
Plane
// 运行时绑定:调用对应派生类的 run 函数
run
run
delete
delete
return
0

运行结果会显示各自派生类的实现:

汽车在公路上飞驰
飞机在蓝天上翱翔

注意:建议使用 override 关键字显式声明重写。如果函数签名不匹配,编译器会直接报错,这能有效防止意外覆盖或遗漏。

另外,虚函数不能是 static 静态函数,因为静态函数属于类本身,不属于具体对象,无法实现运行时绑定。

二、纯虚函数与抽象类

纯虚函数是没有函数体的虚函数,通常用于定义接口规范。包含纯虚函数的类称为抽象类,它不能被实例化,只能作为基类被继承。

2.1 声明语法

在虚函数声明末尾添加 = 0:

class AbstractBase {
public:
    virtual void interfaceFunc() = 0;
};
2.2 核心特性
  • 无法实例化:不能直接创建抽象类的对象,只能定义指针或引用。
  • 强制重写:派生类必须重写所有纯虚函数,否则派生类也会成为抽象类。
  • 接口作用:抽象类只定义接口,具体功能由派生类实现。
2.3 实战案例:图形绘制系统

这是一个典型的抽象类应用场景,定义了图形的通用接口,由具体形状类实现细节:

#include <iostream>
#include <string>
using namespace std;

// 抽象类:图形
class Shape {
public:
    string color;
    
    // 纯虚函数:绘制图形
    virtual void draw() = 0;
    
    // 纯虚函数:计算面积
    virtual double getArea() = 0;
    
    // 普通成员函数:设置颜色
    void setColor(string c) {
        color = c;
    }
};

// 派生类:三角形
class Triangle : public Shape {
private:
    double base;
    double height;
public:
    Triangle(double b, double h) : base(b), height(h) {}

    void draw() override {
        cout << "绘制一个" << color << "的三角形" << endl;
    }
    
    double getArea() override {
        return 0.5 * base * height;
    }
};

// 派生类:正方形
class Square : public Shape {
private:
    double side;
public:
    Square(double s) : side(s) {}

    void draw() override {
        cout << "绘制一个" << color << "的正方形" << endl;
    }
    
    double getArea() override {
        return side * side;
    }
};

int main() {
    // 抽象类不能实例化对象
    // Shape s; // 编译错误

    // 抽象类指针指向派生类对象
    Shape* shape1 = new Triangle(10, 5);
    Shape* shape2 = new Square(8);

    shape1->setColor("红色");
    shape2->setColor("蓝色");

    shape1->draw();
    cout << "三角形面积:" << shape1->getArea() << endl;
    
    shape2->draw();
    cout << "正方形面积:" << shape2->getArea() << endl;

    delete shape1;
    delete shape2;
    return 0;
}

运行结果:

绘制一个红色的三角形
三角形面积:25
绘制一个蓝色的正方形
正方形面积:64

三、虚函数与纯虚函数的核心区别

特性虚函数纯虚函数
函数体有函数体,可提供默认实现无函数体,仅定义接口
类的性质基类可以实例化对象包含纯虚函数的类是抽象类,无法实例化
派生类要求派生类可重写,也可不重写派生类必须重写所有纯虚函数
使用场景基类需要提供默认功能实现基类仅定义接口,功能由派生类实现

四、虚函数表的底层工作机制

C++ 动态多态的底层依赖虚函数表(vtable)和虚函数指针(vptr)。理解这一点有助于规避开发中的隐藏陷阱。

4.1 基本概念
  • 虚函数表(vtable):编译器为每个包含虚函数的类生成一个全局表,存储该类所有虚函数的地址。
  • 虚函数指针(vptr):每个对象内存布局中包含一个隐藏的指针,指向所属类的虚函数表。
  • 继承与重写:派生类的虚函数表继承自基类。若重写虚函数,新地址会覆盖表中对应位置。
4.2 工作流程
  1. 编译时,为每个含虚函数的类生成虚函数表。
  2. 创建对象时,自动添加 vptr 并指向所属类的虚函数表。
  3. 运行时,通过基类指针调用虚函数,先通过 vptr 找到虚函数表。
  4. 根据表中地址调用对应类的函数版本。
4.3 内存布局示例

以 Vehicle 基类和 Car 派生类为例:

  • Vehicle 类的虚函数表:&Vehicle::run
  • Car 类的虚函数表:&Car::run(覆盖基类地址)

当 Vehicle* v = new Car() 时,v 指向对象的 vptr 会指向 Car 类的虚函数表,调用 v->run() 时执行 Car::run。

关键点:虚函数表属于类,所有对象共享;虚函数指针属于对象,每个对象独立占用 4 字节(32 位)或 8 字节(64 位)。

五、虚析构函数:解决资源泄漏问题

当基类指针指向派生类对象并通过 delete 释放时,如果基类析构函数不是虚函数,派生类的析构函数将不会被调用,导致内存泄漏。

5.1 问题演示(非虚析构函数)
#include <iostream>
using namespace std;

class Base {
public:
    Base() { cout << "Base 构造函数被调用" << endl; }
    ~Base() { cout << "Base 析构函数被调用" << endl; } // 非虚析构
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() { data = new int[10]; cout << "Derived 构造函数被调用" << endl; }
    ~Derived() { delete[] data; cout << "Derived 析构函数被调用" << endl; }
};

int main() {
    Base* p = new Derived();
    delete p; // 仅调用基类析构函数
    return 0;
}

运行结果只显示了 Base 的析构,Derived 中的 data 数组未被释放。

5.2 解决方案:虚析构函数

将基类析构函数声明为 virtual:

#include <iostream>
using namespace std;

class Base {
public:
    Base() { cout << "Base 构造函数被调用" << endl; }
    virtual ~Base() { cout << "Base 析构函数被调用" << endl; } // 虚析构
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() { data = new int[10]; cout << "Derived 构造函数被调用" << endl; }
    ~Derived() override { delete[] data; cout << "Derived 析构函数被调用" << endl; }
};

int main() {
    Base* p = new Derived();
    delete p; // 先调用派生类析构,再调用基类析构
    return 0;
}

此时输出顺序变为 Derived 析构在前,Base 析构在后,资源得到正确释放。

✅ 开发规范:只要类中包含虚函数,就应该将析构函数声明为虚析构函数。

六、虚函数的常见陷阱与解决方案

  1. 函数签名不匹配:派生类函数与基类虚函数签名不一致会导致无法触发多态。务必使用 override 关键字检测合法性。
  2. 构造函数/析构函数中调用虚函数:此时对象类型尚未完全构建或正在销毁,虚函数调用不会发生多态。避免在此处调用虚函数。
  3. 性能开销:虚函数调用涉及间接寻址,比普通函数慢。在高性能场景下,可考虑模板等静态多态方式替代。

七、实战案例:员工薪资计算系统

设计一个支持不同角色薪资计算的系统,利用虚函数实现扩展性。

7.1 需求分析
  • 抽象基类 Employee:包含纯虚函数 calculateSalary。
  • 派生类 RegularEmployee:普通员工,薪资 = 基本工资。
  • 派生类 TechEmployee:技术员工,薪资 = 基本工资 + 技术补贴。
  • 派生类 Manager:管理人员,薪资 = 基本工资 + 管理补贴。
7.2 代码实现
#include <iostream>
#include <string>
using namespace std;

// 抽象基类:员工
class Employee {
protected:
    string name;
    double baseSalary;
public:
    Employee(string n, double bs) : name(n), baseSalary(bs) {}

    virtual double calculateSalary() = 0;
    virtual ~Employee() {}

    string getName() { return name; }
};

// 派生类:普通员工
class RegularEmployee : public Employee {
public:
    RegularEmployee(string n, double bs) : Employee(n, bs) {}
    double calculateSalary() override {
        return baseSalary;
    }
};

// 派生类:技术员工
class TechEmployee : public Employee {
private:
    double techAllowance;
public:
    TechEmployee(string n, double bs, double ta) : Employee(n, bs), techAllowance(ta) {}
    double calculateSalary() override {
        return baseSalary + techAllowance;
    }
};

// 派生类:管理人员
class Manager : public Employee {
private:
    double manageAllowance;
public:
    Manager(string n, double bs, double ma) : Employee(n, bs), manageAllowance(ma) {}
    double calculateSalary() override {
        return baseSalary + manageAllowance;
    }
};

void printSalary(Employee* emp) {
    cout << "员工 " << emp->getName() << " 的薪资为:" << emp->calculateSalary() << " 元" << endl;
}

int main() {
    Employee* emp1 = new RegularEmployee("张三", 5000);
    Employee* emp2 = new TechEmployee("李四", 6000, 2000);
    Employee* emp3 = new Manager("王五", 8000, 3000);

    printSalary(emp1);
    printSalary(emp2);
    printSalary(emp3);

    delete emp1;
    delete emp2;
    delete emp3;
    return 0;
}

运行结果:

员工 张三 的薪资为:5000 元
员工 李四 的薪资为:8000 元
员工 王五 的薪资为:11000 元

八、总结

虚函数通过 virtual 关键字声明,支持派生类重写,实现运行时多态;纯虚函数无函数体,用于定义接口,使类成为抽象类。底层依赖虚函数表和虚指针完成动态分发。虚析构函数是防止内存泄漏的关键,凡是有虚函数的类都应声明虚析构。掌握这些机制,配合开闭原则,能显著提升大型 C++ 项目的可维护性与扩展性。

目录

  1. C++ 虚函数与纯虚函数:深入理解多态机制
  2. 一、虚函数的本质与定义
  3. 1.1 声明语法
  4. 1.2 核心特性
  5. 1.3 基础示例
  6. 二、纯虚函数与抽象类
  7. 2.1 声明语法
  8. 2.2 核心特性
  9. 2.3 实战案例:图形绘制系统
  10. 三、虚函数与纯虚函数的核心区别
  11. 四、虚函数表的底层工作机制
  12. 4.1 基本概念
  13. 4.2 工作流程
  14. 4.3 内存布局示例
  15. 五、虚析构函数:解决资源泄漏问题
  16. 5.1 问题演示(非虚析构函数)
  17. 5.2 解决方案:虚析构函数
  18. 六、虚函数的常见陷阱与解决方案
  19. 七、实战案例:员工薪资计算系统
  20. 7.1 需求分析
  21. 7.2 代码实现
  22. 八、总结

更多推荐文章

查看全部
  • Python 开发 MCP Server 集成数据库实战
  • 动态规划(DP)基础:状态表示与转移方程详解
  • AI 驱动的接口测试全流程自动化实现
  • 基于 IsaacLab 的机器人行走训练指南
  • VSCode Copilot 接入 OpenAI 兼容自定义模型方案
  • AI 编程工具深度对比:Cursor、Copilot、Trae 与 Claude Code
  • XMind 文件解析工具 xmindparser 使用指南
  • 基于 Coze 构建 AI 应用:从智能体开发到 Web 部署
  • C++ 异常处理机制与自定义异常体系
  • 低代码 AI 架构助力灵活智能架构落地
  • Python 环境安装与配置 Gurobi 求解器指南
  • 无人机三维路径规划:蚁群算法、A* 与 RRT* 算法对比及 Matlab 实现
  • FunASR:阿里巴巴开源语音识别工具包及部署教程
  • 在 Go 管理后台里落地 Casbin RBAC
  • OpenClaw 本地推理方案:基于 Ollama 部署开源模型替代云端 Token 消耗
  • 5 个维度掌握 python-okx 库:API 整合与量化交易实战
  • Trae IDE 结合 Figma 实现设计稿智能生成前端代码
  • OpenClaw 部署指南:Coding Plan 配置、CC Switch 与飞书机器人接入
  • FFmpeg/OpenCV+C++ 实现直播拉流与推流及视频帧处理
  • 基于 AnythingtoRealCharacters2511 的 ACG 真人化素材自动化方案

相关免费在线工具

  • 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