前置知识: C++

虚函数表与多态内存布局

3 minAdvanced2026/6/14

C++虚函数表与多态内存布局详解:vtable、vptr。

概述

虚函数表(vtable)和虚指针(vptr)是 C++ 实现运行时多态的底层机制。每个包含虚函数都有一个 vtable,存储该所有虚函数的地址;每个该的对象都包含一个 vptr,指向所属的 vtable。理解 vtable 和 vptr 的工作原理,有助于理解多态的性能开销、对象的内存布局以及某些微妙的行为。

基础概念

多态的实现原理

C++ 的动态多态通过虚函数实现。编译器为每个含有虚函数生成一个虚函数表(vtable),其中按声明顺序存储所有虚函数的地址。每个对象在构造时,会将其 vptr 设置为所属的 vtable 地址。当通过基指针调用虚函数时,程序通过 vptr 查找 vtable,再从 vtable 中找到实际要调用的函数地址。

内存布局

对象内存布局(64位系统):

不含虚函数的类:
+--------+
| 成员1  |
| 成员2  |
+--------+

含虚函数的类:
+--------+
| vptr   |  --> 指向类的 vtable
| 成员1  |
| 成员2  |
+--------+

vtable:
+------------------+
| &Derived::foo()  |  第一个虚函数
| &Base::bar()     |  第二个虚函数(未覆盖)
| &Derived::baz()  |  第三个虚函数
+------------------+

快速上手

虚函数表的基本结构

class Base {
public:
    virtual void foo() { std::cout << "Base::foo" << std::endl; }
    virtual void bar() { std::cout << "Base::bar" << std::endl; }
    int base_data_ = 0;
};

class Derived : public Base {
public:
    void foo() override { std::cout << "Derived::foo" << std::endl; }
    int derived_data_ = 1;
};

// Base 的 vtable:
// [0] &Base::foo
// [1] &Base::bar

// Derived 的 vtable:
// [0] &Derived::foo  (覆盖了 Base::foo)
// [1] &Base::bar     (未覆盖,继承自 Base)

多态调用过程

Base* ptr = new Derived;
ptr->foo();  // 调用 Derived::foo

// 底层执行过程:
// 1. 通过 ptr 找到对象的 vptr
// 2. 通过 vptr 找到 Derived 的 vtable
// 3. 从 vtable 的第 0 个槽位取出函数指针 &Derived::foo
// 4. 调用 Derived::foo

详细用法

多重继承的内存布局

class Base1 {
public:
    virtual void foo1() {}
    int data1_;
};

class Base2 {
public:
    virtual void foo2() {}
    int data2_;
};

class Derived : public Base1, public Base2 {
public:
    void foo1() override {}
    void foo2() override {}
    int data3_;
};

// Derived 对象的内存布局:
// +------------------+
// | vptr1 (Base1)    | --> Derived 的 Base1 vtable
// | data1_           |
// | vptr2 (Base2)    | --> Derived 的 Base2 vtable
// | data2_           |
// | data3_           |
// +------------------+

// 当 Derived* 转换为 Base2* 时,指针会偏移到 vptr2 的位置
Derived d;
Base2* b2 = &d;  // 指针值发生了偏移

析构函数

class Base {
public:
    virtual ~Base() { std::cout << "Base 析构" << std::endl; }
    virtual void process() {}
};

class Derived : public Base {
    std::vector<int> data_;
public:
    ~Derived() { std::cout << "Derived 析构" << std::endl; }
    void process() override {}
};

// 正确:虚析构函数确保通过基类指针删除时调用完整的析构链
Base* ptr = new Derived;
delete ptr;  // 调用 Derived::~Derived() 然后 Base::~Base()

// 如果 Base 的析构函数不是 virtual:
// delete ptr;  // 未定义行为!只调用 Base::~Base(),Derived 的资源泄漏

虚函数与构造/析构顺序

class Base {
public:
    Base() {
        // 此时 vptr 指向 Base 的 vtable
        // 调用虚函数会调用 Base 的版本,不是 Derived 的
        process();  // 调用 Base::process()
    }

    virtual void process() { std::cout << "Base::process" << std::endl; }
};

class Derived : public Base {
public:
    Derived() {
        // 此时 vptr 已更新为 Derived 的 vtable
        process();  // 调用 Derived::process()
    }

    void process() override { std::cout << "Derived::process" << std::endl; }
};

// 构造 Derived 时的执行顺序:
// 1. Base 构造函数执行,vptr 指向 Base vtable
// 2. Base::process() 被调用(不是 Derived::process())
// 3. vptr 更新为 Derived vtable
// 4. Derived 构造函数执行
// 5. Derived::process() 被调用

常见场景

动态型识别(RTTI)

#include <typeinfo>
#include <iostream>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
};

// 使用 typeid
void printType(const Shape& s) {
    std::cout << "类型: " << typeid(s).name() << std::endl;
    // 如果 s 是 Circle,输出 Circle 的类型名
}

// 使用 dynamic_cast
void processShape(Shape* s) {
    if (auto* circle = dynamic_cast<Circle*>(s)) {
        std::cout << "这是一个圆" << std::endl;
    }
}

型擦除与 vtable

#include <functional>
#include <memory>

// 使用虚函数实现类型擦除
class AnyCallable {
    struct Base {
        virtual ~Base() = default;
        virtual void call() = 0;
    };

    template<typename F>
    struct Impl : Base {
        F func_;
        Impl(F func) : func_(std::move(func)) {}
        void call() override { func_(); }
    };

    std::unique_ptr<Base> impl_;
public:
    template<typename F>
    AnyCallable(F func) : impl_(std::make_unique<Impl<F>>(std::move(func))) {}

    void operator()() { impl_->call(); }
};

注意事项

  • 每个含有虚函数的对象都会增加一个指针大小的开销(64位系统为 8 字节)
  • 虚函数调用有一次间接寻址开销,且无法被内联(通过指针引用调用时)
  • 构造函数析构函数中调用虚函数不会触发多态,此时 vptr 指向当前而非派生的 vtable
  • 析构函数是必须的:如果基指针可能指向派生对象,基析构函数必须为 virtual
  • RTTI(typeid 和 dynamic_cast)依赖 vtable,且有一定运行时开销
  • 编译器对 vtable 的具体实现有差异,上述布局是典型实现但非标准要求

进阶用法

CRTP 替代虚函数(静态多态)

// CRTP(Curiously Recurring Template Pattern)实现编译期多态
template<typename Derived>
class Base {
public:
    void process() {
        // 编译期确定调用哪个函数,无需 vtable
        static_cast<Derived*>(this)->doProcess();
    }
};

class ConcreteA : public Base<ConcreteA> {
public:
    void doProcess() { std::cout << "A 处理" << std::endl; }
};

class ConcreteB : public Base<ConcreteB> {
public:
    void doProcess() { std::cout << "B 处理" << std::endl; }
};

// 无虚函数开销,编译期确定调用目标
ConcreteA a;
a.process();  // 直接调用 ConcreteA::doProcess,可内联

手动模拟 vtable

#include <array>
#include <functional>

// 使用函数指针数组替代虚函数表
struct VTable {
    void (*draw)(void*);
    void (*update)(void*);
};

class Entity {
    const VTable* vtable_;
    void* data_;
public:
    template<typename T>
    explicit Entity(T& obj) {
        static const VTable vt = {
            [](void* p) { static_cast<T*>(p)->draw(); },
            [](void* p) { static_cast<T*>(p)->update(); }
        };
        vtable_ = &vt;
        data_ = &obj;
    }

    void draw() { vtable_->draw(data_); }
    void update() { vtable_->update(data_); }
};