C++多态通过虚表(vtable)和虚表指针(vptr)实现:每个含虚函数的类有唯一vtable,对象内存首部存vptr指向所属类vtable;父类指针调用虚函数时,根据实际对象的vptr动态查表跳转,而非静态类型;非虚函数编译期绑定,不进vtable;对象切片会丢失vptr,故多态仅适用于指针或引用。

当用父类指针指向子类对象并调用虚函数时,C++不是靠类型声明决定调用哪个函数,而是靠对象实际内存中携带的虚表指针(vptr)在运行时动态查找——这就是多态的核心机制。
虚函数表(vtable)是多态的底层支撑
编译器为每个含虚函数的类生成一张虚函数表,表中按声明顺序存放该类所有虚函数的地址。子类继承父类虚表后,会用自己的虚函数地址覆盖父类对应位置(若重写了该函数)。每个对象在内存头部隐式存储一个指向其所属类虚表的指针(vptr),构造对象时由构造函数自动初始化。
- 父类对象的 vptr 指向父类 vtable
- 子类对象的 vptr 指向子类 vtable(即使没新增虚函数,也会有一份拷贝)
- 虚表本身是只读数据段中的静态结构,不随对象数量变化
父类指针调用虚函数的真实流程
写 red">Base* p = new Derived(); p->func(); 时,编译器生成的指令不是直接跳转,而是三步查表:
- 从 p 所指内存首地址取出 vptr(偏移量为 0)
- 根据 func 在虚表中的索引(比如第 1 项),访问 vptr 指向的虚表 + 偏移地址
- 取出该位置存储的函数地址,然后 call 它
整个过程不依赖 p 的静态类型 Base*,只依赖 p 实际指向的对象内存布局——所以哪怕 p 是 Base*,只要它指向的是 Derived 对象,就一定调用 Derived::func()。
立即学习“C++免费学习笔记(深入)”;
为什么非虚函数不参与多态
非虚函数没有进入虚表,编译器在编译期就根据指针/引用的静态类型决定调用目标,生成直接 call 指令。例如 Base* p = new Derived(); p->non_virtual(); 会无条件调用 Base::non_virtual(),哪怕 Derived 里也定义了同名函数且参数一致——这叫隐藏(hiding),不是重写(overriding)。
- 只有被 virtual 修饰、且在派生类中签名完全匹配的函数才构成重写
- 构造函数、析构函数、static 成员函数天然不能是虚函数(除析构函数可且推荐声明为 virtual)
- inline 和 virtual 一般不共存:虚函数需要地址以填入虚表,而 inline 是编译器优化提示,二者语义冲突
一个易忽略的关键点:对象切片与 vptr 丢失
多态只对指针和引用有效。如果写 Base b = Derived();,会发生对象切片——只复制 Base 部分,Derived 特有成员和 vptr 全部丢弃,b 的 vptr 指向 Base 的虚表,后续调用全是 Base 版本。这也是为什么多态接口必须用指针或引用传递,不能传值。
- 函数参数写 void func(Base b) → 切片,无多态
- 正确写法是 void func(Base& b) 或 void func(Base* b)
- 智能指针如 std::shared_ptr
同样保留 vptr,安全支持多态
基本上就这些。理解 vptr 和 vtable 的存在与协作方式,比死记“父类指针可以调子类函数”更有价值——它让你看清 C++ 多态不是语法糖,而是有明确内存布局和运行时开销的确定机制。










