Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std::unique_ptr

std::unique_ptr 是 C++11 引入的智能指针,表示对资源的独占所有权。它应该是你管理堆对象时的默认选择——没有额外开销,语义清晰,不会出现「不知道谁负责释放」的问题。

一、基本用法

#include <memory>

// 推荐:用 make_unique 创建(C++14)
auto p = std::make_unique<MyClass>(42);

// 也可以用裸指针构造(C++11)
std::unique_ptr<MyClass> p2(new MyClass(42));

p->doSomething();   // 像普通指针一样使用
(*p).doSomething();

unique_ptr 的大小和裸指针一样(64 位系统上 8 字节),因为它不需要存引用计数之类的额外信息——所有权是唯一的,销毁时机在编译期就确定了。

优先用 std::make_unique 而不是 new。除了更简洁,它还能避免一个微妙的异常安全问题:

// 危险:如果 MyClass 构造抛异常,或者 g() 抛异常,
// 都可能让 new 出来的内存泄漏
f(std::unique_ptr<MyClass>(new MyClass()), g());

// 安全
f(std::make_unique<MyClass>(), g());

二、所有权转移

unique_ptr 不能拷贝,只能移动:

auto p1 = std::make_unique<MyClass>();
// auto p2 = p1;                 // 编译错误:不能拷贝
auto p2 = std::move(p1);         // 正确:转移所有权

// 此时 p1 变为 nullptr
if (!p1)
    std::cout << "p1 已失去所有权\n";

这个限制是有意设计的。如果允许拷贝,两个 unique_ptr 会指向同一对象,销毁时就会重复释放。用 std::move 显式转移,代码里所有权流转一目了然。

作为函数参数与返回值

// 接管所有权:调用者不能再使用
void takeOwnership(std::unique_ptr<MyClass> p);

// 只借用:不涉及所有权转移
void useObject(const MyClass& obj);
void useObject(MyClass* obj);

// 返回所有权:把资源交给调用者
std::unique_ptr<MyClass> create()
{
    return std::make_unique<MyClass>();   // 移动语义,无拷贝开销
}

函数返回局部 unique_ptr 时不需要写 std::move,编译器会自动应用移动(NRVO 或隐式移动)。

三、常用接口

接口作用
get()返回裸指针,不转移所有权
release()放弃所有权并返回裸指针,需要手动 delete
reset(p)释放当前对象,接管新指针 p(默认 nullptr)
swap(other)交换两个 unique_ptr
operator bool是否持有对象
operator* / operator->访问对象
auto p = std::make_unique<int>(42);

int* raw = p.get();          // 只读,p 仍然拥有

p.reset();                   // 释放对象,p 变为空
p.reset(new int(100));       // 释放旧的,接管新的

if (p)                       // 判空
    std::cout << *p << '\n';

release() 要小心使用——它把所有权交还给裸指针,之后必须自己 delete:

auto p = std::make_unique<int>(42);
int* raw = p.release();   // p 变空,raw 需要手动管理
delete raw;               // 别忘了

它主要用于与 C 风格接口交接所有权。

四、数组支持

unique_ptr 有数组特化版本 unique_ptr<T[]>,它使用 delete[] 而非 delete:

auto arr = std::make_unique<int[]>(10);   // 10 个 int

arr[0] = 1;      // 支持 operator[]
arr[9] = 10;
// 自动 delete[],无需手动管理

不过大多数情况下 std::vector 是更好的选择——它知道自己的大小,支持迭代器和算法。

五、自定义删除器

unique_ptr 的第二个模板参数是删除器,可以自定义释放方式:

template<class T, class Deleter = std::default_delete<T>>
class unique_ptr;

这让 unique_ptr 能管理任何「需要释放」的资源,不限于 new 出来的内存:

// 管理文件
auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> file(fopen("a.txt", "r"), fileDeleter);

// 管理 malloc 的内存
std::unique_ptr<int, decltype(&std::free)> buf(
    (int*)std::malloc(sizeof(int) * 10), &std::free);

// 管理 socket
auto socketDeleter = [](int* fd) { if (*fd >= 0) close(*fd); delete fd; };
std::unique_ptr<int, decltype(socketDeleter)> sock(new int(fd), socketDeleter);

注意:使用自定义删除器后,unique_ptr 的大小可能变大(需要存储删除器对象)。无状态的删除器(如函数指针、无捕获 lambda)通常不增加大小,因为编译器可以做空基类优化。

六、作为类成员

unique_ptr 常用于实现 Pimpl 惯用法(Pointer to Implementation),把实现细节隐藏起来:

// widget.h
class Widget
{
public:
    Widget();
    ~Widget();                          // 必须在 .cpp 中定义
    Widget(Widget&&) noexcept;          // 同上
    Widget& operator=(Widget&&) noexcept;

    void doSomething();

private:
    struct Impl;
    std::unique_ptr<Impl> impl_;        // 只暴露前置声明
};

// widget.cpp
struct Widget::Impl
{
    std::string data;
    // 复杂的实现细节
};

Widget::Widget() : impl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;            // Impl 在这里才是完整类型

这样做的好处:

  • 头文件不需要包含实现依赖,加快编译
  • 修改实现不会导致使用方重新编译
  • 保持 ABI 稳定

注意析构函数必须在 .cpp 中定义——因为在头文件里 Impl 还是不完整类型,unique_ptr 的析构需要完整类型。

七、作为工厂函数返回值

工厂函数返回 unique_ptr 是标准做法,把所有权明确交给调用者:

class Shape
{
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
};

std::unique_ptr<Shape> createShape(const std::string& type)
{
    if (type == "circle")
        return std::make_unique<Circle>();
    if (type == "square")
        return std::make_unique<Square>();
    return nullptr;
}

// 使用
auto shape = createShape("circle");
if (shape)
    shape->draw();

这里基类的析构函数必须是 virtual,否则通过基类指针删除派生类对象是未定义行为。

八、常见误区

「unique_ptr 不能放进容器」 —— 可以,但容器需要支持移动。std::vector<std::unique_ptr<T>> 是合法的,只是不能用需要拷贝的操作(如 std::copy)。

「返回 unique_ptr 一定要写 std::move」 —— 返回局部变量时不需要,编译器会隐式移动。写 std::move 反而可能阻止 NRVO 优化。

「get() 返回的指针可以随便用」 —— get() 只是借用,unique_ptr 销毁后该指针立刻悬垂。不要保存它。

「release() 之后 unique_ptr 还会释放」 —— 不会。release() 就是放弃所有权,之后必须自己管理。

「自定义删除器不影响大小」 —— 有状态的删除器会增加 unique_ptr 的大小。无状态删除器通常不增加。

「unique_ptr 可以拷贝到 shared_ptr」 —— 可以,用移动:std::shared_ptr<T> sp = std::move(up);。这是把独占所有权升级为共享所有权的唯一方式。

九、相关章节