Skip to content

C++ 内存模型

C++ 内存模型是理解多线程、性能优化和底层编程的基石。搞懂它,才能写出真正正确且高效的并发代码。

内存区域划分

C++ 程序运行时,内存分为以下几个区域:

C++ 进程内存布局(从高地址到低地址)
┌─────────────────────────────┐  高地址
│  内核空间(用户不可访问)      │
├─────────────────────────────┤
│  栈区 Stack                  │  ← 函数调用帧、局部变量、函数参数
│  (向下增长)                 │
├─────────────────────────────┤
│  ↓ 栈增长方向                │
│                             │
│  ↑ 堆增长方向                │
├─────────────────────────────┤
│  堆区 Heap                   │  ← new/malloc 动态分配
├─────────────────────────────┤
│  BSS 段(未初始化全局/静态)   │  ← 零初始化
├─────────────────────────────┤
│  数据段 Data(已初始化全局)   │  ← 有初始值的全局/静态变量
├─────────────────────────────┤
│  代码段 Text(只读)          │  ← 程序指令、字符串字面量
└─────────────────────────────┘  低地址
cpp
#include <iostream>

// 数据段:已初始化全局变量
int global_var = 42;

// BSS 段:未初始化全局变量(自动零初始化)
int uninit_global;

void demo() {
    // 栈区:局部变量
    int local = 10;
    int arr[100];  // 100 个 int,直接在栈上

    // 堆区:动态分配
    int* heap_ptr = new int(99);
    delete heap_ptr;

    // 代码段:字符串字面量(只读)
    const char* str = "hello";  // str 在栈上,"hello" 在代码段
}

对象存储期(Storage Duration)

C++ 定义了四种存储期:

存储期关键字生命周期存储位置
自动无(局部变量)进入/离开作用域
静态static / 全局程序整个生命周期数据段/BSS
线程thread_local线程生命周期TLS 段
动态new / malloc手动控制
cpp
#include <thread>

// 静态存储期
static int s_count = 0;

// 线程局部存储
thread_local int tls_id = 0;

void worker(int id) {
    tls_id = id;  // 每个线程有独立副本
    ++s_count;    // 共享,需要同步!
    std::cout << "thread " << tls_id << "\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    t1.join(); t2.join();
    // tls_id 在每个线程中独立,s_count 是共享的
}

C++ 内存模型:多线程视角

C++11 引入了正式的内存模型,定义了多线程程序中内存操作的可见性和顺序。

内存序(Memory Order)

cpp
#include <atomic>
#include <thread>

std::atomic<int> x{0}, y{0};
int r1, r2;

// 六种内存序
// memory_order_relaxed   — 最宽松,只保证原子性,不保证顺序
// memory_order_consume   — 数据依赖顺序(已废弃,用 acquire 代替)
// memory_order_acquire   — 读屏障:此操作之后的读写不能重排到前面
// memory_order_release   — 写屏障:此操作之前的读写不能重排到后面
// memory_order_acq_rel   — 同时具备 acquire 和 release 语义
// memory_order_seq_cst   — 顺序一致性(默认,最强,最慢)

void thread1() {
    x.store(1, std::memory_order_release);  // 发布
}

void thread2() {
    while (x.load(std::memory_order_acquire) != 1);  // 获取
    // 此处可以安全读取 thread1 在 store 之前写入的所有数据
}

happens-before 关系

happens-before 是内存模型的核心概念:
如果 A happens-before B,则 A 的所有内存效果对 B 可见。

建立 happens-before 的方式:
1. 同一线程内,语句顺序(sequenced-before)
2. mutex 的 unlock happens-before 后续的 lock
3. atomic release-store happens-before acquire-load(看到该值时)
4. thread::join() 使被 join 的线程 happens-before join 之后的代码
5. std::promise::set_value happens-before std::future::get()
cpp
#include <atomic>
#include <cassert>

std::atomic<bool> ready{false};
int data = 0;

void producer() {
    data = 42;                              // (1)
    ready.store(true, std::memory_order_release);  // (2) release
}

void consumer() {
    while (!ready.load(std::memory_order_acquire));  // (3) acquire
    assert(data == 42);  // (4) 永远成立!
    // (2) release-store happens-before (3) acquire-load
    // (1) sequenced-before (2)
    // 所以 (1) happens-before (4)
}

栈内存详解

cpp
#include <iostream>

void foo(int a, int b) {
    // 栈帧结构(x86-64):
    // [返回地址] [保存的 rbp] [局部变量] [参数(部分)]
    int local = a + b;
    std::cout << local << "\n";
}  // 函数返回,栈帧自动销毁

// 栈溢出示例(不要运行!)
void stack_overflow() {
    int arr[1000000];  // 4MB,超过默认栈大小(通常 1-8MB)
    stack_overflow();  // 无限递归
}

// 正确做法:大数组用堆
void safe_large_array() {
    auto arr = std::make_unique<int[]>(1000000);  // 堆分配
}

堆内存与分配器

cpp
#include <memory>
#include <new>

// C++ 堆分配方式对比
void heap_demo() {
    // 1. new/delete(推荐用智能指针包装)
    int* p1 = new int(42);
    delete p1;

    // 2. new[]/delete[](数组)
    int* arr = new int[10]{};
    delete[] arr;

    // 3. placement new(在已有内存上构造)
    alignas(int) char buf[sizeof(int)];
    int* p2 = new (buf) int(99);  // 在 buf 上构造
    p2->~int();                   // 手动析构(不调用 delete!)

    // 4. 智能指针(推荐)
    auto sp = std::make_shared<int>(42);
    auto up = std::make_unique<int>(99);

    // 5. 自定义分配器(高性能场景)
    std::allocator<int> alloc;
    int* p3 = alloc.allocate(10);   // 分配 10 个 int 的空间
    alloc.construct(p3, 42);        // 构造
    alloc.destroy(p3);              // 析构
    alloc.deallocate(p3, 10);       // 释放
}

内存对齐

cpp
#include <cstddef>

// 结构体内存对齐
struct Unoptimized {
    char  a;    // 1 byte
    // 3 bytes padding
    int   b;    // 4 bytes
    char  c;    // 1 byte
    // 7 bytes padding
    double d;   // 8 bytes
};  // sizeof = 24

struct Optimized {
    double d;   // 8 bytes
    int    b;   // 4 bytes
    char   a;   // 1 byte
    char   c;   // 1 byte
    // 2 bytes padding
};  // sizeof = 16

// 手动指定对齐
struct alignas(64) CacheAligned {  // 对齐到 cache line
    int data[16];
};

// 检查对齐
static_assert(alignof(CacheAligned) == 64);
static_assert(offsetof(Optimized, b) == 8);

内存泄漏与常见陷阱

cpp
// 陷阱 1:忘记 delete
void leak() {
    int* p = new int(42);
    // 忘记 delete p; → 内存泄漏
}

// 陷阱 2:double free
void double_free() {
    int* p = new int(42);
    delete p;
    delete p;  // 未定义行为!
}

// 陷阱 3:悬空指针
int* dangling() {
    int local = 42;
    return &local;  // 返回栈变量地址!函数返回后无效
}

// 陷阱 4:数组越界
void out_of_bounds() {
    int arr[5];
    arr[5] = 1;  // 未定义行为!
}

// 正确做法:RAII + 智能指针
void safe() {
    auto p = std::make_unique<int>(42);
    // 自动释放,不会泄漏
}

工具:检测内存问题

bash
# AddressSanitizer(编译时插桩,推荐)
g++ -fsanitize=address -g main.cpp -o main
./main

# Valgrind(运行时检测)
valgrind --leak-check=full ./main

# 编译器静态分析
clang-tidy main.cpp --checks='clang-analyzer-*'

关键认知

C++ 内存模型的核心是:你对内存的每一次操作都有明确的语义。理解存储期、对齐、内存序,是写出高性能、无 bug 的 C++ 代码的前提。

系统学习 C++ 生态,深入底层架构