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

【C + + 】一文吃透 C++ 智能指针:RAII 思想 + 三大指针 + 实战避坑

【C + + 】一文吃透 C++ 智能指针:RAII 思想 + 三大指针 + 实战避坑

🌟个人主页:第七序章   🌈专栏系列:C++ 目录 ❄️前言: ☀️一、智能指针的使用 ☀️二、RAII和智能指针 ⭐RAII思想 ⭐智能指针思想 ☀️三、C++标准库中智能指针的使用 ⭐auto_ptr ⭐unique_ptr ⭐shared_ptr ⭐删除器 ☀️四、智能指针的实现原理 ⭐share_ptr实现原理 ⭐构造函数 ⭐拷贝构造 ⭐拷贝赋值 ⭐析构函数 ⭐删除器 ☀️五、shared_ptr循环引用问题 ⭐weak_ptr解决循环引用问题 ⭐weak_ptr ☀️ 六、智能指针总结 ☀️七、内存泄漏详解与防范 ⭐内存泄漏是什么?为什么危险? ⭐典型的内存泄漏场景分析 ⭐常见内存泄漏类型分类 ⭐内存泄漏检测工具一览 ⭐如何高效避免内存泄漏? ☀️九、shared_

By Ne0inhk

【C++26静态反射元编程实战】:掌握下一代C++黑科技的5大核心技巧

第一章:C++26静态反射元编程概述 C++26 正在将静态反射(Static Reflection)作为核心语言特性之一引入,旨在为开发者提供在编译期获取和操作类型信息的能力,而无需依赖复杂的模板元编程技巧。这一机制允许程序在不运行时开销的前提下,分析类、结构体、函数等实体的结构,从而实现高度通用的序列化、ORM 映射、配置解析等功能。 静态反射的核心理念 静态反射通过新的关键字和标准库设施,如 reflect 和 meta,使编译器能在编译期间暴露类型的元数据。这些元数据可被用于生成代码或执行条件判断,提升类型安全与性能。 基本语法与使用示例 #include <reflect> struct Person { std::string name; int age; }; // 获取Person类型的编译期元对象 constexpr auto meta_person = reflect(Person); // 遍历所有字段名称(假定C++26支持此语法)

By Ne0inhk
墨色规则与血色节点:C++红黑树设计与实现探秘

墨色规则与血色节点:C++红黑树设计与实现探秘

前言     前几天攻克了AVL树,我们已然是平衡二叉树的强者。但旅程还未结束,下一个等待我们的,是更强大、也更传奇的**终极BOSS**——红黑树。它不仅是map和set的强大心脏,更是C++ STL皇冠上的明珠。准备好了吗?让我们一起揭开它的神秘面纱。 1.红黑树的概念 1.1.红黑树是什么     红黑树是一科二叉搜索树,他的每个节点增加一个存储为代表着该节点的颜色,和它的名字一样,节点的颜色可以是红色或者是黑色。通过对任何一条根到叶子的路径上各个节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。     红黑树是被很多条规则进行束缚的二叉搜索树,通过这些规则,可以保证红黑树没有一条路径会比其他路径长出2倍,并且保持其相对平衡,下面我来讲述一下这些规则。 1.2.红黑树的规则     1.每个节点不是黑色的就是红色的(这肯定,不然不会被叫做红黑树了)。     2.根节点必须是黑色的     3.如果一个节点是红色的,则它的两个孩子节点必须是黑色的,也就是说任意一条路径上并不会出现连续的红色的节点。     4.对于任意的一个

By Ne0inhk
2019年信奥赛C++提高组csp-s初赛真题及答案解析(选择题6-10)

2019年信奥赛C++提高组csp-s初赛真题及答案解析(选择题6-10)

2019年信奥赛C++提高组csp-s初赛真题及答案解析(选择题6-10) 第 6 题 由数字 1,1,2,4,8,8 所组成的不同的 4位数的个数是()。 A. 104 B. 102 C. 98 D. 100 答案:B 解析:由数字 1,1,2,4,8,8 组成四位数,需考虑重复数字。分类讨论: * 四个数字各不相同:取 1,2,4,8,排列数 4!=24。 * 两对重复:取两个1和两个8,排列数 4 ! 2

By Ne0inhk