6、C++11 核心特性:智能指针(unique_ptr, shared_ptr, weak_ptr),内存管理的现代方案

说到内存管理,C++程序员大概都有过半夜调试野指针、内存泄漏的经历。我入行那会儿,每次 new 一个对象,都得在脑子里过一遍「这玩意儿到底该在哪儿 delete」。后来 C++11 带来了智能指针,说实话,这真是解放生产力的好东西。今天咱们就聊聊这三个家伙:unique_ptr、shared_ptr、weak_ptr。

为什么需要智能指针?

先想想看,传统裸指针有什么问题?

  • 忘记 delete,内存泄漏
  • 多次 delete,程序崩溃
  • 野指针,悬空引用
  • 异常安全,代码复杂

说白了,就是「谁负责释放」这个责任不清晰。智能指针的核心思想很简单:让对象自己管理自己的生命周期。当指针对象被销毁时,它指向的资源也自动释放。这就是 RAII(资源获取即初始化)的精髓。

核心原则:智能指针是栈上的对象,它包装了堆上的资源。栈对象离开作用域自动析构,析构函数里释放堆资源。就这么简单。

unique_ptr:独占所有权

unique_ptr 是我用得最多的智能指针。它的语义非常明确:一个资源只能被一个 unique_ptr 拥有。不能拷贝,只能移动。

我在项目中遇到过这样一个场景:一个工厂函数创建对象,返回给调用方。用裸指针的话,调用方得记得 delete。用 unique_ptr 就清爽多了:

#include <memory>

class Widget {
public:
    void doSomething() { /* ... */ }
};

// 工厂函数,返回 unique_ptr
std::unique_ptr<Widget> createWidget() {
    return std::make_unique<Widget>();
}

int main() {
    auto w = createWidget();  // 所有权转移给 w
    w->doSomething();
    // w 离开作用域,自动释放 Widget 对象
    return 0;
}

注意这里用了 std::make_unique(C++14 引入,C++11 可以用 std::unique_ptr<Widget>(new Widget()))。我个人习惯总是用 make_xxx 系列函数,原因有二:一是异常安全,二是少打几个字。

小技巧:unique_ptr 可以自定义删除器。比如你需要用 fclose 而不是 delete 来释放资源:

auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> filePtr(fopen("test.txt", "r"), fileDeleter);

shared_ptr:共享所有权

shared_ptr 就更有意思了。它使用引用计数来跟踪有多少个 shared_ptr 指向同一个对象。当最后一个 shared_ptr 被销毁时,对象才被释放。

你想想看,这就像一把钥匙多人共用,最后一个人走的时候锁门。听起来很美好,但引用计数也有代价:

  • 额外的内存开销(控制块)
  • 引用计数的原子操作(线程安全,但慢一点)
  • 循环引用问题(后面讲 weak_ptr 时细说)
#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource released\n"; }
};

int main() {
    std::shared_ptr<Resource> sp1 = std::make_shared<Resource>();
    {
        std::shared_ptr<Resource> sp2 = sp1;  // 引用计数变为 2
        std::cout << "Inside block, use count: " << sp1.use_count() << "\n";
    }  // sp2 销毁,引用计数变为 1
    std::cout << "Outside block, use count: " << sp1.use_count() << "\n";
    // sp1 销毁,引用计数变为 0,Resource 被释放
    return 0;
}

输出:

Resource acquired
Inside block, use count: 2
Outside block, use count: 1
Resource released

我曾经踩过的坑:千万别用裸指针创建多个 shared_ptr!比如:

Resource* raw = new Resource();
std::shared_ptr<Resource> sp1(raw);
std::shared_ptr<Resource> sp2(raw);  // 错误!两个独立控制块,double free!

正确做法:始终用 std::make_shared 或者从另一个 shared_ptr 拷贝构造。

weak_ptr:打破循环引用

weak_ptr 是 shared_ptr 的「小跟班」。它指向一个由 shared_ptr 管理的对象,但不增加引用计数。说白了,它就是个观察者,想知道对象还活着没。

为什么要这个?来看一个经典问题——循环引用:

struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->next = a;  // 循环引用!两个对象都不会被释放
    return 0;  // 没有输出 "Node destroyed"
}

看到了吗?a 和 b 互相持有对方的 shared_ptr,引用计数永远到不了 0。这就是内存泄漏!

解决方案:把其中一个改成 weak_ptr。

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // 用 weak_ptr 打破循环
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->prev = a;  // weak_ptr,不增加引用计数
    return 0;  // 正确释放两个 Node
}

使用 weak_ptr 时,需要先通过 lock() 获取一个临时的 shared_ptr,然后才能访问对象:

std::weak_ptr<Node> wp = b->prev;
if (auto sp = wp.lock()) {  // 如果对象还活着
    sp->next = nullptr;     // 安全访问
} else {
    // 对象已经被释放了
}

如何选择?一张图说清楚

下面这张图是我自己总结的决策流程,帮你快速判断该用哪种智能指针:

智能指针选择决策树 需要管理堆对象? 是 → 继续 所有权是独占的还是共享的? 独占 unique_ptr 独占所有权,轻量高效 共享 shared_ptr 共享所有权,引用计数 有循环引用? weak_ptr 打破循环,观察者模式 总结:优先用 unique_ptr,需要共享用 shared_ptr, 需要打破循环或观察时用 weak_ptr

性能与最佳实践

聊完了用法,咱们说说实际项目中怎么用。我总结了几条经验:

场景 推荐做法 原因
局部对象 unique_ptr 零开销抽象,没有引用计数
容器中存储 unique_ptr 或 shared_ptr unique_ptr 可移动,shared_ptr 可拷贝
多线程共享 shared_ptr 引用计数原子操作,线程安全
缓存/观察者 weak_ptr 不延长生命周期,避免循环引用
函数参数 传递裸指针或引用 函数不拥有所有权,避免不必要的拷贝

我个人习惯:

  • 能用 unique_ptr 绝不用 shared_ptr。引用计数有开销,而且容易写出循环引用的 bug。
  • 创建对象时始终用 std::make_uniquestd::make_shared。别用 new 裸指针去构造智能指针。
  • 函数参数如果只是「借用」对象,传裸指针或引用。函数签名就告诉调用方「我不负责释放」。
  • 返回对象时,返回 unique_ptr。调用方可以决定是独占还是转成 shared_ptr。

避坑指南

最后分享几个我踩过的坑:

  • 不要用 auto_ptr——C++11 已经废弃了,C++17 彻底移除。它拷贝时会转移所有权,行为诡异。
  • shared_ptr 的循环引用——前面讲过了,用 weak_ptr 打破。如果发现对象析构函数没调用,先检查是不是循环引用了。
  • 自定义删除器——unique_ptr 的删除器是类型的一部分,shared_ptr 不是。这意味着两个不同删除器的 unique_ptr 不能放在同一个容器里。
  • enable_shared_from_this——如果你需要在类内部获取指向自己的 shared_ptr,记得继承 std::enable_shared_from_this<T>,然后用 shared_from_this()。千万别从裸指针构造!

嗯,智能指针的内容差不多就这些了。说白了,C++11 引入的这三个家伙,让内存管理从「手工作坊」升级到了「工业化生产」。你只要记住:unique_ptr 是默认选择,shared_ptr 是共享方案,weak_ptr 是观察者。用好它们,你的代码会变得更安全、更清晰。


公众号:蓝海资料掘金营,微信deep3321