C++ 虚函数的实现原理
虚函数是 C++ 实现多态性的核心机制,理解其实现原理对于深入掌握 C++ 至关重要。
虚函数表(vtable)
虚函数通过虚函数表(Virtual Function Table,简称 vtable)来实现。每个包含虚函数的类都有一个对应的虚函数表。
vtable 的特点:
- vtable 是一个静态数组,存储着该类所有虚函数的函数指针
- 每个对象在内存中都有一个指向 vtable 的指针(vptr),通常位于对象内存布局的起始位置
- vtable 在编译时生成,存储在程序的只读数据段
内存布局
包含虚函数的对象内存布局:
shell+------------------+ | vptr (虚表指针) | 指向该类的 vtable +------------------+ | 成员变量 1 | +------------------+ | 成员变量 2 | +------------------+ | ... | +------------------+
vtable 的结构:
shell+------------------+ | type_info 指针 | 用于 RTTI(运行时类型识别) +------------------+ | 虚函数 1 指针 | +------------------+ | 虚函数 2 指针 | +------------------+ | ... | +------------------+
虚函数调用过程
当调用虚函数时,编译器会生成类似以下的代码:
cpp// 假设调用:obj->virtualFunction() // 编译器生成的伪代码: (*(obj->vptr)[index])(obj)
调用步骤:
- 通过对象的 vptr 找到对应的 vtable
- 根据虚函数在 vtable 中的索引找到函数指针
- 通过函数指针调用实际的函数
继承中的 vtable
单继承:
- 派生类会继承基类的 vtable
- 派生类重写的虚函数会替换 vtable 中对应的函数指针
- 派生类新增的虚函数会追加到 vtable 的末尾
多重继承:
- 派生类会有多个 vtable,每个基类对应一个
- 对象中会有多个 vptr,分别指向不同的 vtable
- 调用虚函数时需要根据基类类型选择正确的 vtable
纯虚函数和抽象类
纯虚函数:
cppclass Shape { public: virtual void draw() = 0; // 纯虚函数 virtual double area() = 0; };
特点:
- 纯虚函数在 vtable 中对应的函数指针为 nullptr
- 包含纯虚函数的类称为抽象类,不能实例化
- 派生类必须实现所有纯虚函数才能实例化
虚析构函数
为什么需要虚析构函数:
cppclass Base { public: virtual ~Base() { // 虚析构函数 cout << "Base destructor" << endl; } }; class Derived : public Base { public: ~Derived() { cout << "Derived destructor" << endl; } }; Base* ptr = new Derived(); delete ptr; // 正确调用 Derived 和 Base 的析构函数
如果不声明为虚析构函数:
- 只会调用基类的析构函数,导致派生类资源泄漏
性能考虑
虚函数调用的开销:
- 需要通过 vptr 间接访问 vtable
- 需要通过函数指针间接调用函数
- 破坏了内联优化的可能性
优化建议:
- 在性能关键的代码中,避免过度使用虚函数
- 考虑使用 final 关键字阻止进一步重写,可能帮助编译器优化
- 使用 CRTP(奇异递归模板模式)实现静态多态
代码示例
cpp#include <iostream> using namespace std; class Animal { public: virtual void makeSound() { cout << "Animal makes a sound" << endl; } virtual ~Animal() { cout << "Animal destructor" << endl; } }; class Dog : public Animal { public: void makeSound() override { cout << "Dog barks" << endl; } ~Dog() override { cout << "Dog destructor" << endl; } }; class Cat : public Animal { public: void makeSound() override { cout << "Cat meows" << endl; } ~Cat() override { cout << "Cat destructor" << endl; } }; int main() { Animal* animals[2]; animals[0] = new Dog(); animals[1] = new Cat(); for (int i = 0; i < 2; i++) { animals[i]->makeSound(); // 动态绑定 } for (int i = 0; i < 2; i++) { delete animals[i]; // 正确调用派生类析构函数 } return 0; }
注意事项
- 虚函数不能是静态函数(static)
- 构造函数不能是虚函数
- 析构函数应该是虚函数,除非确定不会被多态使用
- 在构造函数和析构函数中调用虚函数,不会发生动态绑定