虚函数表,以及虚函数指针是实现多态性(Polymorphism)的关键机制。多态性允许我们通过基类的指针或引用来调用派生类的函数
定义
虚函数(Virtual Function)
定义:类中使用virtual 关键字修饰的函数 叫做虚函数
语法:
highlighter- cpp
class Base { public: virtual void show() { cout << "Base show" << endl; } };
虚函数表(Virtual Function Table)
- 定义:当类含有至少一个虚函数时,编译器会为该类创建一个虚函数表。这个表是一个编译时构建的静态数组,存储了指向类中所有虚函数的指针。如果一个派生类重写了这些函数,那么在派生类的虚表中,相应函数的指针会被更新为指向派生类中的版本。
- 作用:v-table使得在运行时可以实现函数的动态绑定,允许通过基类的指针或引用调用正确的函数版本。
虚函数指针(Virtual Pointer)
- 定义:每个含有虚函数的类的对象(实例化出的)会持有一个指向相应虚表的指针,这个指针通常被称为虚指针(vptr)。vptr是对象运行时的一部分,确保了当通过基类指针调用虚函数时,能够查找到正确的函数实现。
- 作用:在对象的生命周期开始时,构造函数会设置vptr以指向相应的虚函数表。如果有派生类对象,它的构造函数会更新vptr,以指向派生类的虚函数表。这保证了通过基类的引用或指针调用虚函数也会执行最派生类的重写版本。
示例
highlighter- cpp
#include <iostream> using namespace std; class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } }; class Derived : public Base { public: void func1() override { cout << "Derived::func1" << endl; } // func2() 继承自 Base }; void printVTable(void* obj) { cout << "vptr Address: " << obj << endl; void** vTable = *(void***)obj; cout << "VTable[0] (func1): " << vTable[0] << endl; cout << "VTable[1] (func2): " << vTable[1] << endl; } int main() { Base* base = new Base(); Derived* derived = new Derived(); cout << "Base object:" << endl; printVTable(base); cout << "\nDerived object:" << endl; printVTable(derived); delete base; delete derived; return 0; }
程序输出如下,可以看到没用重写的func2函数地址是一样的。
highlighter- apache
Base object: vptr Address: 0x8c1510 VTable[0] (func1): 0x422270 VTable[1] (func2): 0x4222b0 Derived object: vptr Address: 0x8c1530 VTable[0] (func1): 0x422330 VTable[1] (func2): 0x4222b0
如下图所示:
面试题
(来自2024腾讯实习面试)场景题:一个类 A,里面有一个打印 helloworld 的虚函数,然后类 A 会在构造函数里调用这个虚函数,此时有个类 B,继承A,重写了这个 helloworld虚函数,问你在创建类 B 时,会打印 A 里的 helloworld 还是 B 里的。
代码如下:
highlighter- arduino
class A { public: A() { print(); } virtual void print() { cout << "A print" << endl; } }; class B : public A { public: void print() override { cout << "B print" << endl; } }; int main() { A* aTemp = new B(); delete aTemp; }
解答:基类构造函数执行的时候,派生类的部分尚未初始化,因此调用的虚函数不会下发到派生类中。
最终会打印 A print,而不是类 B 里重写的版本