← 返回首页

智能指针:从裸指针到现代 C++ 的优雅

刚学 C++ 的时候,老师反复念叨:"记得 new 完之后 delete。""delete 之前先判 nullptr。""数组用 delete[]。"——听起来像在背乘法表。

直到工作第一年,我写了一个模块,某个分支提前 return,资源没释放。线上服务跑了一周,内存悄悄涨到 32G,被运维同事深夜叫起来处理。从那天起,我才真正明白: 内存安全问题不是知识,是纪律。

幸运的是,C++11 之后,标准库给了我们一个优雅的答案——智能指针。这一篇就聊聊三剑客: unique_ptrshared_ptrweak_ptr

一、为什么我们需要智能指针

先看一段"朴素"的代码,体会一下裸指针有多容易失控:

example_naive.cpp C++
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 转移所有权。

example_unique.cpp C++
// 一个简单的资源类
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;归零时,资源被释放。

example_shared.cpp C++
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 管理的资源,但不增加引用计数。它存在的意义就是打破循环引用。

example_weak.cpp C++
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。简单、清晰、不出错。