跳至内容

std::unique_ptr:独占所有权

学习 std::unique_ptr 如何提供零开销的独占所有权,如何使用 make_unique、C 资源的自定义删除器以及工厂函数模式。

什么是 unique_ptr?

std::unique_ptr 是一个独占拥有对象的智能指针 ── 在任何给定时刻恰好只有一个 unique_ptr 指向该对象。当 unique_ptr 被销毁(离开作用域)时,它会自动删除所拥有的对象。

关键特性:
- 不可拷贝 ── 拷贝会创建两个所有者,违反独占所有权。
- 可移动 ── 所有权可以通过 std::move 转移。
- 零开销 ── sizeof(unique_ptr) == sizeof(T*)(使用默认删除器时)。生成的代码与使用原始指针加手动 delete 完全相同。
- 这是默认的智能指针。首先使用 unique_ptr;只有在确实需要共享所有权时才使用 shared_ptr。

make_unique:唯一正确的方式

std::make_unique(args...)(C++14)是创建 unique_ptr 的唯一正确方式。它是异常安全的、简洁的,并且避免写 new。

unique_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Connection {
    std::string host;
    int port;
    Connection(std::string h, int p) : host(std::move(h)), port(p) {
        std::cout << "Connected to " << host << ":" << port << '\n';
    }
    ~Connection() {
        std::cout << "Disconnected from " << host << '\n';
    }
    void query(const std::string& sql) {
        std::cout << "Executing: " << sql << '\n';
    }
};

int main() {
    // CORRECT: use make_unique
    auto conn = std::make_unique<Connection>("localhost", 5432);
    conn->query("SELECT * FROM users");

    // Access the raw pointer (non-owning)
    Connection* raw = conn.get();  // does NOT transfer ownership

    // Transfer ownership
    auto conn2 = std::move(conn);
    // conn is now nullptr
    if (!conn) std::cout << "conn is null after move\n";

    // conn2 destroyed here — destructor called automatically
}  // Output: "Disconnected from localhost"

C 资源的自定义删除器

许多 C 库返回的原始指针必须使用特定函数(而非 delete)来释放。unique_ptr 支持自定义删除器来处理这些情况。删除器成为类型的一部分,因此使用函数指针或无状态 lambda 时开销为零。

custom_deleter.cpp
#include <cstdio>
#include <memory>
#include <iostream>

// Custom deleter for FILE*
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::fclose(f);
            std::cout << "File closed\n";
        }
    }
};

// Type alias for clarity
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;

// Factory function
UniqueFile open_file(const char* path, const char* mode) {
    FILE* f = std::fopen(path, mode);
    if (!f) throw std::runtime_error("Cannot open file");
    return UniqueFile(f);
}

int main() {
    try {
        auto file = open_file("/tmp/test.txt", "w");
        std::fputs("Hello, RAII!\n", file.get());
        // file automatically closed when scope exits
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }

    // Lambda deleter (less common, but useful for one-offs)
    auto deleter = [](int* p) { std::cout << "custom free\n"; delete p; };
    std::unique_ptr<int, decltype(deleter)> p(new int(42), deleter);
}

unique_ptr 与数组

std::unique_ptr 管理一个动态分配的数组并自动调用 delete[]。它提供 operator[] 而不是 operator* 和 operator->。

auto arr = std::make_unique<int[]>(100);  // 100 个零初始化的 int
arr[0] = 42;

然而,std::vector 几乎总是更好的选择 ── 它知道自己的大小、支持迭代,并且同样高效。仅在与 C API 交互时或需要不可调整大小、不可拷贝的缓冲区时才使用 unique_ptr。

返回 unique_ptr 的工厂函数

从工厂函数返回 unique_ptr 是惯用的 C++ 所有权模式。调用者获得独占所有权。如果调用者需要共享所有权,可以进行转换:std::shared_ptr sp = factory();。

factory_unique_ptr.cpp
#include <memory>
#include <iostream>
#include <string>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
};

// Factory — caller owns the result
std::unique_ptr<Shape> make_shape(const std::string& type, double a, double b = 0) {
    if (type == "circle")    return std::make_unique<Circle>(a);
    if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
    return nullptr;  // unknown shape
}

int main() {
    auto s = make_shape("circle", 5.0);
    if (s) {
        std::cout << s->name() << " area: " << s->area() << '\n';
    }
    // s automatically deleted here
}
常见陷阱

永远不要通过 const 引用传递 unique_ptr 给仅观察对象的函数 ── 改为传递原始指针或引用。unique_ptr 参数表示所有权转移。

- void use(Widget* w) ── 观察,无所有权
- void sink(std::unique_ptr w) ── 获取所有权(调用时使用 std::move)
- void reseat(std::unique_ptr& w) ── 可能替换所拥有的对象

永远不要从同一个原始指针构造两个 unique_ptr ── 两者都会调用 delete,导致双重释放。这就是为什么推荐 make_unique:不会有原始指针被意外重用。

最佳实践

1. 默认使用 unique_ptr ── 它应该是你进行堆分配的首选。
2. 始终使用 make_unique ── 永远不要写 unique_ptr(new T(...))。
3. 从工厂函数返回 unique_ptr ── 如果调用者需要,它可以隐式转换为 shared_ptr。
4. 按值传递以转移所有权 ── void sink(unique_ptr p)。调用者必须使用 std::move。
5. 传递原始指针/引用来观察 ── void use(const T& obj) 或 void use(T* obj)。
6. 将移动构造函数/赋值运算符标记为 noexcept ── 对于存储在容器中的类型至关重要。

关键要点
  • unique_ptr 提供独占所有权,相对于原始指针零开销
  • 始终通过 std::make_unique(args...) 创建 ── 永远不要使用裸 new
  • 不可拷贝但可移动 ── 所有权通过 std::move 转移
  • 自定义删除器为 C 资源(FILE*、套接字等)启用 RAII
  • 从工厂函数返回 unique_ptr ── 如果需要可以转换为 shared_ptr
  • 当函数仅观察而不拥有时,传递原始指针或引用

测验 — 检验你的知识

(15 经验值)

1. 与原始 `T*` 相比,`std::unique_ptr<T>` 的开销是多少(使用默认删除器)?

2. 应该如何将 `unique_ptr` 传递给需要获取所有权的函数?

3. 为什么 `std::make_unique` 优于 `unique_ptr<T>(new T(...))`?