刚学 C++ 的时候,老师反复念叨:"记得 new 完之后 delete。""delete 之前先判 nullptr。""数组用 delete[]。"——听起来像在背乘法表。
直到工作第一年,我写了一个模块,某个分支提前 return,资源没释放。线上服务跑了一周,内存悄悄涨到 32G,被运维同事深夜叫起来处理。从那天起,我才真正明白: 内存安全问题不是知识,是纪律。
幸运的是,C++11 之后,标准库给了我们一个优雅的答案——智能指针。这一篇就聊聊三剑客: unique_ptr、shared_ptr、weak_ptr。
一、为什么我们需要智能指针
先看一段"朴素"的代码,体会一下裸指针有多容易失控:
void process() { Resource* r = new Resource(); if (someCondition()) { return; // 漏了 delete,内存泄漏 } r->doWork(); delete r; }
一个提前 return,就可能让对象永远驻留在堆上。异常来了?更糟,析构函数都跑不到。
核心思想: 让"指针"这个对象本身,负责它所指向资源的生命周期。指针析构时,资源就自动释放。这就是 RAII(Resource Acquisition Is Initialization)。
二、unique_ptr —— 独占所有权的"独行侠"
unique_ptr 表达"这块资源,只能有一个人拥有"。它不能被拷贝,只能通过 std::move 转移所有权。
// 一个简单的资源类 class Resource { public: Resource() { std::cout << "acquired\n"; } ~Resource() { std::cout << "released\n"; } void work() { std::cout << "working...\n"; } }; void demo() { std::unique_ptr<Resource> p = std::make_unique<Resource>(); p->work(); // 函数结束,自动 delete,无需手动释放 }
注意 std::make_unique(C++14 起),它能避免手动 new 时的内存泄漏——比如 new 完之后,传参时抛异常,资源就漏了。
转移所有权
auto p1 = std::make_unique<Resource>(); auto p2 = std::move(p1); // 所有权从 p1 转移给 p2 // p1 此时为 nullptr if (!p1) std::cout << "p1 is empty\n";
使用建议: 能用 unique_ptr 就别用 shared_ptr。独占所有权语义最清晰,运行时开销几乎为零(一个裸指针的大小),出错的概率也最小。
三、shared_ptr —— 共享所有权的"共同体"
当一块资源确实需要被多个对象共享时,shared_ptr 通过引用计数来管理生命周期。每多一个 shared_ptr 指向同一资源,计数 +1;析构一个,计数 -1;归零时,资源被释放。
auto p1 = std::make_shared<Resource>(); { auto p2 = p1; // 引用计数: 1 -> 2 // 块结束, p2 析构,引用计数: 2 -> 1 } // 资源仍然活着,因为 p1 还在 p1->work(); // 函数结束,p1 析构,引用计数: 1 -> 0,资源释放
shared_ptr 内部其实由两个指针组成:一个指向资源,一个指向"控制块"(保存引用计数)。这意味着它的大小是裸指针的两倍。拷贝和赋值的代价也比 unique_ptr 大不少。
循环引用:shared_ptr 的阿喀琉斯之踵
当你写下这样的代码,内存就再也不会被释放了:
struct Node { std::shared_ptr<Node> next; std::shared_ptr<Node> prev; ~Node() { std::cout << "~Node\n"; } }; void leakDemo() { auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; // b 的引用计数: 1 -> 2 b->prev = a; // a 的引用计数: 1 -> 2 // 函数结束, a、b 局部变量析构 // 引用计数都减为 1,但谁也释放不了谁 }
这个就是经典的"循环引用"。怎么解?轮到 weak_ptr 出场了。
四、weak_ptr —— 不计数的"旁观者"
weak_ptr 是 shared_ptr 的"弱引用",它指向一个由 shared_ptr 管理的资源,但不增加引用计数。它存在的意义就是打破循环引用。
struct Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // 关键:弱引用 ~Node() { std::cout << "~Node\n"; } }; void fixDemo() { auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; b->prev = a; // a 的引用计数仍然是 1 } // a 出作用域,引用计数 -> 0,析构 // 析构 a 时,a->next (指向 b) 析构,b 的引用计数 -> 0,析构 // 完美闭环,无泄漏
用 weak_ptr 访问资源前,需要 lock() 一下,得到一个 shared_ptr——如果资源已经没了,会得到 nullptr。
if (auto sp = b.prev.lock()) { sp->someMethod(); } else { std::cout << "资源已经没了"; }
五、什么时候该用谁?
| 场景 | 推荐 | 理由 |
|---|---|---|
| 单一拥有者(99% 情况) | unique_ptr | 零开销、语义清晰、不可拷贝更安全 |
| 多个对象共享同一资源 | shared_ptr | 用引用计数,小心循环引用 |
| 需要"观察"但不"拥有" | weak_ptr | 缓存、打破循环、对象池 |
| 作为函数参数(只读) | T* 或 const T& | 智能指针表达所有权,别用错 |
六、写在最后
智能指针看似只是"包装",其实它把"资源管理"这件事,从程序员记性变成了编译器纪律。这不是偷懒,这是让机器去做它擅长的事。
写代码久了,越来越觉得:好的代码不是"我都能控制",而是"我让该负责的人负责该负责的事"。unique_ptr 负责独占,shared_ptr 负责共享,weak_ptr 负责旁观。各自有边界,各自有尊严。
愿你也写出让人放心的 C++。
One-liner takeaway
默认用 unique_ptr;需要共享时再上 shared_ptr;打破循环或观察时,用 weak_ptr。简单、清晰、不出错。