Chenyj Space
返回博客
·34 分钟

C++ 内存管理深入解析:从堆栈到智能指针

C++内存管理智能指针RAIIC/C++

内存区域概览

C++ 程序的内存分为几个区域,每个区域有不同的特性和用途:

code
┌─────────────────────┐  高地址
│       栈 (Stack)      │  ← 局部变量、函数参数
├─────────────────────┤
│         ↓            │
│                      │
│         ↑            │
├─────────────────────┤
│      堆 (Heap)       │  ← 动态分配(new/malloc)
├─────────────────────┤
│    全局/静态存储区    │  ← 全局变量、静态变量
├─────────────────────┤
│    常量存储区        │  ← 字符串常量等
├─────────────────────┤
│    代码区 (Code)      │  ← 程序代码
└─────────────────────┘  低地址

1. 栈(Stack)

特点

  • 由编译器自动管理
  • 空间有限(通常 1-8MB)
  • 分配/释放速度极快(移动栈指针)
  • 后进先出(LIFO)
cpp
void function() {
    int a = 10;           // 栈上分配
    int arr[100];         // 栈上分配
    std::string str = "hello";  // str 对象在栈上,数据在堆上
    
    // 函数结束时自动释放
}

栈溢出

cpp
void stackOverflow() {
    int arr[10000000];  // 可能导致栈溢出
    // 解决:使用堆分配
    int* arr2 = new int[10000000];
    delete[] arr2;
}

2. 堆(Heap)

特点

  • 由程序员手动管理
  • 空间大(受系统内存限制)
  • 分配/释放速度较慢
  • 可能产生内存碎片
cpp
void heapExample() {
    // C 风格
    int* p1 = (int*)malloc(sizeof(int));
    free(p1);
    
    // C++ 风格
    int* p2 = new int(10);
    delete p2;
    
    // 数组
    int* arr = new int[100];
    delete[] arr;
}

3. 全局/静态存储区

cpp
int globalVar = 10;        // 全局变量
static int staticVar = 20; // 静态变量

void function() {
    static int localStatic = 30;  // 静态局部变量
    // 生命周期持续到程序结束
}

内存分配详解

1. new/delete 的实现

cpp
// new 的执行过程
int* p = new int(10);

// 等价于:
void* memory = operator new(sizeof(int));  // 分配内存
int* p = static_cast<int*>(memory);
*p = 10;  // 初始化

// delete 的执行过程
delete p;

// 等价于:
p->~int();  // 析构(对于对象)
operator delete(p);  // 释放内存

2. 内存池(Memory Pool)

问题:频繁 new/delete 导致内存碎片

解决方案:内存池

cpp
#include <vector>
#include <cstddef>

class MemoryPool {
private:
    struct Block {
        Block* next;
    };
    
    Block* freeList;
    std::vector<char*> chunks;
    size_t blockSize;
    size_t chunkSize;
    
public:
    MemoryPool(size_t blockSize, size_t chunkSize = 1024)
        : blockSize(blockSize), chunkSize(chunkSize), freeList(nullptr) {
        allocateChunk();
    }
    
    ~MemoryPool() {
        for (char* chunk : chunks) {
            delete[] chunk;
        }
    }
    
    void* allocate() {
        if (!freeList) {
            allocateChunk();
        }
        
        Block* block = freeList;
        freeList = freeList->next;
        return block;
    }
    
    void deallocate(void* ptr) {
        Block* block = static_cast<Block*>(ptr);
        block->next = freeList;
        freeList = block;
    }
    
private:
    void allocateChunk() {
        char* chunk = new char[blockSize * chunkSize];
        chunks.push_back(chunk);
        
        for (size_t i = 0; i < chunkSize; i++) {
            Block* block = reinterpret_cast<Block*>(chunk + i * blockSize);
            block->next = freeList;
            freeList = block;
        }
    }
};

// 使用示例
struct MyStruct {
    int data[10];
};

MemoryPool pool(sizeof(MyStruct));

MyStruct* obj = static_cast<MyStruct*>(pool.allocate());
// 使用 obj...
pool.deallocate(obj);

3. 定位 new(Placement New)

cpp
#include <new>

// 在已分配的内存上构造对象
char buffer[sizeof(MyStruct)];
MyStruct* obj = new (buffer) MyStruct();  // 定位 new

// 必须手动调用析构函数
obj->~MyStruct();

// 应用场景:
// 1. 内存池
// 2. 自定义内存管理
// 3. 避免内存碎片

智能指针

1. std::unique_ptr

特点:独占所有权,不能拷贝,只能移动

cpp
#include <memory>

// 创建
std::unique_ptr<int> p1(new int(10));
auto p2 = std::make_unique<int>(20);  // C++14,推荐

// 不能拷贝
// std::unique_ptr<int> p3 = p1;  // 错误

// 可以移动
std::unique_ptr<int> p3 = std::move(p1);
// p1 现在为 nullptr

// 自定义删除器
auto deleter = [](FILE* fp) { fclose(fp); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("test.txt", "r"), deleter);

// 数组版本
std::unique_ptr<int[]> arr(new int[100]);
arr[0] = 10;

// 使用场景
// 1. 动态分配的对象
// 2. 资源管理(文件、网络连接等)
// 3. 工厂函数返回值

2. std::shared_ptr

特点:共享所有权,引用计数

cpp
#include <memory>

// 创建
std::shared_ptr<int> p1(new int(10));
auto p2 = std::make_shared<int>(20);  // 推荐,更高效

// 拷贝和赋值
std::shared_ptr<int> p3 = p1;  // 引用计数 +1
p3 = p2;  // p1 引用计数 -1,p2 引用计数 +1

// 引用计数
std::cout << p1.use_count() << std::endl;  // 输出引用计数

// 自定义删除器
std::shared_ptr<int> p4(new int(10), [](int* p) {
    std::cout << "Custom deleter\n";
    delete p;
});

// 循环引用问题
struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "Node destroyed\n"; }
};

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1;  // 循环引用,内存泄漏!

3. std::weak_ptr

特点:弱引用,不增加引用计数,解决循环引用

cpp
#include <memory>

// 解决循环引用
struct Node {
    std::weak_ptr<Node> next;  // 使用 weak_ptr
    ~Node() { std::cout << "Node destroyed\n"; }
};

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1;  // 不会导致内存泄漏

// 使用 weak_ptr
std::weak_ptr<int> wp;
{
    auto sp = std::make_shared<int>(10);
    wp = sp;
    
    // 检查是否有效
    if (auto locked = wp.lock()) {
        std::cout << *locked << std::endl;  // 安全访问
    }
}
// sp 销毁后,wp.lock() 返回 nullptr

// 应用场景:
// 1. 打破循环引用
// 2. 缓存系统
// 3. 观察者模式

4. 智能指针最佳实践

cpp
// 1. 优先使用 make_shared/make_unique
auto p1 = std::make_shared<int>(10);      // 推荐
std::shared_ptr<int> p2(new int(10));     // 不推荐

// 2. 避免混用原始指针和智能指针
void bad(std::shared_ptr<int> p1, std::shared_ptr<int> p2) {}
int* raw = new int(10);
bad(std::shared_ptr<int>(raw), std::shared_ptr<int>(raw));  // 错误!

// 3. 使用 unique_ptr 作为默认选择
std::unique_ptr<int> getDefault() {
    return std::make_unique<int>(10);
}

// 4. 只在需要共享所有权时使用 shared_ptr
class Resource {
    std::shared_ptr<Data> data;  // 多个 Resource 共享数据
};

// 5. 避免返回 shared_ptr 的原始指针
class Bad {
    std::shared_ptr<int> data;
public:
    int* get() { return data.get(); }  // 危险!
};

RAII 模式

1. 什么是 RAII

RAII(Resource Acquisition Is Initialization):

  • 资源获取即初始化
  • 在构造函数中获取资源
  • 在析构函数中释放资源
  • 利用栈对象的自动销毁特性
cpp
// RAII 封装文件操作
class FileHandle {
private:
    FILE* fp;
    
public:
    FileHandle(const char* filename, const char* mode)
        : fp(fopen(filename, mode)) {
        if (!fp) {
            throw std::runtime_error("Failed to open file");
        }
    }
    
    ~FileHandle() {
        if (fp) {
            fclose(fp);
        }
    }
    
    // 禁止拷贝
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // 允许移动
    FileHandle(FileHandle&& other) noexcept : fp(other.fp) {
        other.fp = nullptr;
    }
    
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (fp) fclose(fp);
            fp = other.fp;
            other.fp = nullptr;
        }
        return *this;
    }
    
    // 提供访问接口
    FILE* get() const { return fp; }
    operator bool() const { return fp != nullptr; }
};

// 使用
void writeFile() {
    FileHandle file("test.txt", "w");
    if (file) {
        fprintf(file.get(), "Hello, RAII!");
    }
    // 离开作用域自动关闭文件
}

2. RAII 应用场景

cpp
// 1. 互斥锁
class LockGuard {
private:
    std::mutex& mtx;
    
public:
    LockGuard(std::mutex& m) : mtx(m) {
        mtx.lock();
    }
    
    ~LockGuard() {
        mtx.unlock();
    }
    
    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;
};

// 2. 内存管理
class Buffer {
private:
    char* data;
    size_t size;
    
public:
    Buffer(size_t sz) : size(sz), data(new char[sz]) {}
    ~Buffer() { delete[] data; }
    
    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;
    
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    
    char* get() { return data; }
    size_t getSize() const { return size; }
};

// 3. 数据库连接
class DatabaseConnection {
private:
    Connection* conn;
    
public:
    DatabaseConnection(const std::string& url) 
        : conn(connect(url)) {
        if (!conn) {
            throw std::runtime_error("Connection failed");
        }
    }
    
    ~DatabaseConnection() {
        if (conn) {
            disconnect(conn);
        }
    }
    
    Connection* get() { return conn; }
};

内存泄漏检测

1. 常见内存泄漏场景

cpp
// 1. 忘记释放
void leak1() {
    int* p = new int(10);
    // 忘记 delete p
}

// 2. 异常导致泄漏
void leak2() {
    int* p = new int(10);
    throw std::runtime_error("error");  // p 泄漏!
    delete p;
}

// 3. 循环引用
struct Node {
    std::shared_ptr<Node> next;
};

void leak3() {
    auto n1 = std::make_shared<Node>();
    auto n2 = std::make_shared<Node>();
    n1->next = n2;
    n2->next = n1;  // 循环引用
}

// 4. 基类析构函数非虚
class Base {
public:
    ~Base() {}  // 非虚析构函数
};

class Derived : public Base {
    int* data;
public:
    Derived() : data(new int(10)) {}
    ~Derived() { delete data; }
};

void leak4() {
    Base* p = new Derived();
    delete p;  // 不会调用 Derived 的析构函数!
}

2. 检测工具

bash
# 1. Valgrind(Linux)
valgrind --leak-check=full ./program

# 2. AddressSanitizer(GCC/Clang)
g++ -fsanitize=address -g program.cpp
./a.out

# 3. Visual Studio 内存检测器
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>

int main() {
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    // 程序结束时自动检测泄漏
}

# 4. 自定义内存跟踪
class MemoryTracker {
    static std::map<void*, size_t> allocations;
    
public:
    static void* allocate(size_t size) {
        void* ptr = malloc(size);
        allocations[ptr] = size;
        return ptr;
    }
    
    static void deallocate(void* ptr) {
        allocations.erase(ptr);
        free(ptr);
    }
    
    static void report() {
        std::cout << "Memory leaks: " << allocations.size() << std::endl;
        for (auto& [ptr, size] : allocations) {
            std::cout << "  " << ptr << ": " << size << " bytes\n";
        }
    }
};

最佳实践总结

内存管理原则

  1. 优先使用栈对象:自动管理生命周期
  2. 使用 RAII:封装资源管理
  3. 使用智能指针:避免手动 new/delete
  4. 避免裸指针:除非必要
  5. 明确所有权:谁负责释放

智能指针选择

场景选择
独占所有权unique_ptr
共享所有权shared_ptr
打破循环引用weak_ptr
需要自定义删除器unique_ptrshared_ptr

性能考虑

cpp
// 1. make_shared 更高效
auto p1 = std::make_shared<int>(10);  // 一次内存分配
std::shared_ptr<int> p2(new int(10)); // 两次内存分配

// 2. 避免不必要的 shared_ptr
void process(std::shared_ptr<int> p) {  // 拷贝,增加引用计数
    // 如果不需要共享所有权,用 unique_ptr 或引用
}

void process(int& ref) {  // 更高效
    // ...
}

// 3. 使用移动语义
std::unique_ptr<int> create() {
    auto p = std::make_unique<int>(10);
    return p;  // 移动,不是拷贝
}

总结

核心概念

  1. 内存区域:栈、堆、全局/静态、常量、代码
  2. 分配方式:new/delete、malloc/free、智能指针
  3. 管理策略:RAII、智能指针、内存池
  4. 检测工具:Valgrind、AddressSanitizer、自定义跟踪

常见错误

  • 忘记释放内存
  • 重复释放
  • 使用已释放的内存
  • 循环引用
  • 基类析构函数非虚

学习资源


延伸阅读