C++ 虚函数与纯虚函数:多态的核心实现基石
C++ 虚函数与纯虚函数:多态的核心实现基石
💡 学习目标:深度理解虚函数与纯虚函数的本质区别,掌握虚函数表的底层原理,能够灵活运用二者设计具备多态特性的类结构。
💡 学习重点:虚函数的声明与重写规则、纯虚函数与抽象类的使用场景、虚函数表的工作机制、虚函数的常见陷阱与解决方案。
一、虚函数的本质与定义
✅ 结论:虚函数是 C++ 实现动态多态的核心,通过在基类成员函数前添加 virtual 关键字,允许派生类重写该函数,并在运行时根据对象的实际类型调用对应版本。
1.1 虚函数的声明语法
虚函数的声明必须在基类中进行,语法格式如下:
class 基类名 {public:virtual 返回值类型 函数名(参数列表){// 函数体}};1.2 虚函数的核心特性
- 运行时绑定:函数调用关系在程序运行时确定,而非编译时。
- 重写规则:派生类重写的函数必须与基类虚函数的函数名、参数列表、返回值类型完全一致(协变类型除外)。
- 继承性:基类声明虚函数后,派生类中重写的函数自动成为虚函数,无需重复添加
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 抽象类的核心特性
- 无法实例化:不能直接创建抽象类的对象,只能定义指针或引用。
- 强制重写:派生类必须重写抽象类的所有纯虚函数,否则派生类也会成为抽象类。
- 接口作用:抽象类只定义函数的接口,不实现具体功能,功能的实现由派生类完成。
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 虚函数表的基本概念
- 虚函数表(vtable):当类中包含虚函数时,编译器会为该类生成一个全局的虚函数表。表中存储的是类中所有虚函数的地址。
- 虚函数指针(vptr):每个对象的内存布局中,会包含一个隐藏的虚函数指针。该指针指向所属类的虚函数表。
- 继承与重写:派生类的虚函数表会继承基类的虚函数表。如果派生类重写了某个虚函数,会用新的函数地址覆盖虚函数表中对应的位置。
4.2 虚函数表的工作流程
① 程序编译时,编译器为每个包含虚函数的类生成虚函数表。
② 当创建对象时,编译器自动为对象添加虚函数指针 vptr,并让其指向所属类的虚函数表。
③ 程序运行时,通过基类指针或引用调用虚函数时,会先通过 vptr 找到虚函数表。
④ 根据虚函数表中的函数地址,调用对应类的函数版本,实现动态多态。
4.3 虚函数表的内存布局示例
以 Vehicle 基类和 Car 派生类为例,其虚函数表的内存布局如下:
Vehicle类的虚函数表:&Vehicle::runCar类的虚函数表:&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 需求分析
- 抽象基类
Employee:包含纯虚函数calculateSalary,用于计算薪资。 - 派生类
RegularEmployee:普通员工,薪资 = 基本工资。 - 派生类
TechEmployee:技术员工,薪资 = 基本工资 + 技术补贴。 - 派生类
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++ 项目设计的核心机制。