C++ 中的智能指针:std::shared_ptr 与 std::unique_ptr 的细节与最佳实践

智能指针是 C++11 之后对资源管理进行封装的重要工具,它们通过 RAII(资源获取即初始化)机制,自动管理动态分配的内存,显著降低内存泄漏和悬空指针的风险。常见的智能指针有 std::unique_ptrstd::shared_ptrstd::weak_ptr。本文聚焦于 std::unique_ptrstd::shared_ptr 的细节、区别以及在实际项目中的最佳实践。


1. 设计哲学

1.1 std::unique_ptr

  • 唯一所有权:每个资源只能被一个 unique_ptr 持有,禁止复制,只能移动。
  • 轻量级:内部仅包含裸指针(以及可选的删除器),几乎无额外开销。
  • 延迟删除:对象在 unique_ptr 被销毁时自动调用删除器释放资源。

1.2 std::shared_ptr

  • 共享所有权:同一资源可以被多个 shared_ptr 持有,内部维护引用计数。
  • 线程安全:引用计数的增减操作使用原子操作,保证多线程场景安全。
  • 可能产生循环引用:当相互引用的对象持有 shared_ptr 时,可能导致内存泄漏。

2. 细节差异

特性 std::unique_ptr std::shared_ptr
复制 禁止 允许,引用计数+1
移动 允许 允许
默认删除器 delete delete
自定义删除器 必须在构造时指定 必须在构造时指定
内存占用 1 个指针 2 个指针(指针 + 引用计数)
线程安全 非线程安全 线程安全(计数)
典型用途 所有权唯一的资源 多处共享的资源

3. 实际案例

3.1 资源管理示例

#include <iostream>
#include <memory>

struct File {
    explicit File(const std::string& name) : name_(name) {
        std::cout << "Open file: " << name_ << '\n';
    }
    ~File() {
        std::cout << "Close file: " << name_ << '\n';
    }
    void read() { std::cout << "Reading from " << name_ << '\n'; }

private:
    std::string name_;
};

void processFile(std::unique_ptr <File> f) {
    f->read();
    // f 在此函数结束时自动销毁
}

int main() {
    auto file = std::make_unique <File>("data.txt");
    processFile(std::move(file));
    // file 现在为空
}

说明unique_ptr 确保文件只在 processFile 里被访问,传递时使用 std::move 明确所有权转移。

3.2 共享资源示例

#include <iostream>
#include <memory>

struct Logger {
    Logger(const std::string& name) : name_(name) {
        std::cout << "Logger created: " << name_ << '\n';
    }
    ~Logger() {
        std::cout << "Logger destroyed: " << name_ << '\n';
    }
    void log(const std::string& msg) {
        std::cout << "[" << name_ << "] " << msg << '\n';
    }

private:
    std::string name_;
};

void worker(std::shared_ptr <Logger> logger, int id) {
    logger->log("Worker " + std::to_string(id) + " started");
    // 计数自动递增/递减
}

int main() {
    auto logger = std::make_shared <Logger>("AppLogger");
    worker(logger, 1);
    worker(logger, 2);
    // logger 在 main 结束时被销毁
}

说明:多线程或多模块共享同一个 Loggershared_ptr 自动管理生命周期。


4. 最佳实践

4.1 何时使用 unique_ptr

  • 对象只需要单一拥有者,例如管理文件句柄、线程对象、单例模式的内部实现。
  • 避免不必要的引用计数开销。

4.2 何时使用 shared_ptr

  • 对象需要被多个独立部件共享,例如 GUI 组件、资源缓存。
  • 必须保证所有权共享且对象生命周期与使用者同步。

4.3 防止循环引用

  • 使用 std::weak_ptr 来断开循环,例如父子关系、观察者模式。
  • 示例:
struct Node {
    std::weak_ptr <Node> parent;   // 父节点使用 weak_ptr 避免循环
    std::vector<std::shared_ptr<Node>> children;
};

4.4 自定义删除器

  • 对非 new/delete 分配的资源(如 malloc、文件句柄)需提供自定义删除器。
auto buffer = std::unique_ptr<int[], void(*)(int*)>(reinterpret_cast<int*>(malloc(10 * sizeof(int))), [](int* p){ free(p); });

4.5 线程安全注意

  • 虽然 shared_ptr 的引用计数是线程安全的,但对象本身的状态不是。需要外部同步或使用 std::atomicstd::mutex

5. 性能对比

场景 unique_ptr shared_ptr
单线程 近乎无额外开销 约 1.5 倍内存
多线程 需要手动同步 计数原子操作可减少锁
频繁创建销毁 高效 由于引用计数调增/调减,略慢

通过 std::move 的移动语义,unique_ptr 在大多数单所有权场景下是最优选择。


6. 结语

智能指针的出现,使得 C++ 在资源管理方面与现代语言趋同。正确理解 std::unique_ptrstd::shared_ptr 的语义差异,结合具体业务场景,能显著提升代码的健壮性和可维护性。记住:所有权决定智能指针的类型循环引用需要 weak_ptr自定义删除器可扩展智能指针的使用范围。祝编码愉快!

发表评论