第9章:动态内存管理——new和delete运算符、智能指针与RAII原则

说实话,动态内存管理是C++里最容易出坑的地方之一。我刚开始写C++那会儿,就因为在堆上new了一个对象忘了delete,结果程序跑着跑着内存就爆了。后来排查了半天,才发现是内存泄漏。嗯,从那以后,我对动态内存就格外小心了。

这一章,我们就来聊聊C++里的动态内存管理。说白了,就是怎么在程序运行时按需分配和释放内存。我会结合自己的经验,把new/delete、智能指针、RAII这些概念讲清楚。

9.1 new和delete运算符

C++里,new用来在堆上分配内存,delete用来释放。这俩是配套的,就像钥匙和锁一样。

// 分配单个对象
int* p = new int(42);
delete p;

// 分配数组
int* arr = new int[10];
delete[] arr;

这里有个细节:newdelete配对使用,new[]delete[]配对使用。混用的话,行为是未定义的。我见过有人用new[]分配数组,却用delete释放,结果程序崩溃了。这种错误很难排查,因为不是每次都会崩溃,可能只是内存损坏,等到某个角落才爆发。

注意:new/delete是底层操作,容易出错。现代C++推荐用智能指针替代。

9.2 智能指针:unique_ptr、shared_ptr、weak_ptr

智能指针是C++11引入的,目的就是解决手动管理内存的痛点。你想想看,每次都要记得delete,多累啊。智能指针会自动帮你释放,省心多了。

9.2.1 unique_ptr——独占所有权

unique_ptr是独占的,同一时间只能有一个unique_ptr指向某个对象。它不能拷贝,只能移动。

#include <memory>

std::unique_ptr<int> p1(new int(10));
// std::unique_ptr<int> p2 = p1; // 错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1); // 可以移动

// 推荐用make_unique(C++14)
auto p3 = std::make_unique<int>(20);

我个人习惯用make_unique,因为它更安全,而且代码更简洁。为什么更安全?因为make_unique避免了潜在的异常安全问题。举个例子:

// 不推荐
foo(std::unique_ptr<T>(new T), other_function());

// 推荐
foo(std::make_unique<T>(), other_function());

第一种写法,如果other_function()抛出异常,new T分配的内存可能泄漏。而make_unique把分配和构造封装在一起,异常安全。

9.2.2 shared_ptr——共享所有权

shared_ptr允许多个指针共享同一个对象。它内部维护一个引用计数,当最后一个shared_ptr被销毁时,才释放对象。

auto sp1 = std::make_shared<int>(100);
auto sp2 = sp1; // 引用计数+1

std::cout << sp1.use_count() << std::endl; // 输出2

我在项目中遇到过一个问题:两个shared_ptr互相引用,导致内存泄漏。这就是所谓的循环引用。比如:

struct Node {
    std::shared_ptr<Node> next;
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 循环引用!

这种情况下,a和b的引用计数永远不会降到0,内存就泄漏了。怎么解决?用weak_ptr

9.2.3 weak_ptr——弱引用

weak_ptr是一种不控制对象生命周期的智能指针。它指向一个由shared_ptr管理的对象,但不增加引用计数。要访问对象时,需要先通过lock()获取一个shared_ptr

struct Node {
    std::weak_ptr<Node> next; // 用weak_ptr打破循环
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 没问题了

// 访问时
if (auto sp = a->next.lock()) {
    // 对象还存在
} else {
    // 对象已被释放
}

说白了,weak_ptr就是个观察者,它不拥有对象,只是偶尔看看对象还在不在。

9.3 内存泄漏的检测与预防

内存泄漏是C++程序的老大难问题。我曾经在一个服务器项目里,因为一个循环引用导致内存泄漏,程序跑了三天后内存占用飙到十几个G。排查起来特别痛苦。

怎么检测?有几个常用工具:

  • Valgrind:Linux下的内存检测利器,能检测泄漏、越界等。
  • AddressSanitizer:编译时加-fsanitize=address,运行时就能检测。
  • Visual Studio的CRT调试:Windows下用_CrtDumpMemoryLeaks()

预防的话,记住几条原则:

  1. 尽量用智能指针,别手动new/delete。
  2. 避免循环引用,用weak_ptr打破。
  3. RAII原则——资源获取即初始化。

9.4 RAII原则

RAII是C++里一个非常重要的思想。它的核心是:把资源的生命周期绑定到对象的生命周期上。资源在构造函数中获取,在析构函数中释放。

你想想看,这样有什么好处?不管代码怎么跑,只要对象销毁了,资源就自动释放。异常安全、代码简洁。

class File {
public:
    File(const char* filename) : fp_(fopen(filename, "r")) {
        if (!fp_) throw std::runtime_error("open failed");
    }
    ~File() {
        if (fp_) fclose(fp_);
    }
    // ... 其他操作
private:
    FILE* fp_;
};

// 使用
{
    File f("test.txt");
    // 自动管理,不用手动close
} // 离开作用域,f销毁,文件自动关闭

智能指针本身就是RAII的典型应用。它们管理的是动态内存,但思想是一样的。

我的建议:写C++代码时,养成RAII的思维习惯。任何需要手动释放的资源——内存、文件、锁、socket——都应该用RAII封装。

9.5 本章知识体系

下面这张图,把本章的核心逻辑串起来了:

动态内存管理知识体系 动态内存管理 手动管理 new / delete new[] / delete[] 智能指针 unique_ptr(独占所有权) shared_ptr(共享所有权) weak_ptr(弱引用,打破循环) RAII原则 资源获取即初始化 析构函数自动释放 核心:优先使用智能指针 + RAII,避免手动new/delete

嗯,这张图把本章的内容串起来了。手动管理容易出错,智能指针和RAII是现代C++的推荐做法。记住一点:能用智能指针就别手动new/delete,能用RAII就别裸资源。

核心要点:

  • new/delete是底层操作,容易泄漏,尽量少用。
  • unique_ptr独占所有权,shared_ptr共享所有权,weak_ptr解决循环引用。
  • RAII是C++资源管理的核心思想,把资源绑定到对象生命周期上。
  • 内存泄漏检测工具:Valgrind、AddressSanitizer、CRT调试。

好了,这一章就到这里。动态内存管理是C++里绕不开的话题,但掌握了智能指针和RAII,你就能写出更安全、更健壮的代码。下一章我们聊聊……嗯,先卖个关子,到时候你就知道了。


公众号:蓝海资料掘金营,微信deep3321