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

【AI大模型】DeepSeek + 通义万相高效制作AI视频实战详解

【AI大模型】DeepSeek + 通义万相高效制作AI视频实战详解

目录 一、前言 二、AI视频概述 2.1 什么是AI视频 2.2 AI视频核心特点 2.3 AI视频应用场景 三、通义万相介绍 3.1 通义万相概述 3.1.1 什么是通义万相 3.2 通义万相核心特点 3.3 通义万相技术特点 3.4 通义万相应用场景 四、DeepSeek + 通义万相制作AI视频流程 4.1 DeepSeek + 通义万相制作视频优势 4.1.1 DeepSeek 优势 4.1.2 通义万相视频生成优势 4.2

By Ne0inhk
【DeepSeek微调实践】DeepSeek-R1大模型基于MS-Swift框架部署/推理/微调实践大全

【DeepSeek微调实践】DeepSeek-R1大模型基于MS-Swift框架部署/推理/微调实践大全

系列篇章💥 No.文章01【DeepSeek应用实践】DeepSeek接入Word、WPS方法详解:无需代码,轻松实现智能办公助手功能02【DeepSeek应用实践】通义灵码 + DeepSeek:AI 编程助手的实战指南03【DeepSeek应用实践】Cline集成DeepSeek:开源AI编程助手,终端与Web开发的超强助力04【DeepSeek开发入门】DeepSeek API 开发初体验05【DeepSeek开发入门】DeepSeek API高级开发指南(推理与多轮对话机器人实践)06【DeepSeek开发入门】Function Calling 函数功能应用实战指南07【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:本地部署与API服务快速上手08【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:Web聊天机器人部署指南09【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:基于vLLM 搭建高性能推理服务器10【DeepSeek部署实战】基于Ollama快速部署Dee

By Ne0inhk

用DeepSeek和Cursor从零打造智能代码审查工具:我的AI编程实践

💂 个人网站:【 摸鱼游戏】【神级代码资源网站】【星海网址导航】摸鱼、技术交流群👉 点此查看详情 引言:AI编程革命下的机遇与挑战 GitHub统计显示,使用AI编程工具的开发者平均效率提升55%,但仅有23%的开发者能充分发挥这些工具的潜力。作为一名全栈工程师,我曾对AI编程持怀疑态度,直到一次紧急项目让我彻底改变了看法。客户要求在72小时内交付一个能自动检测代码漏洞、优化性能的智能审查系统,传统开发方式根本不可能完成。正是这次挑战,让我探索出DeepSeek和Cursor这对"黄金组合"的惊人潜力。 一、工具选型:深入比较主流AI编程工具 1.1 为什么最终选择DeepSeek+Cursor? 经过两周的对比测试,我们发现不同工具在代码审查场景的表现差异显著: 工具代码理解深度响应速度定制灵活性多语言支持GitHub Copilot★★★☆★★★★★★☆★★★★Amazon CodeWhisperer★★☆★★★☆★★★★★★☆DeepSeek★★★★☆★★★★★★★☆★★★★☆Cursor★★★☆★★★★☆★★★★★★★★ 关键发现: * Dee

By Ne0inhk

DeepSeek各版本说明与优缺点分析_deepseek各版本区别

DeepSeek各版本说明与优缺点分析 DeepSeek是最近人工智能领域备受瞩目的一个语言模型系列,其在不同版本的发布过程中,逐步加强了对多种任务的处理能力。本文将详细介绍DeepSeek的各版本,从版本的发布时间、特点、优势以及不足之处,为广大AI技术爱好者和开发者提供一份参考指南。 1. DeepSeek-V1:起步与编码强劲 DeepSeek-V1是DeepSeek的起步版本,这里不过多赘述,主要分析它的优缺点。 发布时间: 2024年1月 特点: DeepSeek-V1是DeepSeek系列的首个版本,预训练于2TB的标记数据,主打自然语言处理和编码任务。它支持多种编程语言,具有强大的编码能力,适合程序开发人员和技术研究人员使用。 优势: * 强大编码能力:支持多种编程语言,能够理解和生成代码,适合开发者进行自动化代码生成与调试。 * 高上下文窗口:支持高达128K标记的上下文窗口,能够处理较为复杂的文本理解和生成任务。 缺点: * 多模态能力有限:该版本主要集中在文本处理上,缺少对图像、语音等多模态任务的支持。 * 推理能力较弱:尽管在自然语言

By Ne0inhk