**C++ 中如何安全地使用 std::shared_ptr 与 std::weak_ptr?**

在现代 C++ 中,std::shared_ptrstd::weak_ptr 是管理动态资源的重要工具。正确使用它们可以防止内存泄漏、野指针以及循环引用等问题。下面从几个方面阐述安全使用的最佳实践。

  1. 避免循环引用
    当两个或更多对象相互持有 std::shared_ptr 时,会形成循环引用,导致引用计数永远不为零,资源无法释放。

    • 解决方案:将至少一方改为 std::weak_ptr
    • 示例
      struct B;  // 前向声明
      struct A {
          std::shared_ptr <B> ptrB;
          ~A() { std::cout << "A destroyed\n"; }
      };
      struct B {
          std::weak_ptr <A> ptrA; // 这里使用 weak_ptr
          ~B() { std::cout << "B destroyed\n"; }
      };
  2. 使用 std::make_shared
    std::make_shared 在一次内存分配中同时分配对象和控制块,减少内存碎片,且更安全。

    auto sp = std::make_shared <MyClass>(arg1, arg2);
  3. 避免在容器中直接存放 std::shared_ptr,改用 std::unique_ptr 或原始指针
    当容器本身负责对象的生命周期时,使用 std::unique_ptr 更为合适。

    std::vector<std::unique_ptr<MyClass>> vec;
    vec.push_back(std::make_unique <MyClass>(...));
  4. 显式使用 weak_ptr::lock()
    在需要访问被 weak_ptr 所指向的对象时,先调用 lock() 获得 shared_ptr,并检查是否为空。

    if (auto sp = weakPtr.lock()) {
        sp->doSomething();
    } else {
        // 对象已被销毁
    }
  5. 避免在 shared_ptr 的析构过程中再次创建新的 shared_ptr
    由于析构过程中引用计数会递减,若在析构时又创建新的 shared_ptr,可能导致意外的循环引用。

    • 做法:将对象的析构逻辑拆分到纯成员函数中,避免在析构期间再次构造 shared_ptr
  6. 线程安全
    std::shared_ptr 的引用计数操作是原子性的,但其指向对象的访问不是线程安全的。

    • 若在多线程环境下共享同一对象,使用互斥量(std::mutex)或线程安全的容器保护访问。
  7. 与资源管理对象配合使用 RAII
    对于非堆内存(如文件句柄、网络连接等),将它们封装成 RAII 对象,并在该对象内部管理 shared_ptr

    class FileHandle {
        std::shared_ptr<std::FILE> file_;
    public:
        FileHandle(const char* path, const char* mode)
            : file_(std::fopen(path, mode), std::fclose) {}
        // ...
    };
  8. 避免使用裸指针传递给 std::shared_ptr

    std::shared_ptr <MyClass> sp1(new MyClass()); // OK
    MyClass* raw = new MyClass();
    std::shared_ptr <MyClass> sp2(raw);           // OK,但容易出错
    std::shared_ptr <MyClass> sp3(raw);           // 错误:raw 已被 sp2 管理,重复删除

    建议始终使用 make_sharedmake_unique,不要手动 new。

  9. 考虑 std::scoped_lockstd::unique_lock 的使用
    对于需要在同一作用域内多次锁定同一互斥量的情况,使用 std::scoped_lock 更简洁。

  10. 监控引用计数
    虽然 C++ 标准库不直接提供引用计数查询,但可以通过 std::weak_ptr::use_count() 监控 shared_ptr 的使用次数,帮助调试循环引用。

总结
安全使用 std::shared_ptrstd::weak_ptr 的关键在于:

  • 避免循环引用,至少一方使用 weak_ptr
  • 统一使用 std::make_shared,减少内存碎片。
  • 在容器中使用 unique_ptr 或原始指针,由容器管理生命周期。
  • 使用 weak_ptr::lock() 进行安全访问。
  • 确保多线程访问的同步

通过遵循这些最佳实践,C++ 开发者可以在享受共享所有权带来的便利的同时,最大程度减少资源泄漏和悬空指针的风险。

发表评论