裸指针 + new/delete 是 C++ 内存泄漏和悬垂指针的头号来源:C++11 引入的智能指针用 RAII 思想把"释放内存"绑定到对象生命周期上,让资源管理自动发生。现代 C++ 里,智能指针几乎是动态内存的唯一正解。

1. 裸指针的三个痛点

智能指针把 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,对比结果。这个实验能让你彻底记住循环引用。