C++多态的运行时判定机制,本质上可以概括为:程序在运行阶段根据对象的实际类型(而非声明的指针或引用类型)来确定调用哪个成员函数。这一功能的核心基础正是动态绑定。听起来有些抽象?下面我们逐步深入剖析。
虚函数表(vtable)是关键基础设施
编译器会为每个包含虚函数的类自动生成一张“虚函数表”。该表按照类中虚函数的声明顺序,依次存储每个虚函数的实际地址。每个对象在内存布局的头部,包含一个不可见的隐式指针——通常称为vptr——它指向自身所属类的虚函数表。当通过基类指针或引用调用虚函数时,程序先通过vptr获取实际对象对应类的虚函数表,然后根据函数索引从表中取出地址,并跳转到相应的函数体执行。
- 若派生类重写了某个虚函数,其虚函数表中对应位置的条目会被替换为派生类自身版本的函数地址。
- 派生类新增的虚函数,则追加到虚函数表的末尾;纯虚函数对应的条目通常为空或被填充特殊标记。
- 虚函数表在编译阶段即已生成,运行期间只读不可写,无论创建多少个对象,每个类的虚函数表仅有一份,不会重复构造。
对象实际类型决定vptr指向哪张表
对象在创建时,其vptr即被初始化为指向它的最具体类型(即通过new直接实例化的类)的虚函数表。即使使用基类指针来接收该对象,其内存布局中的vptr也不会改变。
- 例如:
Base* p = new Derived();→ p所指向对象的vptr始终指向Derived::vtable。 - 后续将p赋值给另一个
Base*或GrandBase*变量时,只要对象本身未变,vptr依然保持不变。 - 类型转换(如
static_cast)不会修改vptr;只有对象被真正重建(如拷贝构造、切片操作)才可能切换虚函数表。
调用过程:一次间接寻址 + 一次跳转
对于 p->func() 这样的虚函数调用,底层实际执行以下三个步骤:
- 解引用p,获取对象的内存起始地址。
- 从该地址的固定偏移处读取vptr,得到虚函数表的地址。
- 根据func在类中虚函数声明顺序(即索引号),在虚函数表中对应的偏移位置取出函数地址,并直接跳转执行。
以上过程由编译器自动生成,对程序员完全透明。正是这些细微的操作,决定了“最终调用哪个函数”——完全依赖于运行时的实际对象类型,而非代码中指针的静态类型。
非虚函数与静态绑定对比更清晰
非虚函数的调用在编译阶段就已确定目标地址(称为静态绑定),只识别指针或引用的静态类型。而虚函数则不同,必须推迟到运行期,借助虚函数表机制突破编译时的类型信息限制。
- 不含虚函数的类(如不含虚函数的struct)自然没有vptr,因此
sizeof结果中不会增加额外的8字节(或4字节,取决于平台)的空间开销。 - 虚继承与多重继承会引入更复杂的虚函数表结构——可能包含多个vptr、thunk跳转等,但底层原理一致。
- 现代编译器有时会进行去虚化优化(例如能够证明对象类型唯一时,直接内联调用),但从语义上讲,运行时判定依然是标准做法。

