C++ 中的智能指针:std::unique_ptr 与 std::shared_ptr 的区别及最佳实践

在 C++11 及以后,智能指针已成为资源管理的核心工具。本文聚焦两大主流实现——std::unique_ptrstd::shared_ptr,从语义、性能、安全性等维度对比它们,并给出在实际项目中的最佳实践。

1. 语义区别

  • std::unique_ptr
    • 所有权独占:指针拥有唯一所有者,无法复制,只能移动。
    • 自动释放:所有者销毁时自动调用删除器,适合单一对象或链表、树等结构。
  • std::shared_ptr
    • 共享所有权:可以多处复制,内部维护引用计数。计数为零时销毁。
    • 多线程安全:引用计数自增/自减是原子操作,适合共享资源。

2. 性能考量

特性 unique_ptr shared_ptr
额外开销 0(仅指针) 2 个引用计数(一个强计数、一个弱计数)
线程开销 原子计数操作,轻量但仍有开销
内存占用 8/16 B(64/32 bit) 24/32 B(64/32 bit)+ 管理块

当不需要共享所有权时,unique_ptr 是首选,既安全又高效。

3. 错误使用场景

  • 误用 shared_ptr 造成循环引用

    struct Node {
        std::shared_ptr <Node> next;
        std::weak_ptr <Node> prev; // 必须弱引用
    };

    prev 也是 shared_ptr,两个节点会互相持有,导致内存泄漏。

  • 在多线程里使用 unique_ptr 共享数据
    unique_ptr 不是线程安全的,若跨线程使用必须使用 std::shared_ptr 或外部同步。

  • 与 C API 交互时忘记自定义删除器

    std::unique_ptr<FILE, decltype(&fclose)> fp(fopen(...), fclose);

    unique_ptr 的删除器默认是 delete,不适用于 C 资源。

4. 最佳实践

  1. 优先使用 unique_ptr

    • 对象生命周期由创建者决定,避免不必要的引用计数。
    • 通过 std::move 将所有权转移给容器或返回值。
  2. 在需要共享所有权时才使用 shared_ptr

    • 例如 GUI 事件回调、缓存共享、资源池。
    • 如果可能,使用 std::weak_ptr 防止循环引用。
  3. 自定义删除器

    • 对于自定义资源或 C 风格对象,传入删除器或使用 std::unique_ptr<... , Deleter>
  4. 与 std::make_unique / std::make_shared 结合使用

    • 避免多次 new,保证异常安全。
  5. 考虑 move-only 对象

    • std::vector<std::unique_ptr<T>> 需要使用 emplace_back(std::make_unique<T>(...))
    • std::vector<std::shared_ptr<T>> 可以直接 push_back

5. 代码示例

5.1 资源管理

// 文件句柄管理
class File {
    std::unique_ptr<FILE, decltype(&fclose)> file_;
public:
    explicit File(const char* path) :
        file_(fopen(path, "r"), fclose) {
        if (!file_) throw std::runtime_error("open failed");
    }
    FILE* get() const { return file_.get(); }
};

5.2 共享资源示例

class Widget {
public:
    Widget() { std::cout << "Widget created\n"; }
    ~Widget() { std::cout << "Widget destroyed\n"; }
};

void worker(std::shared_ptr <Widget> w) {
    std::cout << "Worker use widget\n";
}

int main() {
    auto w = std::make_shared <Widget>();
    std::thread t1(worker, w);
    std::thread t2(worker, w);
    t1.join(); t2.join(); // 共享所有权,引用计数自动管理
}

5.3 防止循环引用

struct Node {
    std::shared_ptr <Node> next;
    std::weak_ptr <Node> prev;  // 关键:使用 weak_ptr
};

void link(Node* a, Node* b) {
    a->next = std::shared_ptr <Node>(b);
    b->prev = std::weak_ptr <Node>(a);
}

6. 结语

在 C++ 编程中,智能指针的正确使用是提高代码安全性和可维护性的关键。

  • unique_ptr 适合拥有唯一所有者的资源,既简单又高效。
  • shared_ptr 在需要共享所有权时才使用,并配合 weak_ptr 防止循环引用。

遵循上述最佳实践,可在保持代码简洁的同时,避免内存泄漏和资源竞争问题。

发表评论