必须用基类指针或引用才能触发多态,因为只有静态类型为基类、实际对象为派生类时,虚函数机制才通过vptr和vtable实现动态绑定;直接用派生类对象调用是静态绑定,不构成多态。

为什么必须用基类指针(或引用)才能触发多态
多态不是自动发生的,它依赖于编译器对调用方式的识别。只有当表达式静态类型是基类、而实际指向/绑定的是派生类对象时,虚函数机制才会介入。直接用 Derived 对象调用 vfunc(),走的是静态绑定;用 Base* 指向 Derived 对象再调用,才可能走动态绑定。
常见错误现象:
– 写 Derived d; d.vfunc(); → 总是调用 Derived::vfunc(),但这不是多态,只是普通函数调用
– 写 Base b; b.vfunc(); → 即使 vfunc() 是虚函数,也只调用 Base::vfunc(),因为没发生“向上转型”
- 必须满足“基类指针/引用 + 派生类对象”这个组合,缺一不可
- 指针本身不携带类型信息,但它的静态类型(
Base*)决定了编译器按哪个类的接口去查虚表 - 引用同理:
Base& r = d;和Base* p = &d;效果一致,都是启用动态分发的合法入口
指针类型决定编译期行为,对象类型决定运行期行为
Base* ptr = new Derived(); 这行代码里,ptr 的类型是 Base*,所以:
– 编译器只允许你访问 Base 中声明的成员(包括虚函数、非虚函数、公有数据成员)
– 无法直接写 ptr->y(假设 y 是 Derived 特有成员),会报错 error: 'class Base' has no member named 'y'
但虚函数调用例外:虽然 ptr 是 Base* 类型,ptr->vfunc() 却会在运行时跳转到 Derived::vfunc(),前提是:
– Base::vfunc() 是 virtual
– Derived::vfunc() 正确重写了它(签名一致,无需显式写 virtual,但建议写)
- 非虚函数如
func(),即使子类有同名函数,ptr->func()仍调用Base::func()(隐藏,非重写) - 成员变量永远按指针静态类型访问,不存在“运行时变量多态”
- 强制转型如
static_cast<derived>(ptr)</derived>才能访问子类特有成员,但需确保对象真实类型匹配,否则未定义行为
虚表指针(vptr)藏在对象内存开头,和指针无关但依赖指针触发
每个含虚函数的类对象,编译器会在其内存布局最前面悄悄插入一个 vptr(通常 4 或 8 字节,取决于平台)。这个指针指向该类的虚函数表(vtable),而 vtable 存在静态区,每类一份。
关键点:
– vptr 属于对象,不是指针;Base* ptr 本身不含 vtable 或 vptr
– 但当你通过 ptr 调用虚函数时,CPU 实际执行的是:
1. 从 *ptr 地址读取开头的 vptr
2. 用 vptr 查到对应类的 vtable
3. 根据函数在虚表中的槽位索引(编译期固定)跳转到实际函数地址
- 对象构造时,
vptr被初始化为指向自身类的 vtable(如Derived对象的vptr指向Derived的 vtable) - 如果基类没有虚函数,派生类加了虚函数,派生类对象仍有
vptr;但基类对象没有,此时用基类指针指向派生类对象会导致未定义行为(vptr 位置错乱) - sizeof(
Base) 会比纯数据成员总和大(多出一个指针大小),这是判断类是否含虚函数的简单方法
容易被忽略的坑:指针生命周期与多态安全边界
多态依赖对象真实存在。一旦对象析构,哪怕指针还存着,ptr->vfunc() 就是野指针调用,结果不可预测 —— 可能崩溃,也可能看似正常(尤其虚函数只读成员时),但本质是未定义行为。
- 返回局部对象地址:
Base* f() { Derived d; return &d; }→ 绝对禁止 - 智能指针可缓解,但
shared_ptr<base>持有Derived对象时,仍要求Base的析构函数是virtual,否则派生部分不会被正确析构 - 数组退化问题:
Base arr[10];放Derived对象会切片(slicing),丢失虚表指针和派生部分,arr[i].vfunc()永远调用Base版本
真正起作用的从来不是指针本身,而是它所关联的那个对象是否还活着、类型是否匹配、虚函数是否正确定义 —— 指针只是通往多态的那扇门,门后的东西,得你自己守好。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











