只有通过基类指针或引用调用virtual函数才触发动态绑定;直接用对象名调用为静态绑定;virtual须显式写在基类声明前;函数签名须严格匹配;构造函数不能为虚,析构函数应为virtual;每个含虚函数的类有vtable,对象含vptr指向vtable。

虚函数怎么写才触发动态绑定
只有通过基类指针或引用调用 virtual 函数时,C++ 才会启用运行时多态。直接用对象名调用(如 obj.func())永远走静态绑定,编译器在编译期就确定调用哪个函数。
-
virtual必须显式写在基类函数声明前,派生类重写时可加可不加(但建议加上,提高可读性) - 函数签名(包括参数类型、
const修饰、返回类型协变)必须严格匹配,否则是重载而非重写 - 构造函数不能是虚函数;析构函数应声明为
virtual,否则delete基类指针时不会调用派生类析构函数
虚函数表(vtable)长什么样
每个含虚函数的类在编译后都有一个隐式的静态数组(即 vtable),里面存的是该类所有虚函数的函数指针。每个该类的对象开头都隐含一个指针(vptr),指向其所属类的 vtable。
例如:
class Base {
public:
virtual void f() { cout << "Base::f"; }
virtual void g() { cout << "Base::g"; }
};
class Derived : public Base {
public:
void f() override { cout << "Derived::f"; } // 覆盖 Base::f
void h() { cout << "Derived::h"; } // 非虚函数,不进 vtable
};
此时:Base 对象的 vptr 指向含 &Base::f 和 &Base::g 的表;Derived 对象的 vptr 指向另一张表,其中第一项是 &Derived::f(覆盖后的),第二项是 &Base::g(未被重写,沿用基类实现)。
立即学习“C++免费学习笔记(深入)”;
为什么父类指针能调用子类函数
关键在“间接跳转”:当执行 base_ptr->f() 时,编译器生成的指令是——先通过 base_ptr 找到对象首地址,再读取首地址处的 vptr,再根据函数在虚函数表中的索引(比如第 0 项)查出实际函数地址,最后跳过去执行。
- 这个过程发生在运行时,所以叫“动态绑定”
- 如果基类没声明
virtual,编译器根本不会生成 vtable,也不会插入vptr,自然无法动态分发 - 多重继承下 vtable 更复杂(可能多个 vptr),但单继承场景下结构清晰、开销极小(一次指针解引用 + 一次查表)
容易被忽略的坑:override 和 final 的实际作用
override 不是语法糖,它让编译器检查你是否真的重写了虚函数——若基类没有对应虚函数,或签名不匹配,会直接报错(如 error: 'f' does not override any member functions)。不用 override 可能静默变成重载,导致多态失效。
final 则阻止后续派生类重写该虚函数,也禁止该类被继承(加在类名后)。它影响的是编译期检查,不影响 vtable 布局本身,但能帮助编译器做优化(比如内联判断)。
示例:
struct A {
virtual void foo() {}
};
struct B : A {
void foo() override final {} // OK
};
struct C : B {
void foo() override {} // 错误:B::foo 是 final
};
虚函数机制本身简单,但真正难的是在大型继承体系中保持 vtable 一致性、避免意外切片、以及理解 static_cast / dynamic_cast 对 vptr 的依赖。这些细节一旦出错,往往表现为调用到错误函数或崩溃,且调试时看不到 vtable 的原始布局。








