基于本文回答
0
评论

std::shared_ptr、std::unique_ptr 和 std::weak_ptr 的区别及适用场景

知识点图片

在 C++11 及其之后的标准中,智能指针(Smart Pointers)是管理动态内存的核心工具。它们基于 RAII(资源获取即初始化)原则,能够自动释放内存,从而有效避免内存泄漏和悬空指针问题。

C++ 标准库提供了三种主要的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr。它们的区别主要在于所有权模型底层机制


1. std::unique_ptr(独占所有权)

核心概念

unique_ptr 代表独占式(Exclusive)的所有权。同一时刻,只能有一个 unique_ptr 指向同一个动态分配的对象。当 unique_ptr 被销毁(离开作用域)时,它所指向的对象也会被自动销毁。

特点与机制

  • 不可复制:不能通过赋值或拷贝构造函数复制 unique_ptr(禁用了 copy 语义)。
  • 可移动:可以通过 std::move 将所有权从一个 unique_ptr 转移到另一个(启用了 move 语义)。
  • 零开销:在默认情况下,unique_ptr 的大小和裸指针(raw pointer)完全一样,几乎没有性能开销。

适用场景

  • 局部动态对象:在函数内部需要动态分配内存,且不需要与其他部分共享时(这是最常见的场景)。
  • 作为类的成员变量:当一个类需要管理一个生命周期与自己绑定的动态对象时。
  • 工厂模式的返回值:函数返回一个动态创建的对象,并把内存管理权交接给调用者。

代码示例

cpp
#include <memory>
#include <iostream>

class MyClass {
public:
    void doSomething() { std::cout << "Doing something\n"; }
};

void unique_ptr_demo() {
    // 推荐使用 std::make_unique (C++14 起)
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
    ptr1->doSomething();

    // std::unique_ptr<MyClass> ptr2 = ptr1; // 错误:不能复制!

    // 将所有权从 ptr1 转移给 ptr2
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1); 
    
    if (!ptr1) {
        std::cout << "ptr1 is now empty.\n"; // ptr1 现在为空
    }
    ptr2->doSomething();
} // ptr2 离开作用域,自动 delete 其管理的 MyClass 对象

2. std::shared_ptr(共享所有权)

核心概念

shared_ptr 代表共享式(Shared)的所有权。多个 shared_ptr 可以同时指向同一个对象。它内部使用引用计数(Reference Counting)来管理内存。

特点与机制

  • 引用计数:每当复制一个 shared_ptr 时,内部的引用计数加 1;每当一个 shared_ptr 离开作用域被销毁时,引用计数减 1。
  • 自动释放:当引用计数降为 0 时,说明没有指针再指向该对象,对象会被自动 delete。
  • 性能开销:比 unique_ptr 开销大。因为它需要额外分配一个控制块(Control Block)来存储引用计数,并且引用计数的增减是原子操作(Thread-safe),这会带来一定的性能损耗。

适用场景

  • 复杂的生命周期:当对象的生命周期无法通过单纯的单一作用域确定,且多个组件都需要持有该对象时。
  • 多线程共享数据:多个线程需要访问同一个对象,只要还有一个线程在使用,对象就不能被销毁。
  • 底层数据结构:例如图(Graph)或某些复杂的树结构中,多个节点可能共享对某个子节点的引用。

代码示例

cpp
#include <memory>
#include <iostream>

void shared_ptr_demo() {
    // 推荐使用 std::make_shared (只需一次内存分配,性能更好)
    std::shared_ptr<int> ptr1 = std::make_shared<int>(100);
    std::cout << "Count: " << ptr1.use_count() << "\n"; // 输出: 1

    {
        std::shared_ptr<int> ptr2 = ptr1; // 复制,共享所有权
        std::cout << "Count: " << ptr1.use_count() << "\n"; // 输出: 2
    } // ptr2 离开作用域,引用计数减 1

    std::cout << "Count: " << ptr1.use_count() << "\n"; // 输出: 1
} // ptr1 离开作用域,引用计数变为 0,内存释放

3. std::weak_ptr(弱引用观察者)

核心概念

weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个由 shared_ptr 管理的对象。它的存在是为了协助 shared_ptr 工作。

特点与机制

  • 不增加引用计数:用 shared_ptr 初始化一个 weak_ptr,不会增加控制块中的“强引用计数(Strong Count)”,因此不会阻止对象的销毁。
  • 防悬空(Dangling Pointer):它知道自己指向的对象是否还活着。
  • 不能直接访问数据weak_ptr 没有重载 *-> 运算符。要访问对象,必须调用 .lock() 方法,将其升级(提升)为一个 shared_ptr。如果对象已被销毁,.lock() 会返回一个空的 shared_ptr

适用场景

  • 打破循环引用:这是 weak_ptr 最重要的用途。如果对象 A 有一个 shared_ptr 指向对象 B,对象 B 也用 shared_ptr 指向对象 A,两者的引用计数永远不会降为 0,导致内存泄漏。将其中一个改为 weak_ptr 即可打破僵局。
  • 观察者模式(Observer Pattern):订阅者只需“观察”发布者,但不应该延长发布者的生命周期。
  • 缓存(Caching):保存指向可能被销毁对象的指针,使用前检查是否还在。

代码示例(打破循环引用)

cpp
#include <memory>
#include <iostream>

class B; // 前置声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    // 如果这里用 std::shared_ptr<A>,将导致内存泄漏!
    std::weak_ptr<A> a_ptr; 
    ~B() { std::cout << "B destroyed\n"; }

    void useA() {
        // 使用前必须 lock 提升为 shared_ptr
        if (std::shared_ptr<A> shared_a = a_ptr.lock()) {
            std::cout << "A is still alive\n";
        } else {
            std::cout << "A has been destroyed\n";
        }
    }
};

void weak_ptr_demo() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->b_ptr = b;
    b->a_ptr = a; // weak_ptr 赋值,不增加 A 的引用计数
} // 正常销毁 A 和 B

总结与最佳实践

核心对比表

智能指针 所有权模型 复制 / 移动 性能开销 主要解决的问题
std::unique_ptr 独占 (1 对 1) 不可复制 / 可移动 极低(同裸指针) 替代手动 new/delete,管理局部动态内存
std::shared_ptr 共享 (多 对 1) 可复制 / 可移动 中等(需维护控制块和原子计数) 复杂对象的生命周期管理,多组件共享数据
std::weak_ptr 不拥有 (观察者) 可复制 / 可移动 低(通常结合 shared_ptr 使用) 打破 shared_ptr 循环引用,安全地观察对象

💡 日常开发建议(Rule of Thumb)

  1. 默认首选 std::unique_ptr:只要你不需要在多个地方共享所有权,就无脑用 unique_ptr。它最轻量、最快、意图最清晰。
  2. 谨慎使用 std::shared_ptr:只有在真的无法确定对象何时该被销毁,且确实需要多个持有者共享生命周期时才用。滥用会导致系统性能下降和逻辑混乱。
  3. 永远使用 std::make_uniquestd::make_shared:不要手动 new 再传给智能指针。make_ 系列函数不仅更安全(防止内存泄漏),对于 shared_ptr 来说性能也更好(减少一次内存分配)。
  4. 注意循环引用:如果设计中出现了互相持有的 shared_ptr,立刻检查是否应该把其中一个换成 weak_ptr
右滑查看面试常问