C++ 内存布局详解:类、虚函数、虚表底层原理

C++ 内存布局详解:类、虚函数、虚表底层原理(一文彻底搞懂)

很多人学 C++ 学到"多态""虚函数"就卡住了,不是因为语法复杂,而是不知道对象在内存里到底长什么样

这篇文章不贴标准、不玩玄学,我们从内存布局出发,把下面几个问题一次性讲清楚:

  • 一个类对象占多少字节?

  • 成员函数存在哪里?

  • 为什么虚函数能"动态绑定"?

  • 虚表(vtable)到底是什么?

  • 继承、多继承、虚继承的内存布局差异?

准备好,我们直接拆内存。


一、最基础的起点:空类有多大?

复制代码
class A {};

问:sizeof(A) 是多少?

直觉可能是 0,但答案是:

复制代码
sizeof(A) == 1

为什么不是 0?

C++ 要求:每个对象必须有唯一的地址

如果大小为 0,那下面这种代码就会出问题:

复制代码
A a[10];
&a[0] == &a[1]; // 如果大小为 0,地址就一样了

所以编译器会偷偷塞一个 char 进去,凑出 1 字节。

✅ 结论:空类 ≠ 零大小,只是为了占位。


二、普通类:只有成员变量才进内存

复制代码
class Point {
public:
    int x;
    int y;
};

内存布局(假设 4 字节对齐):

复制代码
+----------------+
| x (4 bytes)    |
+----------------+
| y (4 bytes)    |
+----------------+

sizeof(Point) == 8

关键问题:成员函数在哪?

复制代码
void Point::print() { }

👉 成员函数不在对象里!

所有成员函数都存放在代码段(.text),和普通全局函数没本质区别。

对象里只存成员变量,因为:

  • 函数代码对所有对象共享

  • 只有数据是每个对象独立的

✅ 对象 = 数据 + 虚表指针(如果有)

❌ 对象 ≠ 函数


三、带虚函数的类:虚表指针登场

一旦类中出现 virtual 函数,事情就变了。

复制代码
class Base {
public:
    virtual void f() {}
    int x;
};

内存布局(64 位系统常见情况):

复制代码
+------------------+
| vptr (8 bytes)   | --> vtable
+------------------+
| x (4 bytes)      |
| padding (4 bytes)|
+------------------+

sizeof(Base) == 16

发生了什么?

  1. 编译器在对象最前面 插入了一个隐藏指针:vptr

  2. vptr 指向一张虚函数表(vtable)

  3. 虚表中存放的是函数指针


四、虚表(vtable)到底是什么?

虚表是一个函数指针数组,由编译器在编译期生成,存放在只读数据段。

复制代码
Base b;

简化示意:

复制代码
b:
[vptr] ----> [ &Base::f ]
            [ 其他虚函数 ]
x: 0

当你调用:

复制代码
b.f();

编译器实际做的是:

复制代码
b.vptr[0](&b);

这是一次间接调用


五、多态的底层真相:为什么父类指针能调用子类函数?

复制代码
class Derived : public Base {
public:
    void f() override {}
};

Base* p = new Derived();
p->f();

内存布局(Derived 对象):

复制代码
+------------------+
| vptr             | --> Derived 的 vtable
+------------------+
| x                |
+------------------+

关键点:

  • p 指向的是 Derived 对象

  • vptr 指向的是 Derived 的虚表

  • 虚表中 f 的位置已经被替换成 &Derived::f

所以:

复制代码
p->f();
// 等价于
p->vptr[0](p);

👉 这就是**动态绑定(运行时多态)**的本质。

✅ 多态 = 虚表 + 虚表指针 + 运行时查表


六、单继承下的虚表变化

复制代码
class Base {
public:
    virtual void f() {}
    virtual void g() {}
};

class Derived : public Base {
public:
    void f() override {}
    virtual void h() {}
};

虚表对比:

Base vtable

复制代码
[0] Base::f
[1] Base::g

Derived vtable

复制代码
[0] Derived::f   ← 覆盖
[1] Base::g     ← 继承
[2] Derived::h  ← 新增

规则:

  • 虚函数在表中的位置固定

  • 子类覆盖父类 → 替换对应槽位

  • 子类新增虚函数 → 追加到末尾


七、多继承:多个 vptr 登场

复制代码
class A {
public:
    virtual void fa() {}
    int x;
};

class B {
public:
    virtual void fb() {}
    int y;
};

class C : public A, public B {
public:
    void fa() override {}
    void fb() override {}
};

内存布局:

复制代码
+------------------+
| A::vptr          | --> C 针对 A 的 vtable
+------------------+
| x                |
+------------------+
| B::vptr          | --> C 针对 B 的 vtable
+------------------+
| y                |
+------------------+

⚠️ 注意:

  • C 对象里有 两个 vptr

  • A*B* 指向对象的不同位置

  • 类型转换可能涉及地址调整

    C c;
    B* pb = &c; // pb != (void*)&c


八、为什么会有"虚继承"?

考虑经典菱形问题:

复制代码
class A { public: int x; };
class B : public A {};
class C : public A {};
class D : public B, public C {};

如果不加 virtual

复制代码
D:
[B::A::x]
[C::A::x]

👉 x 有两份,歧义。

虚继承解决方式:

复制代码
class B : virtual public A {};
class C : virtual public A {};

内存布局(典型实现):

复制代码
+------------------+
| B::vptr          |
+------------------+
| offset to A      |
+------------------+
| C::vptr          |
+------------------+
| offset to A      |
+------------------+
| A::x             | ← 只有一份
+------------------+

虚继承的关键机制:

  • 虚基类偏移表(vbase offset table)

  • 通过间接偏移访问公共基类

代价:

  • 更复杂的内存布局

  • 更慢的访问速度

✅ 虚继承不是为了多态,而是为了共享基类子对象


九、一张总览表

场景 是否含 vptr 内存特点
空类 1 字节占位
普通类 仅成员变量
有虚函数 ✅ 1 个 头部插入 vptr
单继承 ✅ 1 个 共用 vptr
多继承 ✅ N 个 每个基类一个 vptr
虚继承 ✅ + 偏移信息 共享基类,布局复杂

十、常见误区澄清

❌ 误区 1:虚函数存在对象里

✅ 错,虚函数在代码段,对象里只有 vptr

❌ 误区 2:多态靠函数名匹配

✅ 错,多态靠 vtable 索引

❌ 误区 3:虚函数一定慢

✅ 只是多了一次间接调用,不是灾难级开销


十一、从汇编视角再看一眼(直觉版)

复制代码
p->f();

非虚函数:

复制代码
call Base::f

虚函数:

复制代码
mov rax, [p]        ; 取 vptr
call [rax]          ; 取第 0 个虚函数

这就是全部魔法。


十二、总结一句话版本

**C++ 的对象内存布局 = 成员变量 + 可能的虚表指针;

虚函数多态 = 编译期生成虚表 + 运行期查表调用。**

相关推荐
键盘会跳舞1 小时前
C++:容器适配器 queue 的源码级拆解
开发语言·c++·queue
还是鼠鼠1 小时前
Spring AI连接DeepSeek与通义千问:application.yaml配置详解
java·通义千问·spring ai·deepseek
计算机小白一个2 小时前
蓝桥杯 Java B 组之哈希表应用(两数之和、重复元素判断)
java·数据结构·算法·蓝桥杯
Loge编程生活2 小时前
打卡信奥刷题(3449)用C++实现信奥题 P10429 [蓝桥杯 2024 省 B] 拔河
开发语言·数据结构·c++·算法·青少年编程
在世修行3 小时前
从零打造 C# 工业视觉检测系统(八):Modbus RTU 协议解析与 PLC 剔除控制实战
开发语言·c#
sunburn-3 小时前
Java 队列全面详解:从入门到面试实战
java·开发语言·汇编·ide·idea
AI直播技术杂谈4 小时前
AI直播推流链路中延迟优化的通用技术方案
开发语言·人工智能·php
vx-程序开发4 小时前
springboot农产品运输服务平台---附源码75498
java·javascript·spring boot·python·eclipse·django·php
Dxy12393102164 小时前
python 中的 APScheduler 使用详解
开发语言·python