C++中的三种智能指针:shared_ptr、unique_ptr 与 weak_ptr 的使用与区别

智能指针是 C++11 引入的资源管理工具,帮助开发者自动管理堆内存,避免内存泄漏和悬空指针等错误。C++ 标准库中提供了三种最常用的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr。下面将从语义、使用场景、实现细节以及常见陷阱四个维度进行深入剖析。

1. 语义与基本特征

指针类型 所有权模型 复制语义 线程安全 典型用法
unique_ptr 独占所有权 只能移动,复制被删除 线程安全的移动、销毁 临时对象、局部资源、单线程场景
shared_ptr 共享所有权 复制拷贝计数 线程安全的计数操作 需要多方共享、生命周期难以预估
weak_ptr 弱引用 复制拷贝计数 线程安全的计数操作 防止循环引用、观察者模式、缓存
  • unique_ptr:指针拥有唯一所有权,不能被复制,只能通过 std::move 转移。销毁时会立即释放指向的对象。
  • shared_ptr:使用引用计数实现共享所有权。每次拷贝都会增计数,销毁时计数归零才真正释放对象。
  • weak_ptr:不参与引用计数,单独存储指向对象的 std::shared_ptr 所共享的原始指针。通过 lock() 可安全地获得 shared_ptr,若对象已被销毁则返回空指针。

2. 典型使用场景

场景 推荐指针
临时所有权、不可共享、性能敏感 unique_ptr
需要跨多个函数或线程共享同一对象、生命周期不确定 shared_ptr
防止 shared_ptr 循环引用、实现观察者/事件模式 weak_ptr
对象内部需要指向自己但不增加所有权计数 weak_ptr

举例 1:文件资源管理

std::unique_ptr<std::FILE, decltype(&std::fclose)> file(
    std::fopen("log.txt", "a"), std::fclose);
if (!file) throw std::runtime_error("open failed");
fputs("Hello, world!\n", file.get());

这里 unique_ptr 与自定义删除器组合使用,确保文件及时关闭。

举例 2:共享对象

std::shared_ptr <Widget> p1 = std::make_shared<Widget>();
std::shared_ptr <Widget> p2 = p1;   // 计数 +1

只要 p1p2 任何一个被销毁,计数会递减,计数归零后真正销毁对象。

举例 3:观察者模式

class Subject {
    std::vector<std::weak_ptr<Observer>> observers;
public:
    void addObserver(const std::shared_ptr <Observer>& obs) {
        observers.emplace_back(obs);
    }
    void notify() {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto sp = it->lock()) { // 仍然存活
                sp->update();
                ++it;
            } else {
                it = observers.erase(it); // 已销毁,移除
            }
        }
    }
};

weak_ptr 防止 SubjectObserver 形成循环引用。

3. 实现细节

  • unique_ptr 本质上是裸指针加自定义删除器,删除器可为函数指针、函数对象或 lambda。
  • shared_ptr 内部维护一个 control block(控制块)来存储引用计数、弱引用计数、删除器以及自定义内存分配器。计数操作使用原子操作,保证多线程安全。
  • weak_ptrshared_ptr 共用同一个控制块,但不影响引用计数。它们的 lock() 操作会在尝试提升计数时使用 CAS(原子比较并交换),如果计数已经为 0 则返回空 shared_ptr

4. 常见陷阱与最佳实践

  1. 循环引用导致内存泄漏

    • 只要两对象相互持有 shared_ptr,计数永远不为 0。
    • 解决方案:在至少一条链使用 weak_ptr
  2. 不恰当的删除器

    • unique_ptr 的默认删除器是 delete,若指针指向数组或文件等需自定义删除器,否则会导致未定义行为。
  3. 多线程共享 shared_ptr

    • shared_ptr 本身线程安全,但其指向的对象并不一定线程安全。
    • 在多线程访问时请使用同步原语(如 std::mutex)或设计无锁访问。
  4. 性能权衡

    • unique_ptr 轻量,适用于所有不需要共享的场景。
    • shared_ptr 在高性能需求时可能带来计数更新的开销,尤其在多线程环境。
    • 对于大对象,最好使用 std::shared_ptr<T[]>std::shared_ptr<std::vector<T>> 以避免多次拷贝。
  5. make_shared vs new

    • std::make_shared 在一个分配中同时创建对象和控制块,减少内存碎片并提高缓存命中率。
    • 仅当需要自定义删除器或控制块时才考虑手动 new

5. 小结

  • unique_ptr:最简洁的所有权管理,适合单一所有者。
  • shared_ptr:实现共享所有权,计数机制使对象生命周期自动管理。
  • weak_ptr:防止循环引用,提供对共享对象的安全观察。

熟练运用这三种智能指针是现代 C++ 开发的基石,它们帮助我们写出更安全、更易维护、并行友好的代码。掌握其语义、使用场景与实现细节,能够在复杂项目中灵活选择最合适的资源管理策略。

发表评论