C/C++ 内存模型补充

5 阅读7分钟

C/C++ 内存模型补充

本文档是对《C/C++ 内存模型》的补充,涵盖更深入的内存管理知识。


一、C 语言内存管理基础

1.1 malloc/free vs new/delete

特性malloc/freenew/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) - 资源获取即初始化

核心思想:

  1. 资源在对象构造时获取
  2. 资源在对象析构时释放
  3. 利用栈对象的生命周期自动管理资源

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、明确所有权、避免循环引用