10、指针(下):动态内存分配(new/delete)、智能指针(unique_ptr、shared_ptr、weak_ptr)、内存泄漏与避免

好,咱们接着聊指针。上一章我们把指针的基础和数组、函数的关系捋了一遍。这一章,咱们要碰一碰 C++ 里最刺激、也最容易翻车的地方——动态内存管理

说白了,就是程序运行时,跟操作系统“要”一块内存来用。用完了,得还回去。这个“要”和“还”的过程,在 C++ 里就是 newdelete。但现实很残酷——很多人只记得“要”,不记得“还”。于是,内存泄漏就来了。

我个人习惯是,能不用裸指针就不用。为什么?因为 C++ 给我们准备了更安全的工具——智能指针。这玩意儿,说白了就是 RAII(资源获取即初始化)思想的具体体现。你只管用,它自动帮你释放。

嗯,咱们一步步来。

10.1 动态内存分配:new 和 delete

先看最原始的方式。C 语言里用 mallocfree,C++ 里用 newdelete。区别在哪?new 不光分配内存,还会调用构造函数。delete 不光释放内存,还会调用析构函数。这是 C++ 面向对象的基础。

int* p = new int(42);      // 分配一个 int,初始化为 42
delete p;                   // 释放

int* arr = new int[10];     // 分配数组
delete[] arr;               // 释放数组,注意 [] 不能少

这里有个坑,我刚开始学的时候踩过——newdelete 必须配对使用。你用 new[] 分配的,必须用 delete[] 释放。混着用,行为是未定义的。轻则内存泄漏,重则程序崩溃。

警告: 永远不要用 delete 释放 new[] 分配的内存,反之亦然。这是 C++ 的硬性规定。

但是,光靠 new/delete 手动管理,太容易出错了。你想想看,一个函数里写了三个 new,中间某个逻辑提前 return 了,后面的 delete 就没执行到。内存泄漏就这么产生了。

所以,C++11 引入了智能指针。这才是现代 C++ 的标配。

10.2 智能指针:unique_ptr

unique_ptr,顾名思义,是“独占所有权”的智能指针。同一时刻,只能有一个 unique_ptr 指向某块内存。它不能被拷贝,只能被移动。

为什么?因为如果允许拷贝,两个 unique_ptr 指向同一块内存,析构时就会 double free。所以设计上直接禁止拷贝,只允许移动语义。

#include <memory>

std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // 编译错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1);  // 可以移动,p1 变为空

我在项目中遇到过一种场景:工厂函数返回一个 unique_ptr,调用方拿到后直接使用。所有权转移清晰,谁创建谁负责,谁持有谁使用。代码逻辑一目了然。

建议: 尽量使用 std::make_unique 而不是直接 new。它更安全,也能避免异常安全问题。

10.3 智能指针:shared_ptr

shared_ptr 就灵活多了。它允许多个指针共享同一块内存。内部维护一个引用计数,每多一个 shared_ptr 指向同一对象,计数加 1;每销毁一个,计数减 1。当计数降到 0 时,自动释放内存。

std::shared_ptr<int> sp1 = std::make_shared<int>(100);
std::shared_ptr<int> sp2 = sp1;  // 引用计数变为 2

// 当 sp1 和 sp2 都离开作用域时,内存自动释放

听起来很完美,对吧?但别高兴太早。shared_ptr 有个大坑——循环引用

举个例子:A 对象持有一个 shared_ptr<B>,B 对象持有一个 shared_ptr<A>。两者互相引用,引用计数永远降不到 0,内存永远释放不了。这就是循环引用导致的内存泄漏。

我曾经在一个图形引擎项目里遇到过这个问题。两个节点互相引用,程序跑着跑着内存就爆了。查了半天才发现是循环引用。

核心要点: shared_ptr 适合“共享所有权”的场景,但要小心循环引用。解决方案是引入 weak_ptr

10.4 智能指针:weak_ptr

weak_ptr 是一种“弱引用”智能指针。它指向 shared_ptr 管理的对象,但不增加引用计数。它不能直接访问对象,必须通过 lock() 方法提升为 shared_ptr 后才能使用。

说白了,weak_ptr 就是用来打破循环引用的。A 持有 shared_ptr<B>,B 持有 weak_ptr<A>。这样 B 不会增加 A 的引用计数,A 释放后,B 的 weak_ptr 自动失效。

std::shared_ptr<int> sp = std::make_shared<int>(200);
std::weak_ptr<int> wp = sp;  // 不增加引用计数

// 使用前需要 lock()
if (auto locked = wp.lock()) {
    std::cout << *locked << std::endl;
} else {
    std::cout << "对象已被释放" << std::endl;
}

嗯,这里要注意:weak_ptrlock() 是线程安全的。多线程环境下,你可以安全地检查对象是否还活着。

10.5 内存泄漏与避免

内存泄漏,说白了就是“申请了内存,但没释放”。程序跑得越久,泄漏越多,最终耗尽系统内存。

常见的泄漏场景:

  • new 之后忘了 delete
  • 异常导致 delete 没执行
  • 循环引用(shared_ptr 互指)
  • 容器中存储了裸指针,容器销毁时没释放

怎么避免?我总结了几个原则:

  1. 优先使用智能指针。能用 unique_ptr 就别用裸指针,能用 shared_ptr 就别自己管理引用计数。
  2. 避免循环引用。如果发现两个对象互相引用,考虑把其中一个改成 weak_ptr
  3. RAII 思想。资源在构造函数中获取,在析构函数中释放。智能指针就是 RAII 的典型代表。
  4. 使用内存检测工具。比如 Valgrind、AddressSanitizer。我每次提交代码前都会跑一遍,能发现很多隐藏问题。
避坑指南: 我曾经在项目里用 shared_ptr 管理一个全局缓存,结果因为循环引用,缓存永远释放不了。后来改成 weak_ptr 才解决。记住:shared_ptr 不是万能的,用之前想清楚所有权模型。

10.6 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一张“内存管理地图”。

C++ 动态内存管理知识体系 动态内存管理 手动管理 (new/delete) 智能指针 (RAII) 内存泄漏与避免 new / delete new[] / delete[] unique_ptr shared_ptr weak_ptr 循环引用 忘记 delete 异常安全 核心原则:优先智能指针,避免裸指针,警惕循环引用

10.7 总结

这一章的内容,说白了就是三件事:

  • 手动管理new/delete,能不用就不用
  • 智能指针unique_ptr 独占,shared_ptr 共享,weak_ptr 破循环
  • 内存泄漏:用 RAII 思想 + 智能指针 + 检测工具,基本能杜绝

我个人觉得,掌握智能指针是区分“C 风格程序员”和“现代 C++ 程序员”的分水岭。你想想看,一个 unique_ptr 就能解决 80% 的内存管理问题,何乐而不为呢?

嗯,指针的内容到这里就告一段落了。下一章咱们会进入 C++ 的另一个核心领域——面向对象编程。到时候再聊。


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