c++如何实现多态性_c++ 虚函数表原理与动态绑定机制【教程】

只有通过基类指针或引用调用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 的原始布局。