裸指针 + new/delete 是 C++ 内存泄漏和悬垂指针的头号来源:C++11 引入的智能指针用 RAII 思想把"释放内存"绑定到对象生命周期上,让资源管理自动发生。现代 C++ 里,智能指针几乎是动态内存的唯一正解。
1. 裸指针的三个痛点
- 忘了 delete:路径一多就容易漏,内存泄漏。
- 重复 delete:两个指针指向同一块内存,double free 崩溃。
- 悬垂指针:delete 之后还去解引用,未定义行为。
智能指针把 delete 放进析构函数,对象销毁时自动释放,三个问题一次解决。
2. unique_ptr:独占所有权
unique_ptr 独占对象,不允许拷贝,只能移动(转移所有权),零额外开销:
#include <memory>
using namespace std;
unique_ptr<int> p = make_unique<int>(42); // C++14 推荐写法
cout << *p; // 像指针一样用
// unique_ptr<int> q = p; // 编译错误:禁止拷贝
unique_ptr<int> q = move(p); // 转移所有权,p 变空
if (!p) { /* p 现在是 nullptr */ }
// 放进容器
vector<unique_ptr<Widget>> widgets;
widgets.push_back(make_unique<Widget>());
3. shared_ptr:共享所有权
shared_ptr 用引用计数跟踪有多少个指针共享对象,计数归零才真正释放:
shared_ptr<int> a = make_shared<int>(10);
{
shared_ptr<int> b = a; // 引用计数 2
*b = 99;
} // b 销毁,计数回到 1
// a 销毁时计数归零,内存被释放
long n = a.use_count(); // 当前共享者数量
int* raw = a.get(); // 拿裸指针(仅观察,别 delete)
注意两个代价:引用计数是原子操作(略慢),且对象和计数块各分配一次内存。因此能用 unique_ptr 就别用 shared_ptr。
4. weak_ptr:打破循环引用
两个对象互相 shared_ptr 持有对方,计数永远不为零,就泄漏了。用 weak_ptr 做"弱引用",不增加计数:
class Node {
shared_ptr<Node> next;
weak_ptr<Node> prev; // 弱引用,防止循环
};
weak_ptr<int> w = a; // 从 shared_ptr 构造
if (auto sp = w.lock()) { // lock() 提升为 shared_ptr
// 对象还活着,安全使用 sp
} else {
// 对象已被释放
}
5. 正确用法速查
| 场景 | 推荐 | 原因 |
|---|---|---|
| 工厂函数返回新对象 | unique_ptr | 所有权明确,开销最小 |
| 容器存多态对象 | unique_ptr | 自动释放,禁止意外拷贝 |
| 多处共享同一资源 | shared_ptr | 引用计数管理生命周期 |
| 缓存/观察,不想延长生命周期 | weak_ptr | 不干扰销毁时机 |
| 纯观察、不拥有 | 裸指针/引用 | 不参与所有权 |
6. 现代 C++ 的资源管理铁律
// 用 make 系列,别直接 new
auto p = make_unique<Widget>(args);
// 接口里用裸指针表示"借用",不转移所有权
void draw(Widget* w); // 只借用
// 返回新对象时返回 unique_ptr,调用方决定如何持有
unique_ptr<Widget> loadWidget();
💡 练习:用 shared_ptr 实现一个链表,故意让 next 和 prev 都是 shared_ptr,观察析构时是否泄漏(打印析构次数);再把 prev 换成 weak_ptr,对比结果。这个实验能让你彻底记住循环引用。