当通过基类指针或引用的方法指向派生类的对象的时候,执行同名的函数,会先去查看虚函数指针的表单,查看是否有对应的函数:(1)有则执行(2)无则执行基类的函数(3)基类中没有这个函数则编译不成功
多态
动态绑定
实现:基类中创建一个虚函数,在不同的派生类中重写这个虚函数
使用:使用基类的指针指向生成的派生类对象,执行的函数为派生类的函数(不同对象相同的接口执行不同的功能)
非多态
静态绑定
实现:基类中创建一个非虚函数,在不同的派生类中重写这个虚函数
使用:使用基类的指针指向生成的派生类对象,执行的函数为基类的函数
只要是基类指针或引用的方式,指向派生类对象,都能够触发多态
基类指针指向派生类对象时,不能够执行派生类独有的函数(除非使用强制类型转换,将基类指针转换为派生类指针类型)
使用基类指针指向派生类对象时,记得需要在基类的析构函数添加virtual关键字,防止delete时无法执行派生类的析构函数
C++ 虚函数:核心作用、原理及场景解析
虚函数(virtual 函数)是 C++ 实现运行时多态(动态多态) 的核心机制,其本质是通过「运行时动态绑定」替代「编译期静态绑定」,让基类的指针/引用能根据指向的实际对象类型 (而非指针/引用的静态类型)调用对应函数。结合Person/Student/Teacher 代码场景,以下从「核心作用、底层原理、使用场景、避坑点」四个维度详细拆解虚函数的作用:
一、虚函数的核心定义
虚函数是在基类中用 virtual 关键字声明的成员函数,派生类可「重写(override)」该函数;当通过基类指针/引用 调用该函数时,编译器不会在编译期确定调用哪个版本,而是在运行时根据指针/引用指向的「实际对象类型」(基类/派生类)选择对应的函数实现。
C++
// 基类声明虚函数
class Person {
public:
virtual void showInfo() { // 虚函数核心:virtual 关键字
cout << "Person 基类" << endl;
}
};
// 派生类重写虚函数
class Student : public Person {
public:
void showInfo() override { // override 显式声明重写(C++11+,推荐)
cout << "Student 派生类" << endl;
}
};
二、虚函数的核心作用(按优先级排序)
作用1:实现运行时多态(最核心)
这是虚函数的根本价值------让「同一接口(基类函数)」适配「不同派生类的行为」,且行为选择延迟到运行时。
对比:无虚函数(静态绑定)vs 有虚函数(动态绑定)
| 无虚函数(静态绑定) | 有虚函数(动态绑定) |
|---|---|
| 编译期根据「指针类型」确定函数版本 | 运行期根据「实际对象类型」确定函数版本 |
| 基类指针指向派生类,仍调用基类函数 | 基类指针指向派生类,调用派生类函数 |
| 无多态能力,代码耦合度高 | 支持多态,代码灵活、可扩展 |
示例(你的代码场景):
C++
int main() {
Person* p1 = new Student(); // 基类指针指向派生类对象
Person* p2 = new Person(); // 基类指针指向基类对象
// 无虚函数:两次都调用 Person::showInfo()(静态绑定)
// 有虚函数:p1调用 Student::showInfo(),p2调用 Person::showInfo()(动态绑定)
p1->showInfo();
p2->showInfo();
delete p1;
delete p2;
return 0;
}
输出(有虚函数):
Plain
Student 派生类
Person 基类
作用2:保证派生类析构函数被完整调用(避坑关键)
若基类析构函数未声明为虚函数,通过基类指针 delete 派生类对象时,只会调用基类析构函数,派生类析构函数被「截断」,导致派生类资源泄漏(如堆内存、文件句柄等)。
示例(你此前问过的 delete 派生类问题):
C++
// 错误:基类析构非虚
class Person {
public:
~Person() { cout << "Person 析构" << endl; } // 非虚析构
};
class Student : public Person {
public:
~Student() { cout << "Student 析构" << endl; }
};
int main() {
Person* p = new Student();
delete p; // 仅调用 Person::析构,Student::析构丢失 → 内存泄漏
return 0;
}
修复:基类析构设为虚函数:
C++
class Person {
public:
virtual ~Person() { cout << "Person 析构" << endl; } // 虚析构
};
// delete p 时:先调用 Student::析构 → 再调用 Person::析构(完整销毁)
输出:
Plain
Student 析构
Person 析构
作用3:解耦代码,符合「开闭原则」
虚函数让「调用方代码」仅依赖基类接口,新增派生类时无需修改调用方代码,只需重写虚函数即可扩展功能(对扩展开放,对修改关闭)。
示例:新增 Teacher 类,调用方无需改动
C++
// 调用方代码(仅依赖基类 Person)
void printPersonInfo(Person* p) {
p->showInfo(); // 虚函数自动适配新派生类
}
// 新增 Teacher 类(仅重写虚函数)
class Teacher : public Person {
public:
void showInfo() override {
cout << "Teacher 派生类" << endl;
}
};
int main() {
printPersonInfo(new Student()); // 输出 Student 派生类
printPersonInfo(new Teacher()); // 输出 Teacher 派生类(调用方未改)
return 0;
}
作用4:避免「对象切片」的功能退化(间接作用)
若用基类对象接收派生类对象(如 Person p = Student();),会发生「对象切片」------仅拷贝派生类中的基类部分,丢失派生类成员和行为。而虚函数+基类指针/引用可避免切片,完整保留派生类的行为:
C++
// 对象切片(无虚函数,功能退化)
Person p = Student();
p.showInfo(); // 调用 Person::showInfo()(丢失 Student 行为)
// 虚函数+指针(无切片,保留派生类行为)
Person* p = new Student();
p->showInfo(); // 调用 Student::showInfo()
三、虚函数的底层原理:虚函数表(vtable)+ 虚指针(vptr)
虚函数的动态绑定依赖编译器生成的「虚函数表(vtable)」和「虚指针(vptr)」,这是理解虚函数作用的关键:
1. 虚函数表(vtable)
-
每个包含虚函数的类(基类/派生类)会被编译器生成一张独立的「虚函数表」,本质是存储虚函数地址的数组;
-
基类的 vtable 存储基类虚函数的地址;
-
派生类的 vtable 会继承基类 vtable,并将重写的虚函数地址替换为自己的实现地址。
2. 虚指针(vptr)
-
每个含虚函数的对象会包含一个隐藏的「虚指针(vptr)」(占8字节,64位系统),指向所属类的 vtable;
-
对象构造时,编译器会初始化 vptr:基类对象的 vptr 指向基类 vtable,派生类对象的 vptr 指向派生类 vtable。
3. 动态绑定的执行流程
C++
Person* p = new Student();
p->showInfo(); // 动态绑定流程:
-
取
p指向的对象的vptr→ 找到Student类的 vtable; -
在 vtable 中找到
showInfo()的地址 → 调用Student::showInfo(); -
若
p指向Person对象,则通过 vptr 找到Personvtable,调用Person::showInfo()。
四、虚函数的扩展用法:纯虚函数与抽象类
若基类仅想定义「接口」,不想提供具体实现,可将虚函数声明为「纯虚函数」(virtual 函数名() = 0;),包含纯虚函数的类称为「抽象类」:
-
抽象类不能实例化(如
Person p;报错),只能作为基类被继承; -
派生类必须重写纯虚函数,否则派生类也成为抽象类。
示例(接口设计):
C++
class Person {
public:
virtual void showInfo() = 0; // 纯虚函数:仅定义接口,无实现
virtual ~Person() = default; // 虚析构仍需声明
};
// Student 必须重写 showInfo(),否则无法实例化
class Student : public Person {
public:
void showInfo() override {
cout << "Student 实现接口" << endl;
}
};
作用 :强制派生类实现统一接口,适合框架/库的接口设计(如 QT 的 QWidget 纯虚函数 paintEvent())。
五、虚函数的避坑点(关键规则)
1. 重写虚函数的严格规则
派生类重写的虚函数必须满足:
-
函数名、参数列表、返回值(协变除外)完全一致;
-
基类函数加
virtual,派生类可加可不加(推荐加override,编译期检查重写是否正确); -
访问权限不影响重写(如基类
public虚函数,派生类可private,但调用时需符合权限)。
2. 构造/析构中调用虚函数不触发多态
-
构造函数执行时:vptr 逐步初始化(先基类 vptr,后派生类 vptr),此时调用虚函数,仅调用当前类的版本(如基类构造中调用
showInfo(),仅调用基类版本); -
析构函数执行时:vptr 逐步回退(先派生类 vptr,后基类 vptr),此时调用虚函数,仅调用当前类的版本。
3. 虚函数的性能开销
-
内存开销:每个对象增加一个 vptr(8字节,64位系统);
-
性能开销:运行时通过 vptr 查找 vtable(纳秒级,几乎可忽略,除非极致性能场景);
-
优化:非多态场景(如仅用栈对象)无需声明虚函数,避免不必要的开销。
六、示例
(1)基类没有虚函数
C++
// 基类:Person(定义统一接口)
class Person {
public:
string name;
Person(string n) : name(n) {}
// 虚函数:核心!告诉编译器"这个函数要运行时绑定"
void showInfo() {
cout << "Person:" << "name:" << name << "(Base::Person)" << endl;
}
// 虚析构函数:避免派生类析构不执行(关键!)
virtual ~Person() {}
};
// 派生类1:Student(重写虚函数)
class Student : public Person {
public:
int score;
Student(string n, int s) : Person(n), score(s) {}
// 重写基类虚函数(override 关键字可选,建议加,编译期检查重写是否正确)
void showInfo(){
cout << "Student" << "name:" << name << ",score:" << score << "(Son::Student)" << endl;
}
};
// 派生类2:Teacher(重写虚函数)
class Teacher : public Person {
public:
string course;
Teacher(string n, string c) : Person(n), course(c) {}
// void showInfo() override {
// cout << "姓名:" << name << ",课程:" << course << "(派生类Teacher)" << endl;
// }
};
//调用方:只需接收基类指针,无需关心具体是Student/Teacher
void printPersonInfo(Person* p) {
p->showInfo(); // 运行时:根据p指向的实际对象,执行对应showInfo()
}
int main() {
Person* p1 = new Student("zhangsan", 95); // 基类指针指向Student对象
Person* p2 = new Teacher("lisi", "C++");// 基类指针指向Teacher对象
Person p3("father");
Person p4 = p3;
p4.name = "father_fake";
Student p5("zhangsan_fake", 100);
Teacher p6("lisi_fake", "C#");
printPersonInfo(p1); // 输出:姓名:张三,分数:95(派生类Student)
printPersonInfo(p2); // 输出:姓名:李四,课程:C++(派生类Teacher)
printPersonInfo(&p3);
printPersonInfo(&p4);
printPersonInfo(&p5);
printPersonInfo(&p6);
p1->showInfo();
p2->showInfo();
p3.showInfo();
p4.showInfo();
p5.showInfo();
p6.showInfo();
Person *p7 = &p5;
p7->showInfo();
delete p1; // 调用Student析构 + Person虚析构
delete p2; // 调用Teacher析构 + Person虚析构
return 0;
}
结果:

(2)基类有虚函数
示例:
c++
// 基类:Person(定义统一接口)
class Person {
public:
string name;
Person(string n) : name(n) {}
// 虚函数:核心!告诉编译器"这个函数要运行时绑定"
virtual void showInfo() {
cout << "Person:" << "name:" << name << "(Base::Person)" << endl;
}
// 虚析构函数:避免派生类析构不执行(关键!)
virtual ~Person() {}
};
// 派生类1:Student(重写虚函数)
class Student : public Person {
public:
int score;
Student(string n, int s) : Person(n), score(s) {}
// 重写基类虚函数(override 关键字可选,建议加,编译期检查重写是否正确)
void showInfo(){
cout << "Student" << "name:" << name << ",score:" << score << "(Son::Student)" << endl;
}
};
// 派生类2:Teacher(重写虚函数)
class Teacher : public Person {
public:
string course;
Teacher(string n, string c) : Person(n), course(c) {}
// void showInfo() override {
// cout << "姓名:" << name << ",课程:" << course << "(派生类Teacher)" << endl;
// }
};
//调用方:只需接收基类指针,无需关心具体是Student/Teacher
void printPersonInfo(Person* p) {
p->showInfo(); // 运行时:根据p指向的实际对象,执行对应showInfo()
}
int main() {
Person* p1 = new Student("zhangsan", 95); // 基类指针指向Student对象
Person* p2 = new Teacher("lisi", "C++");// 基类指针指向Teacher对象
Person p3("father");
Person p4 = p3;
p4.name = "father_fake";
Student p5("zhangsan_fake", 100);
Teacher p6("lisi_fake", "C#");
printPersonInfo(p1); // 输出:姓名:张三,分数:95(派生类Student)
printPersonInfo(p2); // 输出:姓名:李四,课程:C++(派生类Teacher)
printPersonInfo(&p3);
printPersonInfo(&p4);
printPersonInfo(&p5);
printPersonInfo(&p6);
p1->showInfo();
p2->showInfo();
p3.showInfo();
p4.showInfo();
p5.showInfo();
p6.showInfo();
Person *p7 = &p5;
p7->showInfo();
delete p1; // 调用Student析构 + Person虚析构
delete p2; // 调用Teacher析构 + Person虚析构
return 0;
}
结果:

六、总结:虚函数的核心价值
| 维度 | 虚函数的核心作用 |
|---|---|
| 功能 | 实现运行时多态,让基类指针适配不同派生类行为 |
| 资源管理 | 保证派生类析构函数完整执行,避免内存泄漏 |
| 代码设计 | 解耦代码,符合开闭原则,支持接口化设计 |
| 底层 | 通过 vtable/vptr 实现动态绑定 |
结合你的代码场景(Person* p = new Student();),虚函数是让基类指针「识别」派生类对象的唯一方式------没有虚函数,基类指针永远只能调用基类函数,失去多态能力;有了虚函数,才能真正实现「一个接口,多种形态」的面向对象设计。 |