C++ 内存模型与对象模型详解
本文深入讲解 C++ 程序的内存布局、对象的内存结构、虚函数表机制以及智能指针的内部实现。⭐ 标记为高频重点。
一、C++ 程序的内存布局
⭐ 程序运行时的内存分区
当一个 C++ 程序运行时,操作系统为其分配的虚拟地址空间大致如下:
┌─────────────────────────────────────────┐ 高地址 (0xFFFF...)
│ │
│ 内核空间 (Kernel) │ ← 用户程序不可访问
│ │
├─────────────────────────────────────────┤
│ │
│ 栈 (Stack) ↓ 向低地址增长 │ ← 局部变量、函数参数、返回地址
│ │
│ ┌─────────────────────────────────┐ │
│ │ main() 的栈帧 │ │
│ │ ├── int a = 10; │ │
│ │ ├── double b = 3.14; │ │
│ │ └── 返回地址 │ │
│ ├─────────────────────────────────┤ │
│ │ Foo() 的栈帧 │ │
│ │ ├── int x = 42; │ │
│ │ └── char buf[256]; │ │
│ └─────────────────────────────────┘ │
│ │
│ ↕ 可增长空间 │
│ │
│ 堆 (Heap) ↑ 向高地址增长 │ ← new / malloc 动态分配
│ │
│ ┌─────────────────────────────────┐ │
│ │ new int(42) │ │
│ │ new std::string("hello") │ │
│ │ malloc(1024) │ │
│ └─────────────────────────────────┘ │
│ │
├─────────────────────────────────────────┤
│ │
│ BSS 段 (.bss) │ ← 未初始化的全局/静态变量
│ int g_uninit; // 默认 0 │
│ static int s; // 默认 0 │
│ │
├─────────────────────────────────────────┤
│ │
│ 数据段 (.data) │ ← 已初始化的全局/静态变量
│ int g_init = 42; │
│ static int s = 100; │
│ │
├─────────────────────────────────────────┤
│ │
│ 只读数据段 (.rodata) │ ← 字符串字面量、const 全局
│ "Hello, World!" │
│ const int MAX = 100; │
│ │
├─────────────────────────────────────────┤
│ │
│ 代码段 (.text) │ ← 编译后的机器指令
│ main() 的机器码 │
│ Foo() 的机器码 │
│ │
└─────────────────────────────────────────┘ 低地址 (0x0000...)
⭐ 各内存区域详细对比
| 区域 | 管理方式 | 生命周期 | 增长方向 | 大小 | 特点 |
|---|---|---|---|---|---|
| 栈 | 编译器自动 | 随函数调用创建/返回销毁 | 高→低 | 较小(1-8 MB) | 速度快,无碎片 |
| 堆 | 手动 new/delete | 手动控制 | 低→高 | 较大(GB级) | 灵活,可能碎片 |
| 全局/静态区 | 编译器分配 | 程序整个运行期 | — | 固定 | 自动初始化为 0 |
| 常量区 | 编译器分配 | 程序整个运行期 | — | 固定 | 只读 |
| 代码区 | 编译器生成 | 程序整个运行期 | — | 固定 | 只读,可共享 |
⭐ 函数调用的栈帧结构
每次函数调用,都会在栈上创建一个栈帧(Stack Frame):
高地址
┌──────────────────────────────┐
│ 调用者栈帧 │
├──────────────────────────────┤
│ 参数 N │ ← 函数参数(从右到左压栈,cdecl 约定)
│ 参数 N-1 │
│ ... │
│ 参数 1 │
├──────────────────────────────┤
│ 返回地址 (Return Address) │ ← call 指令自动压入
├──────────────────────────────┤ ← EBP(帧指针)指向这里
│ 上一个 EBP(保存的帧指针) │
├──────────────────────────────┤
│ 局部变量 1 │
│ 局部变量 2 │
│ ... │
│ 临时变量 │
├──────────────────────────────┤ ← ESP(栈指针)指向这里
│ (空闲栈空间) │
└──────────────────────────────┘
低地址
cpp
int Add(int a, int b) { // a, b 在参数区
int result = a + b; // result 在局部变量区
return result; // 返回值通常通过寄存器(EAX)传递
}
int main() {
int x = Add(3, 5); // 1. 压入参数 5, 3
// 2. 压入返回地址
// 3. 跳转到 Add
// 4. Add 创建自己的栈帧
// 5. 执行完毕,销毁栈帧,返回
}
二、C++ 对象的内存模型
⭐ 普通类对象的内存布局
cpp
class Simple {
int a_; // 4 字节
double b_; // 8 字节
char c_; // 1 字节
};
Simple 对象的内存布局(64 位系统):
偏移 成员 大小 说明
┌────┬──────────┬──────┬───────────────────────┐
│ 0 │ a_ │ 4B │ int │
├────┼──────────┼──────┼───────────────────────┤
│ 4 │ (填充) │ 4B │ 对齐到 8 字节边界 │
├────┼──────────┼──────┼───────────────────────┤
│ 8 │ b_ │ 8B │ double │
├────┼──────────┼──────┼───────────────────────┤
│ 16 │ c_ │ 1B │ char │
├────┼──────────┼──────┼───────────────────────┤
│ 17 │ (填充) │ 7B │ 总大小对齐到最大成员的倍数│
└────┴──────────┴──────┴───────────────────────┘
sizeof(Simple) = 24(而不是 13)
优化:调整成员顺序减少填充
cpp
class Optimized {
double b_; // 8 字节(偏移 0)
int a_; // 4 字节(偏移 8)
char c_; // 1 字节(偏移 12)
// 3 字节填充
};
// sizeof(Optimized) = 16(节省了 8 字节)
🌈 规则:按成员大小从大到小排列,可以有效减少内存对齐造成的浪费。
⭐ 含虚函数的对象内存布局
cpp
class Base {
public:
virtual void Func1() {}
virtual void Func2() {}
int x_ = 10;
};
Base 对象的内存布局:
┌────────────────────────────────┐
│ vptr(虚指针) │ 8B(64位)│ → 指向 Base 的 vtable
├────────────────────────────────┤
│ x_ │ 4B │
├────────────────────────────────┤
│ (填充) │ 4B │
└────────────────────────────────┘
sizeof(Base) = 16
Base 的 vtable(虚函数表):
┌─────────────────────────────┐
│ [0] → Base::Func1() │
│ [1] → Base::Func2() │
│ [2] → type_info (RTTI) │
└─────────────────────────────┘
⭐ 单继承的对象内存布局
cpp
class Derived : public Base {
public:
void Func1() override {} // 重写 Func1
virtual void Func3() {} // 新增虚函数
int y_ = 20;
};
Derived 对象的内存布局:
┌────────────────────────────────┐
│ vptr(虚指针) │ 8B │ → 指向 Derived 的 vtable
├────────────────────────────────┤
│ x_(继承自 Base) │ 4B │
├────────────────────────────────┤
│ y_(Derived 自己)│ 4B │
└────────────────────────────────┘
sizeof(Derived) = 16
Derived 的 vtable(虚函数表):
┌──────────────────────────────────┐
│ [0] → Derived::Func1() ✏️ │ ← 重写了 Base 的 Func1
│ [1] → Base::Func2() │ ← 未重写,仍指向 Base 版本
│ [2] → Derived::Func3() 🆕 │ ← 新增的虚函数
│ [3] → type_info (RTTI) │
└──────────────────────────────────┘
多态调用的完整流程:
Base* p = new Derived();
p->Func1();
调用过程:
① p 的静态类型是 Base*,但 p 实际指向 Derived 对象
② 从对象起始位置取出 vptr
↓
③ vptr 指向 Derived 的 vtable
↓
④ 在 vtable 的 [0] 位置找到 Derived::Func1()
↓
⑤ 调用 Derived::Func1()
┌──────────┐ ┌─────────────────────────┐
│ Derived │ │ Derived 的 vtable │
│ 对象 │ │ │
│ ┌──────┐ │ │ [0] → Derived::Func1() │ ← 找到这里
│ │ vptr─┼─┼─────►│ [1] → Base::Func2() │
│ ├──────┤ │ │ [2] → Derived::Func3() │
│ │ x_ │ │ └─────────────────────────┘
│ ├──────┤ │
│ │ y_ │ │
│ └──────┘ │
└──────────┘
⭐ 多重继承的对象内存布局
cpp
class A {
public:
virtual void FuncA() {}
int a_ = 1;
};
class B {
public:
virtual void FuncB() {}
int b_ = 2;
};
class C : public A, public B {
public:
void FuncA() override {}
void FuncB() override {}
int c_ = 3;
};
C 对象的内存布局(多重继承 → 多个 vptr):
┌────────────────────────────────────┐
│ A 的子对象部分: │
│ ┌──────────────────────────────┐ │
│ │ vptr_A │ 8B │ │ → 指向 C 的 vtable_A
│ ├──────────────────────────────┤ │
│ │ a_ │ 4B + 4B 填充 │ │
│ └──────────────────────────────┘ │
├────────────────────────────────────┤
│ B 的子对象部分: │
│ ┌──────────────────────────────┐ │
│ │ vptr_B │ 8B │ │ → 指向 C 的 vtable_B
│ ├──────────────────────────────┤ │
│ │ b_ │ 4B + 4B 填充 │ │
│ └──────────────────────────────┘ │
├────────────────────────────────────┤
│ C 自己的成员: │
│ │ c_ │ 4B + 4B 填充 │ │
└────────────────────────────────────┘
sizeof(C) = 40
注意:多重继承时基类指针的转换涉及地址偏移!
A* pa = &c; // pa == &c(指向起始)
B* pb = &c; // pb == &c + sizeof(A 子对象)(指向 B 的子对象起始)
⭐ 菱形继承与虚继承的内存布局
普通菱形继承(A 存在两份):
虚继承(解决方案,A 只存在一份):
cpp
class A { public: int a_ = 1; };
class B : virtual public A { public: int b_ = 2; }; // 虚继承
class C : virtual public A { public: int c_ = 3; }; // 虚继承
class D : public B, public C { public: int d_ = 4; };
D 对象的内存布局(虚继承):
┌──────────────────────────────────┐
│ B 的子对象: │
│ ├── vbptr_B → B 的虚基类表 │ ← 虚基类指针(指向偏移量表)
│ ├── b_ │
├──────────────────────────────────┤
│ C 的子对象: │
│ ├── vbptr_C → C 的虚基类表 │
│ ├── c_ │
├──────────────────────────────────┤
│ D 自己的成员: │
│ ├── d_ │
├──────────────────────────────────┤
│ A 的子对象(共享): │ ← 只有一份 A!
│ ├── a_ │
└──────────────────────────────────┘
虚基类表(vbtable)记录了从当前位置到虚基类子对象的偏移量。
三、C++ 对象的生命周期
⭐ 对象的构造与析构流程
cpp
class Base {
public:
Base() { std::cout << "1. Base ctor\n"; }
virtual ~Base(){ std::cout << "6. Base dtor\n"; }
};
class Member {
public:
Member() { std::cout << "2. Member ctor\n"; }
~Member() { std::cout << "5. Member dtor\n"; }
};
class Derived : public Base {
Member m_;
public:
Derived() { std::cout << "3. Derived ctor\n"; }
~Derived() { std::cout << "4. Derived dtor\n"; }
};
// 输出顺序:
// 构造:1 → 2 → 3
// 析构:4 → 5 → 6
⭐ 为什么构造函数中不能调用虚函数?
cpp
class Base {
public:
Base() {
Print(); // ⚠️ 调用的是 Base::Print,不是 Derived::Print!
}
virtual void Print() { std::cout << "Base\n"; }
};
class Derived : public Base {
int value_ = 42;
public:
void Print() override {
std::cout << "Derived: " << value_ << "\n"; // value_ 此时还未初始化!
}
};
Derived d; // 输出 "Base",不是 "Derived: 42"
⚠️ 构造函数中调用虚函数不会表现出多态行为,因为此时派生类部分尚未构造完成。
四、智能指针的内部实现
⭐ unique_ptr 的实现原理
┌──────────────────────────────────────────────┐
│ unique_ptr<T> │
│ ┌────────────┐ │
│ │ raw_ptr_ │ ─────→ T 对象(堆上) │
│ └────────────┘ │
│ │
│ 特点: │
│ • 内部只有一个裸指针,sizeof == sizeof(T*) │
│ • 禁止拷贝(delete 了拷贝构造和赋值) │
│ • 允许移动(移动后原指针置 nullptr) │
│ • 析构时自动 delete │
│ • 零开销抽象(和裸指针一样高效) │
└──────────────────────────────────────────────┘
简化实现:
cpp
template <typename T>
class UniquePtr {
T* ptr_;
public:
explicit UniquePtr(T* p = nullptr) : ptr_(p) {}
~UniquePtr() { delete ptr_; }
// 禁止拷贝
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
// 允许移动
UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
};
⭐ shared_ptr 的实现原理
shared_ptr<T> 的结构(两个指针):
┌───────────────────┐
│ shared_ptr sp1 │
│ ┌─────────────┐ │ ┌───────────────────┐
│ │ 对象指针 ptr │──┼────→│ T 对象(堆上) │
│ ├─────────────┤ │ └───────────────────┘
│ │ 控制块指针 │──┼──┐
│ └─────────────┘ │ │
└───────────────────┘ │
│ ┌───────────────────────┐
┌───────────────────┐ └─→│ 控制块 (Control │
│ shared_ptr sp2 │ │ Block) │
│ ┌─────────────┐ │ │ │
│ │ 对象指针 ptr │──┼────→│ strong_count: 2 │ ← sp1 + sp2
│ ├─────────────┤ │ │ weak_count: 1 │ ← wp1
│ │ 控制块指针 │──┼────→│ deleter: default │
│ └─────────────┘ │ │ allocator: default │
└───────────────────┘ └───────────────────────┘
┌──→
┌───────────────────┐ │
│ weak_ptr wp1 │ │
│ ┌─────────────┐ │ │
│ │ 对象指针 ptr │──┼──┘ (不增加 strong_count)
│ ├─────────────┤ │
│ │ 控制块指针 │──┼────→ 同一个控制块
│ └─────────────┘ │
└───────────────────┘
引用计数的变化流程:
操作 strong_count weak_count
─────────────────────────────────────────────────────────
auto sp1 = make_shared<T>(); 1 1*
auto sp2 = sp1; 2 1
weak_ptr<T> wp = sp1; 2 2
sp2.reset(); 1 2
sp1.reset(); 0 → 析构T 2
(控制块还在)
wp 过期(expired)
wp 析构 — 1
最后一个 weak 析构 — 0 → 释放控制块
*注:控制块本身贡献 1 个 weak_count
⭐ make_shared vs shared_ptr(new T) 的内存差异
shared_ptr<T>(new T()): make_shared<T>():
分配 #1: 分配 #1(合并):
┌───────────┐ ┌───────────────────────┐
│ T 对象 │ │ 控制块 + T 对象 │
└───────────┘ │ ┌─────────────────┐ │
│ │ strong_count │ │
分配 #2: │ │ weak_count │ │
┌───────────────┐ │ │ deleter │ │
│ 控制块 │ │ ├─────────────────┤ │
│ strong_count │ │ │ T 的数据 │ │
│ weak_count │ │ └─────────────────┘ │
│ deleter │ └───────────────────────┘
└───────────────┘
优势:
2 次内存分配 • 只有 1 次内存分配
对象和控制块不连续 • 缓存友好(连续内存)
五、new / delete 的底层实现
⭐ new 表达式的执行步骤
new[] 和 delete[] 的区别
new T[5] 的内存布局:
┌──────────────────────────────────────────────────┐
│ 数组大小 (N=5) │ T[0] │ T[1] │ T[2] │ T[3] │ T[4] │
└──────────────────────────────────────────────────┘
↑ 实际分配起点 ↑ 返回给用户的指针
delete[] 时:
① 从返回指针前面读取 N = 5
② 逆序调用 5 次析构:T[4]→T[3]→T[2]→T[1]→T[0]
③ 释放整块内存
⚠️ new[] 必须用 delete[] 释放:
• new T[5] + delete → 只析构 T[0],泄漏 T[1]~T[4]!
• new T + delete[] → 未定义行为!
六、内存泄漏检测与防范
常见内存问题分类
⭐ 常用内存检测工具
| 工具 | 类型 | 平台 | 检测能力 |
|---|---|---|---|
| Valgrind | 动态分析 | Linux | 内存泄漏、越界、未初始化读取 |
| AddressSanitizer (ASan) | 编译器插桩 | 全平台 | 越界、UAF、双重释放、泄漏 |
| LeakSanitizer (LSan) | 编译器插桩 | Linux/macOS | 专注内存泄漏检测 |
| ThreadSanitizer (TSan) | 编译器插桩 | 全平台 | 数据竞争检测 |
| Clang Static Analyzer | 静态分析 | 全平台 | 编译期发现潜在问题 |
bash
# AddressSanitizer 使用示例
g++ -fsanitize=address -g main.cpp -o main
./main # 自动报告内存问题
# Valgrind 使用示例
valgrind --leak-check=full ./main
七、堆内存分配器原理
⭐ malloc 的底层原理简述
用户调用 malloc(size)
↓
┌─────────────────────────────────────────┐
│ glibc 的 ptmalloc2 分配器 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 空闲链表 (Free List) │ │
│ │ │ │
│ │ 小块 (< 512B): │ │
│ │ fastbin → 单链表, 不合并 │ │
│ │ smallbin → 双链表, 精确大小 │ │
│ │ │ │
│ │ 大块 (≥ 512B): │ │
│ │ largebin → 按大小范围排序 │ │
│ │ │ │
│ │ 超大块 (≥ 128KB): │ │
│ │ 直接 mmap() 映射 │ │
│ └──────────────────────────────────┘ │
│ │
│ 分配策略: │
│ ① 先查 fastbin/smallbin │
│ ② 再查 unsorted bin │
│ ③ 再查 largebin │
│ ④ 切割 top chunk │
│ ⑤ 调用 sbrk() 扩展堆 │
│ ⑥ 超大块直接 mmap() │
└─────────────────────────────────────────┘
💬 评论