【C++】虚函数是什么?为什么需要它?
虚函数运行原理、“多态”如何实现(vtable 虚表机制),是 C++ 面向对象底层非常重要的知识点。
1. 虚函数是什么?为什么需要它?
- 虚函数允许用父类指针/引用调用“子类自己的实现”。
- 这就是多态(Polymorphism):同一个接口,不同的实现,运行时动态决定实际调用哪个函数。
2. 多态代码举例
class Base {
public:virtual void Print() { std::cout << "Base" << std::endl; }
};
class Derived : public Base {
public:void Print() override { std::cout << "Derived" << std::endl; }
};Base* p = new Derived();
p->Print(); // 输出 "Derived",而不是 "Base"
如果没有virtual,输出就是"Base"。有了virtual,输出"Derived"。
3. vtable(虚表)实现机制
什么是 vtable?
- vtable(虚函数表)是编译器为每个有虚函数的类生成的一个“函数指针数组”。
- 每个对象实例里会有一个指向vtable的隐藏指针(通常叫vptr)。
- vtable表里存放着该类所有虚函数的“实际实现函数地址”。
具体原理:
- 当你通过父类指针/引用调用虚函数时,
- 程序会通过对象里的vptr找到正确的vtable,然后调用vtable里实际存的子类实现地址。
- 所以最终无论用哪个指针,都会自动调到“真正的函数实现”。
4. 运行流程图解
-
声明类(有virtual函数)
-
编译时
- 编译器为每个类生成vtable(如果有虚函数)。
- 对象里自动加vptr,指向对应类的vtable。
-
运行时
- 用基类指针/引用操作时,走vptr->vtable->实际函数地址,自动完成多态分发。
5. vtable/vptr的“模拟”代码
假设有:
class Base { virtual void f(); };
class Derived : public Base { void f() override; };
编译器背后大致做了这样的事:
typedef void(*FunPtr)();struct vtable_Base {FunPtr f;
};struct vtable_Derived {FunPtr f;
};struct Base {vtable_Base* vptr;
};struct Derived {vtable_Derived* vptr;
};
当你调用 p->f();
时,其实是:
p->vptr->f(p);
(p作为this指针传进去)
6. 小结和常见面试点
-
多态的底层原理: 靠虚函数表(vtable)+ 每个对象的vptr隐藏指针来实现。
-
作用: 让“父类指针/引用指向子类对象”时,调用的是子类自己的实现。
-
代价:
- 每个对象多一个指针(vptr)。
- 虚函数调用需要查表,比普通函数指针多一点点性能损耗,但几乎可以忽略。
-
其它说明:
- 不用virtual时,没有vtable/vptr,不支持多态。
7. 简单动图理解
Base* p = new Derived();
p->Print();----- 内存结构大致如下 -----p
│
└───> [vptr] ───> vtable (Derived类)│└──> Derived::Print
- 编译时写成
p->Print()
,实际运行时会查vptr和vtable,执行Derived::Print()。
8. 调试技巧
- 用
sizeof
看类大小,含虚函数的类多一个指针(32位下4字节,64位下8字节)。 - 可以用 GDB 查看对象的vptr和vtable。
9. 一张表对比
普通函数 | 虚函数 |
---|---|
编译时绑定 | 运行时绑定 |
没有vtable/vptr | 有vtable/vptr |
没有多态 | 支持多态 |
结论
- “虚函数 + vtable + vptr”= C++多态本质!
- 让 父类指针/引用调用到正确的子类方法,灵活解耦、高级抽象