最近学习C++,这里简单记录一下虚函数(使用virtual修饰函数)相关内容,记录于此,方便自己查阅和回顾。
正文
虚函数
虚函数是在基类中用 virtual 关键字声明的成员函数。当派生类重写该函数时,通过基类指针或引用调用它,实际执行的是派生类的函数。
virtual void func() { ... }
虚函数并非一种特殊的函数类型,它只是被赋予了运行时多态能力的普通成员函数。
代码实现无差异:虚函数体内可以包含任意合法 C++ 代码(计算、IO、异常处理等),与普通函数完全一致。
存储位置相同:虚函数的机器码同样存放在代码段(
.text),编译后的指令与普通函数无异。调用语法相同:使用
.或->调用虚函数的语法与普通函数完全一致。唯一区别:
virtual关键字触发了编译器生成虚函数表(vtable)和虚函数表指针(vptr),将编译时静态绑定改为运行时动态绑定。
class Animal { public: virtual void speak() {// 基类虚函数 cout << "Animal speaks" << endl; } }; class Dog : public Animal { public: void speak() override {// 派生类重写 cout << "Dog barks" << endl; } }; class Cat : public Animal { public: void speak() override {// 派生类重写 cout << "Cat meows" << endl; } }; int main() { Animal* a1 = new Dog(); Animal* a2 = new Cat(); a1->speak(); // 输出: Dog barks a2->speak(); // 输出: Cat meows delete a1; delete a2; return 0; }
底层机制
虚函数的动态绑定是通过以下机制实现:
vtable(虚函数表):每个包含虚函数的类对应一张表,存储该类所有虚函数的地址。派生类覆盖虚函数时,表中对应槽位被替换为派生类版本。
vptr(虚函数表指针):每个对象内部维护一个隐藏指针,指向其所属类的 vtable。构造时初始化,析构时更新。
调用过程:
ptr->virtualFunc()→ 通过ptr的 vptr 找到 vtable → 在 vtable 中查找virtualFunc的槽位 → 调用实际函数地址。
关键理解:vtable 是类级别的,vptr 是对象级别的。虚函数的"多态能力"完全由这两者支撑,与函数体内容无关。
易错知识点
虚函数必须有函数体?
不一定。纯虚函数(
= 0)可以没有实现,但普通虚函数可以有完整实现,且派生类可以选择调用基类版本(Base::func())。虚函数不能内联?
并非绝对。当编译器能确定调用目标时(如通过对象直接调用而非指针/引用),虚函数仍可被内联。但通过基类指针/引用的调用无法内联。
虚函数性能一定差?
间接调用的开销极小(通常 < 10ns),在绝大多数场景下可忽略。只有在极端性能敏感的内循环中才需考虑。
纯虚函数
虚函数提供“默认实现”,纯虚函数定义“强制接口”。
// 纯虚函数:定义接口,无实现 virtual void func() = 0;
虚函数 vs 普通函数
| 对比维度 | 普通成员函数 | 虚函数 |
|---|---|---|
| 绑定时机 | 编译时静态绑定 | 运行时动态绑定 |
| 调用依据 | 指针/引用的声明类型 | 对象的实际类型 |
| 底层机制 | 直接地址调用 | 通过 vptr → vtable 间接调用 |
| 性能开销 | 无额外开销 | 一次指针解引用 + 一次间接调用 |
| 内联优化 | 可被编译器内联 | 通常无法内联(动态调用目标不确定) |
小结
虚函数的本质:一个被 virtual 修饰的普通成员函数,其代码实现与普通函数完全相同,区别仅在于调用时通过 vtable 实现运行时动态绑定。
AI

