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

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

C++ 虚函数通过 virtual 关键字实现运行时多态,派生类可重写基类函数。纯虚函数无函数体,定义抽象类接口,强制子类实现。底层依赖虚函数表 vtable 和虚指针 vptr 完成动态绑定。虚析构函数防止资源泄漏,设计时需遵循开闭原则,利用 override 确保重写安全。

PhpPioneer发布于 2026/3/29更新于 2026/7/1830 浏览
C++ 虚函数与纯虚函数:深入理解多态机制

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

在 C++ 面向对象设计中,多态是核心特性之一。它允许我们使用基类指针或引用来操作派生类对象,并在运行时根据实际类型调用相应的函数。实现这一特性的基石就是虚函数(Virtual Function)和纯虚函数(Pure Virtual Function)。

一、虚函数的本质与定义

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

1.1 声明语法

虚函数必须在基类中声明。基本语法如下:

class Base {
public:
    virtual void func() {
        // 函数体
    }
};
1.2 核心特性
  1. 运行时绑定:函数调用关系在程序运行时确定,而非编译时。
  2. 重写规则:派生类重写的函数必须与基类虚函数的函数名、参数列表、返回值类型完全一致(协变类型除外)。
  3. 继承性:基类声明虚函数后,派生类中重写的函数自动成为虚函数,无需重复添加 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;
    }
};

// 派生类:飞机
class Plane : public Vehicle {
public:
    // 重写基类虚函数
    void run() override {
        cout << "飞机在蓝天上翱翔" << endl;
    }
};

int main() {
    // 基类指针指向派生类对象
    Vehicle *v1 = new Car();
    Vehicle *v2 = new Plane();

    // 运行时绑定:调用对应派生类的 run 函数
    v1->run();
    v2->run();

    // 释放内存
    delete v1;
    delete v2;
    return 0;
}

运行结果会输出各自派生类的实现:

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

注意:

  • override 关键字用于检测重写的合法性,若函数签名不匹配,编译器会直接报错,建议强制使用。
  • 虚函数不能是 static 静态函数,因为静态函数属于类,不属于对象,无法实现运行时绑定。

二、纯虚函数与抽象类

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

2.1 声明语法

在虚函数声明的末尾添加 = 0,即可将其定义为纯虚函数:

class Base {
public:
    virtual void func() = 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;
}

运行结果(存在内存泄漏):

Base 构造函数被调用
Derived 构造函数被调用
Base 析构函数被调用

问题分析: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;
}

运行结果(资源正确释放):

Base 构造函数被调用
Derived 构造函数被调用
Derived 析构函数被调用
Base 析构函数被调用

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

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

6.1 陷阱 1:函数签名不匹配导致重写失败

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

6.2 陷阱 2:构造函数和析构函数中调用虚函数

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

6.3 陷阱 3:忽视虚函数的性能开销

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

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

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

7.1 需求分析
  1. 抽象基类 Employee:包含纯虚函数 calculateSalary,用于计算薪资。
  2. 派生类 RegularEmployee:普通员工,薪资 = 基本工资。
  3. 派生类 TechEmployee:技术员工,薪资 = 基本工资 + 技术补贴。
  4. 派生类 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;
}
7.3 运行结果
员工 张三 的薪资为: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 陷阱 1:函数签名不匹配导致重写失败
  20. 6.2 陷阱 2:构造函数和析构函数中调用虚函数
  21. 6.3 陷阱 3:忽视虚函数的性能开销
  22. 七、实战案例:基于虚函数的员工薪资计算系统
  23. 7.1 需求分析
  24. 7.2 完整代码实现
  25. 7.3 运行结果
  26. 八、本章总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Flutter 组件 tavily_dart 在鸿蒙平台的适配与 AI 搜索集成实战
  • Flutter 三方库 webkit_inspection_protocol 在鸿蒙系统的适配指南
  • CycleGAN 详解与实现:无配对图像转换技术
  • Java 无锁队列 Disruptor 核心原理分析
  • VS Code 中常用的 Python 代码格式化工具
  • 程序员转行方向推荐:数据分析师、AI 大模型工程师、产品经理与云计算工程师
  • 基于 Leaflet 和天地图的长沙市免费运动场所 WebGIS 可视化
  • eBay 商品数据采集实战:Python 调用网页抓取 API 接入方案
  • SpringBoot 校园讲座管理系统设计与实现
  • EvoCUA 刷新开源 SOTA:会操作电脑且持续进化的智能体
  • 使用 Ollama 本地运行 Llama-3.2-3B 模型指南
  • 码农与软件工程师的核心区别及能力要求
  • OpenAI 发布 GPT-4o 模型,实现多模态实时交互
  • 位图、布隆过滤器与哈希切分的高效数据处理技术
  • 昇腾 NPU 部署 Llama 2 模型的性能测试与优化实践
  • 默认安全治理实践:水平越权检测与前端安全防控
  • 医疗领域自然语言处理应用与实战
  • Meta Llama 3 发布:性能媲美 GPT-4 的开源大模型详解
  • Python 为何在数据分析领域广受欢迎?
  • LLM 面试真题与答案详解:基础、微调及 LangChain 篇

相关免费在线工具

  • 加密/解密文本

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