C/C++ 内存模型补充
本文档是对《C/C++ 内存模型》的补充,涵盖更深入的内存管理知识。
一、C 语言内存管理基础
1.1 malloc/free vs new/delete
| 特性 | malloc/free | new/delete |
|---|---|---|
| 来源 | C 标准库 | C++ 运算符 |
| 构造/析构 | 不调用 | 自动调用 |
| 类型安全 | 返回 void* | 类型安全 |
| 失败处理 | 返回 NULL | 抛出异常 |
| 数组支持 | 手动计算大小 | new[]/delete[] |
// C 风格
int* arr = (int*)malloc(10 * sizeof(int));
if (arr != NULL) {
// 使用 arr
free(arr); // 必须手动释放
}
// C++ 风格
int* arr = new int[10];
// 使用 arr
delete[] arr; // 必须手动释放
1.2 栈 vs 堆
| 特性 | 栈 (Stack) | 堆 (Heap) |
|---|---|---|
| 分配方式 | 自动 | 手动 |
| 释放方式 | 自动 | 手动 |
| 速度 | 快 | 慢 |
| 大小限制 | 较小 (MB 级) | 较大 (GB 级) |
| 生命周期 | 作用域结束 | 显式释放 |
| 碎片 | 无 | 可能有 |
void example() {
// 栈分配 - 自动管理
int x = 10;
char buffer[256];
// 堆分配 - 手动管理
int* p = new int(10);
delete p; // 必须记得释放
}
二、内存布局
2.1 进程内存空间
高地址
┌─────────────────────────┐
│ 内核空间 │
├─────────────────────────┤
│ 命令行参数 │
│ 环境变量 │
├─────────────────────────┤
│ ↓ 栈 │ ← 局部变量、函数参数
│ 生长方向 │
│ │
│ │
│ ↑ 堆 │ ← 动态分配 (malloc/new)
│ 生长方向 │
├─────────────────────────┤
│ BSS 段 │ ← 未初始化全局变量
├─────────────────────────┤
│ 数据段 │ ← 已初始化全局变量
├─────────────────────────┤
│ 代码段 (.text) │ ← 程序代码
└─────────────────────────┘
低地址
2.2 各段详解
| 段名 | 内容 | 权限 | 示例 |
|---|---|---|---|
| 代码段 | 机器指令 | 只读 | 函数体 |
| 数据段 | 已初始化全局/静态变量 | 读写 | static int x = 10; |
| BSS 段 | 未初始化全局/静态变量 | 读写 | static int y; |
| 堆 | 动态分配内存 | 读写 | malloc, new |
| 栈 | 局部变量、函数调用 | 读写 | 局部变量 |
#include <stdio.h>
#include <stdlib.h>
int g_init = 10; // 数据段
int g_uninit; // BSS 段
void func() {
static int s_init = 20; // 数据段
static int s_uninit; // BSS 段
int local = 30; // 栈
int* heap = malloc(100); // 堆
}
int main() {
const char* str = "hello"; // "hello"在代码段,str 在栈
return 0;
}
三、RAII 原则详解
3.1 什么是 RAII
RAII (Resource Acquisition Is Initialization) - 资源获取即初始化
核心思想:
- 资源在对象构造时获取
- 资源在对象析构时释放
- 利用栈对象的生命周期自动管理资源
3.2 RAII 的优势
// ❌ 非 RAII - 容易泄露
void process_file_bad() {
FILE* f = fopen("test.txt", "r");
if (!f) return;
// 处理文件
if (some_error) {
return; // 忘记 fclose,资源泄露!
}
fclose(f);
}
// ✅ RAII - 安全
class FileHandle {
FILE* file_;
public:
FileHandle(const char* path, const char* mode)
: file_(fopen(path, mode)) {}
~FileHandle() {
if (file_) fclose(file_);
}
FILE* get() { return file_; }
// 禁止拷贝
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void process_file_good() {
FileHandle f("test.txt", "r");
if (!f.get()) return;
// 处理文件
if (some_error) {
return; // 安全!析构函数会自动 fclose
}
// 离开作用域自动释放
}
3.3 标准库中的 RAII
// unique_ptr - 内存资源的 RAII
unique_ptr<int> p(new int(10));
// lock_guard - 锁资源的 RAII
void thread_safe() {
std::lock_guard<std::mutex> lock(mutex_);
// 临界区,离开作用域自动解锁
}
// ifstream - 文件资源的 RAII
std::ifstream file("test.txt");
// 离开作用域自动关闭
四、移动语义
4.1 左值 vs 右值
| 类型 | 特点 | 示例 |
|---|---|---|
| 左值 (lvalue) | 有名字,可取地址 | int x;, x |
| 右值 (rvalue) | 临时值,不可取地址 | 5, x + 1 |
int x = 10; // x 是左值
int y = x + 5; // x+5 是右值
std::vector<int> v;
v.push_back(10); // 10 是右值
4.2 std::move 详解
std::move 将左值转换为右值引用,启用移动语义。
#include <utility>
#include <iostream>
class Buffer {
int* data_;
size_t size_;
public:
Buffer(size_t size) : size_(size) {
data_ = new int[size];
std::cout << "构造\n";
}
// 拷贝构造函数 - 深拷贝
Buffer(const Buffer& other) : size_(other.size_) {
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "拷贝构造\n";
}
// 移动构造函数 - 窃取资源
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
std::cout << "移动构造\n";
}
~Buffer() {
std::cout << "析构\n";
delete[] data_;
}
};
void test() {
Buffer b1(100);
Buffer b2 = b1; // 调用拷贝构造
Buffer b3 = std::move(b1); // 调用移动构造
}
4.3 移动语义在智能指针中的应用
// unique_ptr 只能移动,不能拷贝
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1); // p1 变为空
// shared_ptr 可以拷贝(引用计数+1),也可以移动(引用计数不变)
std::shared_ptr<int> s1 = std::make_shared<int>(10);
std::shared_ptr<int> s2 = s1; // 拷贝,引用计数=2
std::shared_ptr<int> s3 = std::move(s1); // 移动,引用计数=2
五、自定义删除器
5.1 为什么需要自定义删除器
默认删除器使用 delete,但有些资源需要特殊释放方式:
- FILE* 需要
fclose - 自定义内存池需要特殊释放
- 数组需要
delete[]
5.2 使用示例
// 1. 管理 FILE*
struct FileDeleter {
void operator()(FILE* f) const {
if (f) fclose(f);
}
};
std::unique_ptr<FILE, FileDeleter> file(
fopen("test.txt", "r"),
FileDeleter{}
);
// 2. 管理数组
auto arr = std::unique_ptr<int[]>(new int[100]);
// 3. 使用 lambda 作为删除器
auto deleter = [](int* p) {
std::cout << "删除 " << *p << std::endl;
delete p;
};
std::unique_ptr<int, decltype(deleter)> p(new int(10), deleter);
// 4. 管理 OpenGL 资源
struct GLTextureDeleter {
void operator()(GLuint* id) const {
if (id) {
glDeleteTextures(1, id);
delete id;
}
}
};
std::unique_ptr<GLuint, GLTextureDeleter> texture(
new GLuint,
GLTextureDeleter{}
);
5.3 shared_ptr 的自定义删除器
// shared_ptr 的删除器存储在控制块中,不影响指针大小
std::shared_ptr<FILE> file(
fopen("test.txt", "r"),
[](FILE* f) {
if (f) fclose(f);
}
);
// 大小对比
std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 16 字节
std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 8 字节
// unique_ptr 带删除器会增大
std::cout << sizeof(std::unique_ptr<int, void(*)(int*)>) << std::endl; // 16 字节
六、线程安全
6.1 控制块的原子操作
shared_ptr 的控制块使用原子操作保证线程安全:
// 引用计数增减是原子的
std::shared_ptr<int> sp = std::make_shared<int>(10);
// 多线程安全地拷贝
std::thread t1([&sp]() {
auto copy1 = sp; // 原子增加引用计数
});
std::thread t2([&sp]() {
auto copy2 = sp; // 原子增加引用计数
});
6.2 线程安全保证
| 操作 | 线程安全 |
|---|---|
| 引用计数增减 | ✅ 是 |
| 不同 shared_ptr 对象访问同一对象 | ✅ 是 |
| 同一 shared_ptr 对象的并发修改 | ❌ 否 |
| 所管理对象的访问 | ❌ 否 |
std::shared_ptr<Data> ptr = std::make_shared<Data>();
// ✅ 安全 - 不同 shared_ptr 对象
std::thread t1([ptr]() { auto p = ptr; });
std::thread t2([ptr]() { auto p = ptr; });
// ❌ 不安全 - 同一 shared_ptr 对象并发修改
std::thread t1([&ptr]() { ptr.reset(); });
std::thread t2([&ptr]() { ptr = make_shared<Data>(); });
// ❌ 不安全 - 所管理对象的访问需要额外同步
ptr->value = 10; // 需要 mutex 保护
6.3 线程安全的最佳实践
class ThreadSafeCounter {
std::shared_ptr<int> counter_;
std::mutex mutex_;
public:
ThreadSafeCounter()
: counter_(std::make_shared<int>(0)) {}
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
(*counter_)++;
}
int get() const {
std::lock_guard<std::mutex> lock(mutex_);
return *counter_;
}
};
七、常见陷阱和最佳实践
7.1 常见陷阱
// ❌ 陷阱 1: 混用 malloc 和 delete
int* p1 = (int*)malloc(sizeof(int));
delete p1; // 未定义行为!应该用 free(p1)
// ❌ 陷阱 2: 混用 new 和 free
int* p2 = new int(10);
free(p2); // 未定义行为!应该用 delete p2
// ❌ 陷阱 3: 手动 delete 智能指针管理的对象
std::unique_ptr<int> up(new int(10));
delete up.get(); // 双重释放!
// ❌ 陷阱 4: 循环引用
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // 应该是 weak_ptr
};
// ❌ 陷阱 5: shared_ptr 管理栈对象
int x = 10;
std::shared_ptr<int> sp(&x); // 危险!x 析构后 sp 会再次释放
7.2 最佳实践
// ✅ 实践 1: 优先使用 make_shared/make_unique
auto p1 = std::make_shared<int>(10);
auto p2 = std::make_unique<int>(10);
// ✅ 实践 2: 明确所有权
// 独占所有权用 unique_ptr
std::unique_ptr<Resource> resource;
// 共享所有权用 shared_ptr
std::shared_ptr<Data> data;
// 观察不拥有用 weak_ptr
std::weak_ptr<Data> observer;
// ✅ 实践 3: 在类中使用智能指针
class MyClass {
std::unique_ptr<Helper> helper_; // 独占
std::shared_ptr<Config> config_; // 共享
std::weak_ptr<Observer> observer_; // 观察
};
// ✅ 实践 4: 工厂函数返回智能指针
std::unique_ptr<Shape> createShape(ShapeType type) {
switch(type) {
case ShapeType::Circle:
return std::make_unique<Circle>();
case ShapeType::Rect:
return std::make_unique<Rectangle>();
}
}
// ✅ 实践 5: 容器存储智能指针
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>());
7.3 智能指针选择指南
┌─────────────────────┐
│ 需要管理资源吗? │
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
│ 否 │ 是 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 单一所有者? │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────┼─────────┐ │
│ │ 是 │ 否 │ │
│ ▼ ▼ │ │
│ unique_ptr shared_ptr │ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ 需要观察引用?│ │ │
│ └───────┬──────┘ │ │
│ │ │ │
│ ┌────────┼───────┐│ │
│ │ 是 │ 否 ││ │
│ ▼ ▼ ││ │
│ weak_ptr 不需要 ││ │
│ ││ │
└────────────────────────┘│ │
▼ │
原始指针/引用 │
八、总结
| 主题 | 关键点 |
|---|---|
| 内存管理 | 栈自动、堆手动、智能指针自动化 |
| 内存布局 | 代码段、数据段、BSS、堆、栈 |
| RAII | 构造获取、析构释放、利用栈生命周期 |
| 移动语义 | std::move、右值引用、避免拷贝 |
| 自定义删除器 | 特殊资源释放、lambda 删除器 |
| 线程安全 | 引用计数原子、对象访问需同步 |
| 最佳实践 | make_shared、明确所有权、避免循环引用 |