C++ 虚函数与纯虚函数:多态的核心实现基石

C++ 虚函数与纯虚函数:多态的核心实现基石

C++ 虚函数与纯虚函数:多态的核心实现基石

在这里插入图片描述

💡 学习目标:深度理解虚函数与纯虚函数的本质区别,掌握虚函数表的底层原理,能够灵活运用二者设计具备多态特性的类结构。
💡 学习重点:虚函数的声明与重写规则、纯虚函数与抽象类的使用场景、虚函数表的工作机制、虚函数的常见陷阱与解决方案。

一、虚函数的本质与定义

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

1.1 虚函数的声明语法

虚函数的声明必须在基类中进行,语法格式如下:

class 基类名 {public:virtual 返回值类型 函数名(参数列表){// 函数体}};

1.2 虚函数的核心特性

  1. 运行时绑定:函数调用关系在程序运行时确定,而非编译时。
  2. 重写规则:派生类重写的函数必须与基类虚函数的函数名、参数列表、返回值类型完全一致(协变类型除外)。
  3. 继承性:基类声明虚函数后,派生类中重写的函数自动成为虚函数,无需重复添加 virtual 关键字(建议保留以增强可读性)。

1.3 虚函数的基础使用案例

#include<iostream>#include<string>usingnamespace std;// 基类:交通工具classVehicle{public:// 虚函数:行驶virtualvoidrun(){ cout <<"交通工具正在行驶"<< endl;}};// 派生类:汽车classCar:publicVehicle{public:// 重写基类虚函数voidrun()override{// override 关键字显式声明重写 cout <<"汽车在公路上飞驰"<< endl;}};// 派生类:飞机classPlane:publicVehicle{public:// 重写基类虚函数voidrun()override{ cout <<"飞机在蓝天上翱翔"<< endl;}};intmain(){// 基类指针指向派生类对象 Vehicle *v1 =newCar(); Vehicle *v2 =newPlane();// 运行时绑定:调用对应派生类的 run 函数 v1->run(); v2->run();// 释放内存delete v1;delete v2;return0;}

1.4 运行结果

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

⚠️ 注意事项

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

二、纯虚函数与抽象类

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

2.1 纯虚函数的声明语法

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

class 基类名 {public:virtual 返回值类型 函数名(参数列表)=0;};

2.2 抽象类的核心特性

  1. 无法实例化:不能直接创建抽象类的对象,只能定义指针或引用。
  2. 强制重写:派生类必须重写抽象类的所有纯虚函数,否则派生类也会成为抽象类。
  3. 接口作用:抽象类只定义函数的接口,不实现具体功能,功能的实现由派生类完成。

2.3 纯虚函数的实战案例:图形绘制系统

#include<iostream>#include<string>usingnamespace std;// 抽象类:图形(包含纯虚函数)classShape{public: string color;// 纯虚函数:绘制图形virtualvoiddraw()=0;// 纯虚函数:计算面积virtualdoublegetArea()=0;// 普通成员函数:设置颜色voidsetColor(string c){ color = c;}};// 派生类:三角形classTriangle:publicShape{private:double base;// 底double height;// 高public:Triangle(double b,double h):base(b),height(h){}// 必须重写所有纯虚函数voiddraw()override{ cout <<"绘制一个"<< color <<"的三角形"<< endl;}doublegetArea()override{return0.5* base * height;}};// 派生类:正方形classSquare:publicShape{private:double side;// 边长public:Square(double s):side(s){}voiddraw()override{ cout <<"绘制一个"<< color <<"的正方形"<< endl;}doublegetArea()override{return side * side;}};intmain(){// 抽象类不能实例化对象// Shape s; // 编译错误// 抽象类指针指向派生类对象 Shape *shape1 =newTriangle(10,5); Shape *shape2 =newSquare(8); shape1->setColor("红色"); shape2->setColor("蓝色"); shape1->draw(); cout <<"三角形面积:"<< shape1->getArea()<< endl; shape2->draw(); cout <<"正方形面积:"<< shape2->getArea()<< endl;delete shape1;delete shape2;return0;}

2.4 运行结果

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

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

核心区别总结:虚函数有函数体,基类可以实例化;纯虚函数无函数体,包含纯虚函数的类是抽象类,无法实例化。

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

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

💡 C++ 动态多态的底层实现依赖虚函数表(vtable)虚函数指针(vptr),理解其原理能帮助我们规避开发中的隐藏陷阱。

4.1 虚函数表的基本概念

  1. 虚函数表(vtable):当类中包含虚函数时,编译器会为该类生成一个全局的虚函数表。表中存储的是类中所有虚函数的地址。
  2. 虚函数指针(vptr):每个对象的内存布局中,会包含一个隐藏的虚函数指针。该指针指向所属类的虚函数表。
  3. 继承与重写:派生类的虚函数表会继承基类的虚函数表。如果派生类重写了某个虚函数,会用新的函数地址覆盖虚函数表中对应的位置。

4.2 虚函数表的工作流程

① 程序编译时,编译器为每个包含虚函数的类生成虚函数表。
② 当创建对象时,编译器自动为对象添加虚函数指针 vptr,并让其指向所属类的虚函数表。
③ 程序运行时,通过基类指针或引用调用虚函数时,会先通过 vptr 找到虚函数表。
④ 根据虚函数表中的函数地址,调用对应类的函数版本,实现动态多态。

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>usingnamespace std;classBase{public:Base(){ cout <<"Base 构造函数被调用"<< endl;}~Base(){ cout <<"Base 析构函数被调用"<< endl;}// 非虚析构};classDerived:publicBase{private:int*data;// 动态分配的内存public:Derived(){ data =newint[10]; cout <<"Derived 构造函数被调用"<< endl;}~Derived(){delete[] data; cout <<"Derived 析构函数被调用"<< endl;}};intmain(){ Base *p =newDerived();delete p;// 仅调用基类析构函数,派生类析构未调用return0;}

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

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

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

5.3 解决方案:虚析构函数

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

#include<iostream>usingnamespace std;classBase{public:Base(){ cout <<"Base 构造函数被调用"<< endl;}virtual~Base(){ cout <<"Base 析构函数被调用"<< endl;}// 虚析构};classDerived:publicBase{private:int*data;public:Derived(){ data =newint[10]; cout <<"Derived 构造函数被调用"<< endl;}~Derived()override{delete[] data; cout <<"Derived 析构函数被调用"<< endl;}};intmain(){ Base *p =newDerived();delete p;// 先调用派生类析构,再调用基类析构return0;}

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

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>usingnamespace std;// 抽象基类:员工classEmployee{protected: string name;double baseSalary;// 基本工资public:Employee(string n,double bs):name(n),baseSalary(bs){}// 纯虚函数:计算薪资virtualdoublecalculateSalary()=0;// 虚析构函数virtual~Employee(){}// 普通函数:获取姓名 string getName(){return name;}};// 派生类:普通员工classRegularEmployee:publicEmployee{public:RegularEmployee(string n,double bs):Employee(n, bs){}doublecalculateSalary()override{return baseSalary;}};// 派生类:技术员工classTechEmployee:publicEmployee{private:double techAllowance;// 技术补贴public:TechEmployee(string n,double bs,double ta):Employee(n, bs),techAllowance(ta){}doublecalculateSalary()override{return baseSalary + techAllowance;}};// 派生类:管理人员classManager:publicEmployee{private:double manageAllowance;// 管理补贴public:Manager(string n,double bs,double ma):Employee(n, bs),manageAllowance(ma){}doublecalculateSalary()override{return baseSalary + manageAllowance;}};// 通用函数:打印员工薪资voidprintSalary(Employee *emp){ cout <<"员工 "<< emp->getName()<<" 的薪资为:"<< emp->calculateSalary()<<" 元"<< endl;}intmain(){ Employee *emp1 =newRegularEmployee("张三",5000); Employee *emp2 =newTechEmployee("李四",6000,2000); Employee *emp3 =newManager("王五",8000,3000);printSalary(emp1);printSalary(emp2);printSalary(emp3);delete emp1;delete emp2;delete emp3;return0;}

7.3 运行结果

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

八、本章总结

✅ 虚函数通过 virtual 关键字声明,支持派生类重写,实现运行时多态;纯虚函数无函数体,用于定义接口,包含纯虚函数的类是抽象类。
✅ 虚函数的底层实现依赖虚函数表和虚函数指针,虚函数表存储虚函数地址,虚函数指针指向虚函数表。
✅ 虚析构函数是解决派生类资源泄漏的关键,只要类中包含虚函数,就应该将析构函数声明为虚函数。
✅ 虚函数的核心优势是支持代码扩展,符合开闭原则,是大型 C++ 项目设计的核心机制。

Read more

基于腾讯云HAI + DeepSeek快速设计自己的个人网页

基于腾讯云HAI + DeepSeek快速设计自己的个人网页

前言:通过结合腾讯云HAI 强大的云端运算能力与DeepSeek先进的 AI技术,本文介绍高效、便捷且低成本的设计一个自己的个人网页。你将了解到如何轻松绕过常见的技术阻碍,在腾讯云HAI平台上快速部署DeepSeek模型,仅需简单几步,就能获取一个包含个人简介、技能特长、项目经历及联系方式等核心板块的响应式网页。 目录 一、DeepSeek模型部署在腾讯云HAI 二、设计个人网页 一、DeepSeek模型部署在腾讯云HAI 把 DeepSeek 模型部署于腾讯云 HAI,用户便能避开官网访问限制,直接依托腾讯云 HAI 的超强算力运行 DeepSeek-R1 等模型。这一举措不仅降低了技术门槛,还缩短了部署时间,削减了成本。尤为关键的是,凭借 HAI 平台灵活且可扩展的特性,用户能够依据自身特定需求定制专属解决方案,进而更出色地适配特定业务场景,满足各类技术要求 。 点击访问腾讯云HAI控制台地址: 算力管理 - 高性能应用服务 - 控制台 腾讯云高性能应用服务HAI已支持DeepSeek-R1模型预装环境和CPU算力,只需简单的几步就能调用DeepSeek - R1

By Ne0inhk
AI革命先锋:DeepSeek与蓝耘通义万相2.1的无缝融合引领行业智能化变革

AI革命先锋:DeepSeek与蓝耘通义万相2.1的无缝融合引领行业智能化变革

云边有个稻草人-ZEEKLOG博客 目录 引言 一、什么是DeepSeek? 1.1 DeepSeek平台概述 1.2 DeepSeek的核心功能与技术 二、蓝耘通义万相2.1概述 2.1 蓝耘科技简介 2.2 蓝耘通义万相2.1的功能与优势 1. 全链条智能化解决方案 2. 强大的数据处理能力 3. 高效的模型训练与优化 4. 自动化推理与部署 5. 行业专用解决方案 三、蓝耘通义万相2.1与DeepSeek的对比分析 3.1 核心区别 3.2 结合使用的优势 四、蓝耘注册流程 五、DeepSeek与蓝耘通义万相2.1的集成应用 5.1 集成应用场景 1. 智能医疗诊断

By Ne0inhk
如何通过 3 个简单步骤在 Windows 上本地运行 DeepSeek

如何通过 3 个简单步骤在 Windows 上本地运行 DeepSeek

它是免费的——社区驱动的人工智能💪。         当 OpenAI 第一次推出定制 GPT 时,我就明白会有越来越多的人为人工智能做出贡献,并且迟早它会完全由社区驱动。         但从来没有想过它会如此接近😂让我们看看如何在 Windows 机器上完全免费使用第一个开源推理模型!  步骤 0:安装 Docker 桌面         我确信很多人已经安装了它,所以可以跳过,但如果没有 — — 这很简单,只需访问Docker 的官方网站,下载并运行安装 👍         如果您需要一些特定的设置,例如使用 WSL,那么有很多指导视频,请查看!我将继续下一步。 步骤 1:安装 CUDA 以获得 GPU 支持         如果您想使用 Nvidia 显卡运行 LLM,则必须安装 CUDA 驱动程序。(嗯……是的,它们需要大量的计算能力)         打开CUDA 下载页面,

By Ne0inhk
在 VSCode 中本地运行 DeepSeek,打造强大的私人 AI

在 VSCode 中本地运行 DeepSeek,打造强大的私人 AI

本文将分步向您展示如何在本地安装和运行 DeepSeek、使用 CodeGPT 对其进行配置以及开始利用 AI 来增强您的软件开发工作流程,所有这些都无需依赖基于云的服务。  步骤 1:在 VSCode 中安装 Ollama 和 CodeGPT         要在本地运行 DeepSeek,我们首先需要安装Ollama,它允许我们在我们的机器上运行 LLM,以及CodeGPT,它是集成这些模型以提供编码辅助的 VSCode 扩展。 安装 Ollama Ollama 是一个轻量级平台,可以轻松运行本地 LLM。 下载Ollama 访问官方网站:https://ollama.com * 下载适合您的操作系统(Windows、macOS 或 Linux)的安装程序。 * 验证安装 安装后,打开终端并运行: ollama --version  如果 Ollama 安装正确,

By Ne0inhk