C++ 虚函数 超全知识点总结
虚函数是 C++ 多态(动态多态) 的核心,专门解决父类指针 / 引用指向子类对象时,调用正确函数的问题。我会把所有考点、用法、原理、坑点一次性讲全。
一、基础概念
1. 定义
在类的成员函数 前加 virtual 关键字,就是虚函数 。作用:运行时决定调用哪个类的函数(动态绑定 / 晚绑定)。
2. 核心目的
让父类指针 / 引用 指向子类对象 时,能调用子类重写的函数,而不是父类版本。
3. 语法格式
cpp
运行
class 父类 {
public:
virtual 返回值 函数名(参数) { // 虚函数
// 实现
}
};
class 子类 : public 父类 {
public:
返回值 函数名(参数) override { // 重写(建议加 override)
// 子类实现
}
};
二、必须掌握的 4 个基础规则
1. 只有成员函数能加 virtual
全局函数、静态函数、构造函数 不能是虚函数。
2. 子类重写时,virtual 可写可不写
只要父类声明了 virtual,子类自动继承虚属性。建议:子类重写加 override 关键字(编译器检查是否正确重写)。
3. 动态绑定条件(缺一不可)
- 函数必须是 虚函数
- 通过 父类指针 / 父类引用 调用
- 指向 / 引用 子类对象
满足这 3 条,才会发生多态。
4. 构造函数不能是虚函数,析构函数建议是虚函数
- 构造:对象还没创建,无法调用虚函数
- 析构:父类指针删子类对象时,防止内存泄漏
三、虚析构函数(必考)
1. 为什么需要
父类指针指向 new 出来的子类对象,delete 时:
- 析构不是虚函数 → 只调用父类析构,子类资源泄漏
- 析构是虚函数 → 先调用子类析构,再调用父类析构
2. 写法
cpp
运行
class Base {
public:
virtual ~Base() { cout << "父类析构\n"; }
};
3. 结论
只要类里有虚函数,析构函数一律写成虚析构!
四、纯虚函数 & 抽象类
1. 纯虚函数定义
没有函数体,用 = 0 结尾:
cpp
运行
virtual void func() = 0;
2. 抽象类
包含至少一个纯虚函数的类。
- 不能实例化对象
- 必须由子类重写所有纯虚函数才能实例化
- 作用:定义接口,强制子类实现
3. 示例
cpp
运行
class Shape { // 抽象类
public:
virtual void draw() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
void draw() override { // 必须实现
cout << "画圆\n";
}
};
五、override /final 关键字(C++11)
1. override(检查重写)
告诉编译器:我要重写父类虚函数,写错就报错。
cpp
运行
void func() override; // 正确
void fun() override; // 编译报错(函数名写错)
2. final(禁止重写 / 禁止继承)
- 修饰函数:子类不能再重写
- 修饰类:该类不能被继承
cpp
运行
virtual void func() final; // 禁止重写
class A final {}; // 禁止继承
六、虚函数底层原理(面试必考)
1. 虚函数表 vtable
- 每个有虚函数的类 ,编译器会生成一张 虚函数表(静态,属于类)
- 表中存放:虚函数地址
- 子类重写 → 表中地址替换为子类函数地址
2. 虚指针 vptr
- 每个对象 会包含一个 虚指针(4/8 字节)
- 指向所属类的虚函数表
3. 调用流程
plaintext
父类指针 → 对象 vptr → 虚函数表 → 找到正确函数地址 → 调用
4. 开销
- 空间:每个对象多一个指针(4/8 字节)
- 时间:多一次查表(几乎可忽略)
七、多重继承下的虚函数
- 子类会继承所有父类的虚函数
- 子类只有一张虚表(合并所有父类虚表)
- 子类重写任意父类虚函数,表中地址都会更新
八、10 个高频易错点
1. 静态函数不能是虚函数
静态函数属于类,不属于对象,没有 this 指针 → 无法动态绑定。
2. inline 与 virtual 冲突
虚函数是运行时确定,inline 是编译时展开 → 虚函数不能真正内联。
3. 构造函数中调用虚函数
不会发生多态!只会调用当前类的函数。(构造时子类还没初始化完成)
4. 友元函数不能是虚函数
友元不是类成员,无法加 virtual。
5. 函数签名必须完全一致才能重写
- 函数名、参数、const 都必须一样
- 返回值可协变(父返回父指针,子返回子指针)
6. 缺省参数用父类的
虚函数调用动态绑定,但缺省参数编译期确定,永远用父类默认值。
7. 虚函数可以是 private
依然能多态!权限检查在编译期,调用在运行期。
8. 不能定义抽象类对象
包含纯虚函数 → 编译报错。
9. 子类没实现纯虚函数 → 子类仍是抽象类
依旧不能创建对象。
10. 虚函数可以有实现
纯虚函数也能写实现体,作用:给子类提供默认逻辑。
九、完整示例代码(可直接运行)
cpp
运行
#include <iostream>
using namespace std;
// 父类
class Animal {
public:
virtual void sound() { // 虚函数
cout << "动物叫\n";
}
virtual ~Animal() { // 虚析构
cout << "Animal 析构\n";
}
};
// 子类
class Dog : public Animal {
public:
void sound() override { // 重写
cout << "汪汪汪\n";
}
~Dog() {
cout << "Dog 析构\n";
}
};
int main() {
Animal* p = new Dog(); // 父指针指向子对象
p->sound(); // 多态:调用 Dog::sound()
delete p; // 先调用 Dog 析构,再 Animal 析构
return 0;
}
输出:
plaintext
汪汪汪
Dog 析构
Animal 析构