C++ 内存管理

内存管理是 C++ 最难也最强大的部分。和 Java/Python 不同,C++ 程序员直接控制内存——能极致优化性能,但稍不留神就是内存泄漏段错误。好消息是,现代 C++(C++11 起)用智能指针解决了大部分痛点。这一章我们看 C++ 内存管理的全部要点。

一、内存的五个区

C++ 程序的内存大致分五个区:

#include <iostream>
using namespace std;

int globalVar = 1;       // 全局区:程序整个生命周期

void demo() {
    int localVar = 2;            // 栈:函数返回时自动销毁
    static int staticVar = 3;    // 全局区(但只在此函数可见):程序结束销毁

    int* heapVar = new int(4);   // 堆:new 出来的,必须手动 delete
    delete heapVar;              // 不删就内存泄漏!
}

int main() {
    demo();

    // 栈的特点:快、自动管理、空间小(默认几 MB)
    // 堆的特点:慢、手动管理、空间大(几乎全部可用内存)

    // 栈上分配:直接定义变量
    int a = 10;             // 在栈上
    int arr[100];           // 在栈上(数组)

    // 堆上分配:用 new
    int* p = new int(20);   // 在堆上,*p = 20
    int* bigArr = new int[1000000];  // 堆上分配 400 万字节(栈上放不下)
    delete p;
    delete[] bigArr;        // 注意:数组用 delete[]
    return 0;
}

二、new 与 delete

new 在堆上分配内存并构造对象,delete 析构并释放:

#include <iostream>
using namespace std;

class Point {
public:
    int x, y;
    Point(int x = 0, int y = 0) : x(x), y(y) {
        cout << "Point(" << x << "," << y << ") 构造" << endl;
    }
    ~Point() {
        cout << "Point 析构" << endl;
    }
};

int main() {
    // new:在堆上分配并构造对象,返回指针
    Point* p1 = new Point;            // 默认构造
    Point* p2 = new Point(3, 4);      // 带参构造
    Point* arr = new Point[2];        // 数组:每个元素默认构造

    cout << p2->x << ", " << p2->y << endl;   // 3, 4

    // delete:析构并释放内存
    delete p1;
    delete p2;
    delete[] arr;                     // 数组用 delete[](用 delete 是未定义行为)

    // new 失败默认抛 bad_alloc 异常(也可以用 nothrow 不抛)
    int* huge = new(nothrow) int[10000000000LL];
    if (huge == nullptr) {
        cout << "分配失败" << endl;
    }
    return 0;
}

关键规则:

三、内存泄漏与各种坑

手动内存管理容易出 5 类典型 bug:

#include <iostream>
using namespace std;

void leak() {
    int* p = new int(42);
    // 函数返回,p 这个局部变量(指针本身)销毁
    // 但它指向的堆内存还在,且再也没人能引用到它了
    // —— 这就是"内存泄漏"
}

void alsoLeak() {
    int* p = new int(42);
    p = new int(100);   // 把第一个 int "丢了",又没 delete
    delete p;           // 只删了第二个
}

void doubleFree() {
    int* p = new int(42);
    delete p;
    // delete p;          // 错误:重复释放,程序崩溃
}

void useAfterFree() {
    int* p = new int(42);
    delete p;
    // cout << *p << endl;  // 错误:使用已释放内存,未定义行为
}

void arrayMismatch() {
    int* arr = new int[10];
    // delete arr;        // 错误:数组应该用 delete[]
    delete[] arr;         // 正确
}

int main() {
    leak();
    alsoLeak();
    return 0;
}

这些坑让 C++ 名声"难"。但现代 C++ 用下面的方法基本能根除它们。

四、RAII:资源管理的核心思想

RAII(Resource Acquisition Is Initialization)是 C++ 最重要的设计哲学。核心思想:

把资源(内存/文件/锁)绑定到对象的生命周期——构造时获取,析构时释放。

#include <iostream>
#include <fstream>
#include <mutex>
using namespace std;

// RAII:Resource Acquisition Is Initialization
// 核心思想:把资源(内存/文件/锁)绑定到对象的生命周期
//          构造时获取,析构时释放

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t n) : data(new int[n]), size(n) {}
    ~Buffer() { delete[] data; }   // 析构自动释放!

    int& operator[](size_t i) { return data[i]; }
};

void safeFunc() {
    Buffer buf(100);     // 构造时分配
    buf[0] = 42;
    // 即便这里抛异常、提前 return,析构函数也会被调用
    // buf 自动释放内存——不会泄漏
}

// STL 已经按 RAII 实现:
// - vector、string:管理动态数组/字符缓冲
// - fstream:管理文件句柄
// - lock_guard / unique_lock:管理互斥锁
// - 智能指针:管理堆对象

void safeFile() {
    ifstream file("data.txt");   // 构造时打开
    if (!file) return;
    // 读文件...
    // 函数返回,file 析构自动关闭
}

RAII 的优势:

STL 容器(vector、string)、fstream、智能指针全部基于 RAII——这就是为什么现代 C++ 程序员几乎不写裸 new/delete。

五、unique_ptr:独占所有权(C++11)

unique_ptr 是智能指针的默认选择。它独占一个对象,不能拷贝,只能移动:

#include <iostream>
#include <memory>
using namespace std;

class Resource {
public:
    Resource() { cout << "Resource 构造" << endl; }
    ~Resource() { cout << "Resource 析构" << endl; }
    void use() { cout << "使用 Resource" << endl; }
};

int main() {
    // unique_ptr:独占所有权,不能拷贝,只能移动
    // 出作用域自动 delete

    auto p = make_unique<Resource>();   // C++14 推荐
    // 等价于 unique_ptr<Resource> p(new Resource);

    p->use();
    (*p).use();

    // 转移所有权(只能移动)
    unique_ptr<Resource> p2 = move(p);
    // p 现在是 nullptr
    if (!p) cout << "p 已被搬空" << endl;
    p2->use();

    // 出作用域时 p2 自动销毁 Resource
    return 0;
}

unique_ptr 的特点:

六、shared_ptr:共享所有权

shared_ptr 用引用计数实现共享所有权——多个 shared_ptr 指向同一对象,最后一个销毁时对象才析构:

#include <iostream>
#include <memory>
using namespace std;

class Foo {
public:
    Foo() { cout << "Foo 构造" << endl; }
    ~Foo() { cout << "Foo 析构" << endl; }
};

int main() {
    // shared_ptr:共享所有权,引用计数
    // 多个 shared_ptr 可以指向同一个对象
    // 最后一个销毁时,对象才被 delete

    auto p1 = make_shared<Foo>();    // 引用计数 = 1
    cout << "count: " << p1.use_count() << endl;   // 1

    {
        shared_ptr<Foo> p2 = p1;     // 拷贝,引用计数 = 2
        cout << "count: " << p1.use_count() << endl;   // 2
    }                                // p2 销毁,引用计数 = 1

    cout << "count: " << p1.use_count() << endl;   // 1
    // 函数结束,p1 销毁,引用计数 = 0,Foo 被析构

    // ⚠️ shared_ptr 的坑:循环引用
    // 两个对象互相用 shared_ptr 指向对方,引用计数永远不为 0
    // 导致内存泄漏。解决:用 weak_ptr 打破循环
    return 0;
}

shared_ptr 的注意点:

七、weak_ptr:打破循环引用

weak_ptr 是 shared_ptr 的"观察者",不增加引用计数,主要用于打破循环引用:

#include <iostream>
#include <memory>
using namespace std;

class Node {
public:
    shared_ptr<Node> next;
    weak_ptr<Node> prev;          // 用 weak_ptr 避免循环引用
    ~Node() { cout << "Node 析构" << endl; }
};

int main() {
    // weak_ptr:不增加引用计数,是 shared_ptr 的"观察者"
    auto a = make_shared<Node>();
    auto b = make_shared<Node>();
    a->next = b;                  // b 的引用计数 = 2
    b->prev = a;                  // a 的引用计数 = 2(weak 不增加)
    // a 离开作用域:引用计数 2->1(因为 prev 是 weak,不影响)
    // b 离开作用域:引用计数 2->1
    // 但 a->next 还指向 b,所以 b 还活着
    // 等 a 销毁,a->next 释放,b 才销毁

    // weak_ptr 使用前要 lock() 提升为 shared_ptr
    weak_ptr<Node> w = a;
    if (auto sp = w.lock()) {     // 提升成功,对象还活着
        cout << "对象还在" << endl;
    }

    a->next = nullptr;            // 显式断开,a 销毁后 b 也会销毁
    return 0;
}

// 智能指针选型:
// - 独占资源:用 unique_ptr(默认选择)
// - 共享资源:用 shared_ptr
// - 观察者:用 weak_ptr(不影响生命周期)

weak_ptr 的特点:

八、智能指针选型

九、现代 C++ 实践

现代 C++(C++11+)的内存管理原则:

// 现代 C++ 内存管理原则:

// 1. 优先用栈对象,不用堆对象
class Good {
    std::string name;       // 栈上的 string,自动管理
    std::vector<int> data;  // 栈上的 vector,自动管理
};

// 2. 必须用堆时,用智能指针
#include <memory>
auto p = std::make_unique<Good>();

// 3. 几乎不用裸 new/delete
// 反面教材:
// int* p = new int(42);
// ... 复杂逻辑 ...
// forget to delete -> 内存泄漏

// 4. STL 容器自带内存管理
// vector/map/string 自己管内存,你只管用

// 5. 函数参数:
// - 只读:const T&  或  T(小类型)
// - 可修改:T&
// - 转移所有权:unique_ptr<T>/shared_ptr<T>
// - 借用但不拥有:T*(原始指针,但调用者仍负责生命周期)

总结口诀:

小结

这一章你掌握了 C++ 内存管理:栈与堆的区别、new/delete 与各种坑、RAII 设计哲学、智能指针三件套(unique_ptr/shared_ptr/weak_ptr)、现代 C++ 几乎不用裸 new/delete 的实践。下一篇我们看异常处理——怎么处理程序运行时的错误。

← 上一篇 C++ STL

下一篇 C++ 异常

✈️💬