C++ 中的智能指针:unique_ptr、shared_ptr 与 weak_ptr 的最佳实践

在 C++ 现代化的进程中,智能指针成为了资源管理的核心工具。它们通过 RAII(资源获取即初始化)机制,帮助程序员避免手动 delete 带来的内存泄漏、悬空指针等错误。本文将对三种最常用的智能指针——std::unique_ptrstd::shared_ptrstd::weak_ptr 进行深入剖析,并给出实际项目中的最佳实践建议。

1. std::unique_ptr:单一所有权,最快速的指针

1.1 基本语义

unique_ptr 表示独占所有权:同一时间只能有一个 unique_ptr 拥有同一块资源。它使用 RAII 自动在离开作用域时 delete 所管理的对象。

1.2 关键特性

  • 无复制:拷贝构造/赋值被删除,只能移动。
  • 自定义删除器std::unique_ptr<T, Deleter> 可以传递自定义删除器,以支持非 delete 的资源(例如 malloc/free、文件句柄)。
  • 轻量级:在大多数实现中只包含一个裸指针,几乎不增加额外开销。

1.3 使用场景

  • 仅在单线程或明确所有权转移的场景下使用。
  • 作为函数返回值,将资源交给调用者。
  • 在容器中存放独占所有权对象(如 std::vector<std::unique_ptr<T>>)。

1.4 常见陷阱

  • 不当移动:误将 unique_ptr 复制给同一变量,导致野指针。
  • 循环引用:与 shared_ptr 配合使用时,需要确保不会形成闭环。

2. std::shared_ptr:共享所有权,引用计数

shared_ptr 引入了引用计数机制,允许多个指针实例共享同一资源。资源在最后一个指针销毁时才真正释放。

2.1 关键细节

  • 引用计数:实现上往往使用两块内存:控制块(计数等)和资源块。
  • 线程安全:对引用计数的增减是原子操作,但对资源的操作并不安全。
  • 内存泄漏:如果两个 shared_ptr 对象持有互相的指针,可能导致循环引用,导致资源永远不会释放。

2.2 使用技巧

  • 使用 std::make_shared:一次性分配控制块和对象,效率更高。
  • 避免隐式转换:显式使用 `std::shared_ptr ` 而不是裸指针。
  • 弱引用:当需要观察对象而不拥有时,使用 weak_ptr

3. std::weak_ptr:弱引用,避免循环引用

weak_ptr 并不拥有资源,只是对 shared_ptr 的非拥有引用。它可以查看资源是否仍存活。

3.1 典型用法

class Node {
public:
    std::vector<std::shared_ptr<Node>> children;
    std::weak_ptr <Node> parent;
};

父节点保持对子节点的强引用,子节点通过弱引用指向父节点,避免循环引用。

3.2 lock() 的意义

weak_ptr::lock() 返回一个临时 shared_ptr,如果资源已被销毁,则返回空指针。

4. 实践建议

场景 推荐智能指针 说明
单一所有权 std::unique_ptr 最轻量、最安全
共享所有权 std::shared_ptr 需要多方共享
观察对象 std::weak_ptr 只需检查是否存在
资源池 std::shared_ptr + weak_ptr 对象生命周期不确定

4.1 避免悬空指针

  • 对每个 shared_ptr,使用 weak_ptr 观察或在容器中使用 unique_ptr
  • 通过 lock() 保护访问。

4.2 减少堆分配

  • 通过 make_shared 一次性分配。
  • 对于临时对象,考虑使用 unique_ptr 或裸指针。

4.3 自定义删除器

当使用非 new/delete 分配的资源时,例如 FILE*HWND,请使用自定义删除器,以确保资源正确释放。

5. 代码示例

#include <memory>
#include <iostream>
#include <vector>

struct Widget {
    Widget(int id) : id(id) { std::cout << "Widget " << id << " constructed\n"; }
    ~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
    int id;
};

int main() {
    // unique_ptr 示例
    std::unique_ptr <Widget> uptr = std::make_unique<Widget>(1);
    // 传递所有权
    std::unique_ptr <Widget> moved = std::move(uptr);

    // shared_ptr 示例
    std::shared_ptr <Widget> sp1 = std::make_shared<Widget>(2);
    std::shared_ptr <Widget> sp2 = sp1; // 共享
    std::cout << "Ref count: " << sp1.use_count() << "\n";

    // weak_ptr 示例
    std::weak_ptr <Widget> wp = sp1;
    if (auto locked = wp.lock()) {
        std::cout << "Widget still alive, id = " << locked->id << "\n";
    }

    // 循环引用示例
    struct Node {
        std::vector<std::shared_ptr<Node>> children;
        std::weak_ptr <Node> parent;
    };
    auto root = std::make_shared <Node>();
    auto child = std::make_shared <Node>();
    child->parent = root;
    root->children.push_back(child);
    // 通过 weak_ptr 防止循环引用
}

6. 小结

  • unique_ptr 是最快速、最安全的单一所有权管理工具。
  • shared_ptr 适用于真正需要共享所有权的场景,但需警惕循环引用。
  • weak_ptr 解决了 shared_ptr 的循环引用问题,成为观察者模式的天然选择。

通过合理搭配使用这三种智能指针,可以让 C++ 代码更安全、更高效,减少内存泄漏、悬空指针等难以追踪的错误。

发表评论