C++ 虚函数与纯虚函数:多态的核心实现
深入讲解 C++ 虚函数与纯虚函数的区别及多态实现原理。涵盖虚函数声明语法、重写规则、运行时绑定机制、虚函数表底层工作逻辑。介绍了抽象类特性、虚析构函数防止内存泄漏的方法,以及构造函数中调用虚函数的陷阱。通过图形绘制系统和员工薪资计算案例,展示了多态在实际开发中的应用,并提供了代码示例与运行结果分析。

深入讲解 C++ 虚函数与纯虚函数的区别及多态实现原理。涵盖虚函数声明语法、重写规则、运行时绑定机制、虚函数表底层工作逻辑。介绍了抽象类特性、虚析构函数防止内存泄漏的方法,以及构造函数中调用虚函数的陷阱。通过图形绘制系统和员工薪资计算案例,展示了多态在实际开发中的应用,并提供了代码示例与运行结果分析。

结论:虚函数是 C++ 实现动态多态的核心,通过在基类成员函数前添加 virtual 关键字,允许派生类重写该函数,并在运行时根据对象的实际类型调用对应版本。
虚函数的声明必须在基类中进行,语法格式如下:
class 基类名 {
public:
virtual 返回值类型 函数名 (参数列表) {
// 函数体
}
};
virtual 关键字(建议保留以增强可读性)。#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;
}
};
// 派生类:飞机
class Plane : public Vehicle {
public:
// 重写基类虚函数
void run() override {
cout << "飞机在蓝天上翱翔" << endl;
}
};
int main() {
// 基类指针指向派生类对象
Vehicle *v1 = new Car();
Vehicle *v2 = new Plane();
// 运行时绑定:调用对应派生类的 run 函数
v1->run();
v2->run();
// 释放内存
delete v1;
delete v2;
return 0;
}
汽车在公路上飞驰
飞机在蓝天上翱翔
注意事项
override 关键字用于检测重写的合法性,若函数签名不匹配,编译器会直接报错,建议强制使用。static 静态函数,因为静态函数属于类,不属于对象,无法实现运行时绑定。纯虚函数是没有函数体的虚函数,用于定义接口规范;包含纯虚函数的类称为抽象类,抽象类无法实例化对象,只能作为基类被继承。
在虚函数声明的末尾添加 = 0,即可将其定义为纯虚函数:
class 基类名 {
public:
virtual 返回值类型 函数名 (参数列表) = 0;
};
#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 s; // 编译错误
// 抽象类指针指向派生类对象
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;
}
绘制一个红色的三角形
三角形面积:25
绘制一个蓝色的正方形
正方形面积:64
核心区别总结:虚函数有函数体,基类可以实例化;纯虚函数无函数体,包含纯虚函数的类是抽象类,无法实例化。
| 特性 | 虚函数 | 纯虚函数 |
|---|---|---|
| 函数体 | 有函数体,可提供默认实现 | 无函数体,仅定义接口 |
| 类的性质 | 基类可以实例化对象 | 包含纯虚函数的类是抽象类,无法实例化 |
| 派生类要求 | 派生类可重写,也可不重写 | 派生类必须重写所有纯虚函数 |
| 使用场景 | 基类需要提供默认功能实现 | 基类仅定义接口,功能由派生类实现 |
C++ 动态多态的底层实现依赖虚函数表(vtable) 和虚函数指针(vptr),理解其原理能帮助我们规避开发中的隐藏陷阱。
vptr,并让其指向所属类的虚函数表。vptr 找到虚函数表。以 Vehicle 基类和 Car 派生类为例,其虚函数表的内存布局如下:
Vehicle 类的虚函数表:&Vehicle::runCar 类的虚函数表:&Car::run(覆盖基类的函数地址)当 Vehicle* v = new Car() 时,v 指向的对象的 vptr 会指向 Car 类的虚函数表,调用 v->run() 时,实际执行的是 Car::run。
关键注意点
vptr,占用 4 字节(32 位系统)或 8 字节(64 位系统)内存。当基类指针指向派生类对象并通过 delete 释放时,如果基类析构函数不是虚函数,会导致派生类的析构函数无法被调用,从而引发内存泄漏。
#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;
}
Base 构造函数被调用
Derived 构造函数被调用
Base 析构函数被调用
问题分析:Derived 类中动态分配的 data 数组未被释放,导致内存泄漏。
将基类的析构函数声明为虚函数,即可实现派生类析构函数的正确调用:
#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;
}
Base 构造函数被调用
Derived 构造函数被调用
Derived 析构函数被调用
Base 析构函数被调用
开发规范:只要类中包含虚函数,就应该将析构函数声明为虚析构函数,避免内存泄漏。
问题:派生类重写的函数与基类虚函数的参数列表或返回值类型不一致,导致无法触发多态。
解决方案:严格保证函数签名一致,使用 override 关键字检测重写合法性。
问题:构造函数和析构函数执行时,对象的类型是当前类的类型,而非派生类类型,此时调用虚函数无法实现多态。 解决方案:避免在构造函数和析构函数中调用虚函数,若需要调用,直接使用普通函数。
问题:虚函数调用需要通过虚函数表间接寻址,比普通函数调用多一层开销,在高性能场景下可能影响效率。 解决方案:在对性能要求极高的场景,尽量减少虚函数的使用;可以通过模板等静态多态方式替代。
需求:设计一个员工薪资计算系统,支持普通员工、技术员工、管理人员三种角色,不同角色的薪资计算规则不同,要求利用虚函数实现动态多态,新增角色时无需修改原有代码。
Employee:包含纯虚函数 calculateSalary,用于计算薪资。RegularEmployee:普通员工,薪资 = 基本工资。TechEmployee:技术员工,薪资 = 基本工资 + 技术补贴。Manager:管理人员,薪资 = 基本工资 + 管理补贴。#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;
}
员工 张三 的薪资为:5000 元
员工 李四 的薪资为:8000 元
员工 王五 的薪资为:11000 元
虚函数通过 virtual 关键字声明,支持派生类重写,实现运行时多态;纯虚函数无函数体,用于定义接口,包含纯虚函数的类是抽象类。
虚函数的底层实现依赖虚函数表和虚函数指针,虚函数表存储虚函数地址,虚函数指针指向虚函数表。
虚析构函数是解决派生类资源泄漏的关键,只要类中包含虚函数,就应该将析构函数声明为虚函数。
虚函数的核心优势是支持代码扩展,符合开闭原则,是大型 C++ 项目设计的核心机制。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online