10、指针(下):动态内存分配(new/delete)、智能指针(unique_ptr、shared_ptr、weak_ptr)、内存泄漏与避免
好,咱们接着聊指针。上一章我们把指针的基础和数组、函数的关系捋了一遍。这一章,咱们要碰一碰 C++ 里最刺激、也最容易翻车的地方——动态内存管理。
说白了,就是程序运行时,跟操作系统“要”一块内存来用。用完了,得还回去。这个“要”和“还”的过程,在 C++ 里就是 new 和 delete。但现实很残酷——很多人只记得“要”,不记得“还”。于是,内存泄漏就来了。
我个人习惯是,能不用裸指针就不用。为什么?因为 C++ 给我们准备了更安全的工具——智能指针。这玩意儿,说白了就是 RAII(资源获取即初始化)思想的具体体现。你只管用,它自动帮你释放。
嗯,咱们一步步来。
10.1 动态内存分配:new 和 delete
先看最原始的方式。C 语言里用 malloc 和 free,C++ 里用 new 和 delete。区别在哪?new 不光分配内存,还会调用构造函数。delete 不光释放内存,还会调用析构函数。这是 C++ 面向对象的基础。
int* p = new int(42); // 分配一个 int,初始化为 42
delete p; // 释放
int* arr = new int[10]; // 分配数组
delete[] arr; // 释放数组,注意 [] 不能少
这里有个坑,我刚开始学的时候踩过——new 和 delete 必须配对使用。你用 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_ptr 的 lock() 是线程安全的。多线程环境下,你可以安全地检查对象是否还活着。
10.5 内存泄漏与避免
内存泄漏,说白了就是“申请了内存,但没释放”。程序跑得越久,泄漏越多,最终耗尽系统内存。
常见的泄漏场景:
new之后忘了delete- 异常导致
delete没执行 - 循环引用(
shared_ptr互指) - 容器中存储了裸指针,容器销毁时没释放
怎么避免?我总结了几个原则:
- 优先使用智能指针。能用
unique_ptr就别用裸指针,能用shared_ptr就别自己管理引用计数。 - 避免循环引用。如果发现两个对象互相引用,考虑把其中一个改成
weak_ptr。 - RAII 思想。资源在构造函数中获取,在析构函数中释放。智能指针就是 RAII 的典型代表。
- 使用内存检测工具。比如 Valgrind、AddressSanitizer。我每次提交代码前都会跑一遍,能发现很多隐藏问题。
shared_ptr 管理一个全局缓存,结果因为循环引用,缓存永远释放不了。后来改成 weak_ptr 才解决。记住:shared_ptr 不是万能的,用之前想清楚所有权模型。
10.6 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一张“内存管理地图”。
10.7 总结
这一章的内容,说白了就是三件事:
- 手动管理:
new/delete,能不用就不用 - 智能指针:
unique_ptr独占,shared_ptr共享,weak_ptr破循环 - 内存泄漏:用 RAII 思想 + 智能指针 + 检测工具,基本能杜绝
我个人觉得,掌握智能指针是区分“C 风格程序员”和“现代 C++ 程序员”的分水岭。你想想看,一个 unique_ptr 就能解决 80% 的内存管理问题,何乐而不为呢?
嗯,指针的内容到这里就告一段落了。下一章咱们会进入 C++ 的另一个核心领域——面向对象编程。到时候再聊。
公众号:蓝海资料掘金营,微信deep3321