虚函数表与多态内存布局
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_); }
};