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

C++ 虚函数与纯虚函数:多态机制的深度解析

综述由AI生成C++ 虚函数与纯虚函数是实现动态多态的关键机制。虚函数允许基类指针调用派生类版本,依赖虚函数表 vtable 和虚指针 vptr 在运行时绑定。纯虚函数定义接口规范,使类成为不可实例化的抽象类。深入解析了二者语法区别、底层内存布局及虚析构函数对防止内存泄漏的重要性,并通过图形系统和薪资计算等实战案例展示了多态在实际开发中的应用与最佳实践。

清心发布于 2026/3/30更新于 2026/6/517 浏览
C++ 虚函数与纯虚函数:多态机制的深度解析

C++ 虚函数与纯虚函数:多态机制的深度解析

在这里插入图片描述

一、虚函数的本质与定义

虚函数是 C++ 实现动态多态的核心。通过在基类成员函数前添加 virtual 关键字,允许派生类重写该函数,并在运行时根据对象的实际类型调用对应版本。

1.1 声明语法

虚函数的声明必须在基类中进行:

class BaseClass {
public:
    virtual void functionName(int param) {
        // 函数体
    }
};
1.2 核心特性
  1. 运行时绑定:函数调用关系在程序运行时确定,而非编译时。
  2. 重写规则:派生类重写的函数必须与基类虚函数的函数名、参数列表、返回值类型完全一致(协变类型除外)。
  3. 继承性:基类声明虚函数后,派生类中重写的函数自动成为虚函数,无需重复添加 virtual 关键字(建议保留以增强可读性)。
1.3 基础使用案例
#include <iostream>
#include <string>
using namespace std;

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

// 派生类:汽车
class  :  Vehicle {
:
    
    {
        cout <<  << endl;
    }
};


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

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

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

    
     v1;
     v2;
     ;
}
Car
public
public
// 重写基类虚函数
void run() override
"汽车在公路上飞驰"
// 派生类:飞机
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 BaseClass {
public:
    virtual void functionName(int param) = 0;
};
2.2 抽象类的核心特性
  1. 无法实例化:不能直接创建抽象类的对象,只能定义指针或引用。
  2. 强制重写:派生类必须重写抽象类的所有纯虚函数,否则派生类也会成为抽象类。
  3. 接口作用:抽象类只定义函数的接口,不实现具体功能,功能的实现由派生类完成。
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 基本概念
  1. 虚函数表(vtable):当类中包含虚函数时,编译器会为该类生成一个全局的虚函数表。表中存储的是类中所有虚函数的地址。
  2. 虚函数指针(vptr):每个对象的内存布局中,会包含一个隐藏的虚函数指针。该指针指向所属类的虚函数表。
  3. 继承与重写:派生类的虚函数表会继承基类的虚函数表。如果派生类重写了某个虚函数,会用新的函数地址覆盖虚函数表中对应的位置。
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。

关键点:

  • 虚函数表属于类,所有对象共享同一个虚函数表,节省内存空间。
  • 虚函数指针属于对象,每个对象都有独立的 vptr,占用 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;
}

问题分析: Derived 类中动态分配的 data 数组未被释放,导致内存泄漏。

5.2 解决方案:虚析构函数

将基类的析构函数声明为虚函数,即可实现派生类析构函数的正确调用:

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

开发规范: 只要类中包含虚函数,就应该将析构函数声明为虚析构函数,避免内存泄漏。

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

6.1 函数签名不匹配导致重写失败

问题:派生类重写的函数与基类虚函数的参数列表或返回值类型不一致,导致无法触发多态。 解决方案:严格保证函数签名一致,使用 override 关键字检测重写合法性。

6.2 构造函数和析构函数中调用虚函数

问题:构造函数和析构函数执行时,对象的类型是当前类的类型,而非派生类类型,此时调用虚函数无法实现多态。 解决方案:避免在构造函数和析构函数中调用虚函数,若需要调用,直接使用普通函数。

6.3 忽视虚函数的性能开销

问题:虚函数调用需要通过虚函数表间接寻址,比普通函数调用多一层开销,在高性能场景下可能影响效率。 解决方案:在对性能要求极高的场景,尽量减少虚函数的使用;可以通过模板等静态多态方式替代。

七、实战案例:基于虚函数的员工薪资计算系统

需求设计一个员工薪资计算系统,支持普通员工、技术员工、管理人员三种角色,不同角色的薪资计算规则不同,要求利用虚函数实现动态多态,新增角色时无需修改原有代码。

7.1 完整代码实现
#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. 6.1 函数签名不匹配导致重写失败
  20. 6.2 构造函数和析构函数中调用虚函数
  21. 6.3 忽视虚函数的性能开销
  22. 七、实战案例:基于虚函数的员工薪资计算系统
  23. 7.1 完整代码实现
  24. 八、本章总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Spatial Joy 2025 全球 AR&AI 赛事:开发者资源与参赛指南
  • AI 实践:工具函数调用(Function Calling)实战指南
  • Python PyQt6 桌面应用开发实战指南
  • Windows11 VMware 17.6.0 更新 Tools 报错无法连接服务器解决方案
  • 滑动窗口算法核心思路与四道经典题解
  • 基于 LangChain 实现的知识库问答工具
  • 从 vw/vh 到 clamp():前端响应式设计的痛点与进化
  • 根据设计图生成前端代码,零基础入门到精通,收藏这篇就够了
  • 机器人系统设计核心:从架构拆解到工程落地实践
  • SQL Prompt 工具介绍与正版使用建议
  • 具身智能深度解析:深度视觉如何赋予足式机器人跑酷能力
  • Photoshop 集成 ComfyUI AI 绘画功能指南
  • LeetCode 88:合并两个有序数组(C 语言双指针详解)
  • Windows 本地运行 DeepSeek 的 3 个简单步骤
  • 具身智能:深度视觉如何赋予足式机器人极限运动能力
  • AIGC 模型推理延迟优化:C++ 级方案解析
  • Node.js 20+ 使用 crypto.webcrypto 优化加密性能实战
  • Apollo EM Planner 算法深度剖析:从路径规划到公式推导
  • C++Qt 实现的邮政客户投诉工单处理系统
  • JavaScript 集合 (Set、WeakSet) 与映射 (Map、WeakMap)

相关免费在线工具

  • 加密/解密文本

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