C++ 中的智能指针:unique_ptr 与 shared_ptr 的区别与使用场景

智能指针是 C++11 之后引入的内置资源管理工具,它们帮助程序员在使用堆内存时避免内存泄漏、悬空指针以及双重释放等常见错误。C++ 标准库提供了多种智能指针类型,其中最常用的是 std::unique_ptrstd::shared_ptr。本文将详细比较这两种智能指针的工作原理、使用场景以及它们在实际项目中的最佳实践。

1. 基本概念回顾

  • **`std::unique_ptr

    `** *独占所有权*:同一时刻只能有一个 `unique_ptr` 拥有某个对象的指针。它通过移动语义实现所有权转移,不能被拷贝。 *销毁机制*:当 `unique_ptr` 超出作用域或被显式销毁时,自动调用 `delete`(或自定义删除器)释放资源。
  • **`std::shared_ptr

    `** *共享所有权*:通过引用计数实现多份指针共享同一对象。每一次 `shared_ptr` 的拷贝都会将引用计数加 1,销毁时计数减 1,计数归零时删除对象。 *线程安全*:引用计数的增减是原子操作,保证多线程访问时不会出现计数错误。

2. 何时使用 unique_ptr

  1. 资源所有权清晰
    当你需要明确表明某个对象的唯一所有者时,使用 unique_ptr。这符合“所有权应该是显式且单一”的设计原则。

  2. 高性能场景
    unique_ptr 的实现几乎没有额外的开销(仅移动指针),适合对性能要求极高的代码。

  3. 非共享生命周期
    对象不需要在多个位置共享,或者共享的逻辑可以通过指针传递而非 shared_ptr 的引用计数。

  4. 防止循环引用
    在需要构建图结构或包含回指针的场景时,使用 unique_ptr 能避免循环引用导致的内存泄漏。

3. 何时使用 shared_ptr

  1. 需要共享所有权
    当多个对象、函数或线程需要共同持有同一资源时,shared_ptr 是最自然的选择。

  2. 资源生命周期不确定
    对象的生命周期由多个使用者共同决定,无法预先确定谁负责销毁。

  3. 接口设计需要容忍多方持有
    API 的返回值或参数可以是 shared_ptr,让调用方决定是否需要保留对象。

  4. 与旧代码或第三方库交互
    很多旧代码和库使用裸指针或自定义引用计数,使用 shared_ptr 可以方便地包装或转换。

4. 性能与内存占用对比

特性 unique_ptr shared_ptr
引用计数 8~16 字节(平台依赖)
复制/移动 只能移动 复制增加计数
线程安全 复制不可用(移动线程安全) 引用计数原子操作
适用场景 单一所有者 多重所有者

尽管 shared_ptr 在多所有者场景下非常方便,但它的额外引用计数会导致轻微的性能开销,尤其是在高频率创建和销毁对象时。因此,在性能敏感的代码路径上尽量使用 unique_ptr

5. 常见使用误区

  1. 在容器中使用裸指针
    直接在 std::vector<T*>std::map<Key,T*> 中存储裸指针,容易导致手动内存管理错误。
    解决方案:使用 std::vector<std::unique_ptr<T>>std::unordered_map<Key, std::shared_ptr<T>>

  2. 循环引用导致内存泄漏
    shared_ptr 互相指向会形成循环,计数永不归零。
    解决方案:引入 std::weak_ptr 来打破循环。

  3. unique_ptr 误用为共享指针
    通过 std::move 复制 unique_ptr 只会转移所有权,而不是复制对象。
    提醒:如果需要真正复制对象,必须显式调用复制构造函数或使用 std::make_shared

6. 实战案例

6.1 使用 unique_ptr 管理文件句柄

class FileHandler {
public:
    explicit FileHandler(const std::string& path)
        : file_(std::fopen(path.c_str(), "r")) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }

    ~FileHandler() {
        if (file_) std::fclose(file_);
    }

    // 禁止拷贝
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;

    // 允许移动
    FileHandler(FileHandler&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }

    FileHandler& operator=(FileHandler&& other) noexcept {
        if (this != &other) {
            if (file_) std::fclose(file_);
            file_ = other.file_;
            other.file_ = nullptr;
        }
        return *this;
    }

    // 业务方法
    std::string ReadLine() {
        char buffer[1024];
        if (std::fgets(buffer, sizeof(buffer), file_)) {
            return std::string(buffer);
        }
        return {};
    }

private:
    std::FILE* file_;
};

6.2 共享图节点示例

struct Node {
    int value;
    std::vector<std::weak_ptr<Node>> neighbors; // 避免循环引用
};

using NodePtr = std::shared_ptr <Node>;

NodePtr createNode(int val) {
    return std::make_shared <Node>(Node{val, {}});
}

void addEdge(NodePtr a, NodePtr b) {
    a->neighbors.emplace_back(b);
    b->neighbors.emplace_back(a);
}

7. 结语

unique_ptrshared_ptr 并非互相排斥,而是根据资源所有权和生命周期的需求选择合适的工具。正确使用它们能让 C++ 程序在自动内存管理、安全性和性能之间取得良好的平衡。希望本文能帮助你在实际项目中做出更明智的智能指针选择。

发表评论