跳到主要内容
C++ 虚函数与纯虚函数:多态的核心实现 | 极客日志
C++ 算法
C++ 虚函数与纯虚函数:多态的核心实现 综述由AI生成 深入讲解 C++ 虚函数与纯虚函数的区别及多态实现原理。涵盖虚函数声明语法、重写规则、运行时绑定机制、虚函数表底层工作逻辑。介绍了抽象类特性、虚析构函数防止内存泄漏的方法,以及构造函数中调用虚函数的陷阱。通过图形绘制系统和员工薪资计算案例,展示了多态在实际开发中的应用,并提供了代码示例与运行结果分析。
moshang 发布于 2026/3/30 更新于 2026/5/24 34 浏览C++ 虚函数与纯虚函数:多态的核心实现
一、虚函数的本质与定义
结论 :虚函数是 C++ 实现动态多态的核心,通过在基类成员函数前添加 virtual 关键字,允许派生类重写该函数,并在运行时根据对象的实际类型调用对应版本。
1.1 虚函数的声明语法
虚函数的声明必须在基类 中进行,语法格式如下:
class 基类名 {
public :
virtual 返回值类型 函数名 (参数列表) {
}
};
1.2 虚函数的核心特性
运行时绑定 :函数调用关系在程序运行时确定,而非编译时。
重写规则 :派生类重写的函数必须与基类虚函数的函数名、参数列表、返回值类型 完全一致(协变类型除外)。
继承性 :基类声明虚函数后,派生类中重写的函数自动成为虚函数,无需重复添加 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;
}
};
: Vehicle {
:
{
cout << << endl;
}
};
{
Vehicle *v1 = ();
Vehicle *v2 = ();
v1-> ();
v2-> ();
v1;
v2;
;
}
class
Plane
public
public
void run () override
"飞机在蓝天上翱翔"
int main ()
new
Car
new
Plane
run
run
delete
delete
return
0
1.4 运行结果
override 关键字用于检测重写的合法性,若函数签名不匹配,编译器会直接报错,建议强制使用。
虚函数不能是 static 静态函数,因为静态函数属于类,不属于对象,无法实现运行时绑定。
二、纯虚函数与抽象类 纯虚函数是没有函数体的虚函数,用于定义接口规范 ;包含纯虚函数的类称为抽象类,抽象类无法实例化对象,只能作为基类被继承。
2.1 纯虚函数的声明语法 在虚函数声明的末尾添加 = 0,即可将其定义为纯虚函数:
class 基类名 {
public :
virtual 返回值类型 函数名 (参数列表) = 0 ;
};
2.2 抽象类的核心特性
无法实例化 :不能直接创建抽象类的对象,只能定义指针或引用。
强制重写 :派生类必须重写抽象类的所有 纯虚函数,否则派生类也会成为抽象类。
接口作用 :抽象类只定义函数的接口,不实现具体功能,功能的实现由派生类完成。
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 *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 ;
}
2.4 运行结果 绘制一个红色的三角形
三角形面积:25
绘制一个蓝色的正方形
正方形面积:64
三、虚函数与纯虚函数的核心区别 核心区别总结 :虚函数有函数体,基类可以实例化;纯虚函数无函数体,包含纯虚函数的类是抽象类,无法实例化。
特性 虚函数 纯虚函数 函数体 有函数体,可提供默认实现 无函数体,仅定义接口 类的性质 基类可以实例化对象 包含纯虚函数的类是抽象类,无法实例化 派生类要求 派生类可重写,也可不重写 派生类必须重写所有纯虚函数 使用场景 基类需要提供默认功能实现 基类仅定义接口,功能由派生类实现
四、虚函数表的底层工作机制 C++ 动态多态的底层实现依赖虚函数表(vtable) 和虚函数指针(vptr) ,理解其原理能帮助我们规避开发中的隐藏陷阱。
4.1 虚函数表的基本概念
虚函数表(vtable) :当类中包含虚函数时,编译器会为该类生成一个全局的虚函数表。表中存储的是类中所有虚函数的地址。
虚函数指针(vptr) :每个对象的内存布局中,会包含一个隐藏的虚函数指针。该指针指向所属类的虚函数表。
继承与重写 :派生类的虚函数表会继承基类的虚函数表。如果派生类重写了某个虚函数,会用新的函数地址覆盖虚函数表中对应的位置。
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>
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 ;
}
5.2 运行结果(存在内存泄漏) Base 构造函数被调用
Derived 构造函数被调用
Base 析构函数被调用
问题分析 :Derived 类中动态分配的 data 数组未被释放,导致内存泄漏。
5.3 解决方案:虚析构函数 将基类的析构函数声明为虚函数,即可实现派生类析构函数的正确调用:
#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 ;
}
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>
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++ 项目设计的核心机制。
相关免费在线工具 加密/解密文本 使用加密算法(如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