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++ 代码的前提。