4. unique_ptr(上):独占所有权模型,基本用法与make_unique

好,咱们今天来聊聊 unique_ptr。这是 C++11 引入的智能指针里,我个人用得最多的一个。为什么?因为它简单、高效,而且语义清晰——说白了,它就是「独占」两个字。

独占所有权模型:这东西是我的,你别碰

unique_ptr 的核心思想,就是「独占所有权」。一个资源(比如堆上 new 出来的对象),只能被一个 unique_ptr 拥有。你不能拷贝它,只能移动它。

我刚开始用 C++ 智能指针时,其实有点不习惯。以前写代码,new 了就得惦记着 delete,生怕忘了。有了 unique_ptr,你只管 new,它自动帮你 delete。嗯,就是这么省心。

核心原则:unique_ptr 的拷贝构造函数和拷贝赋值运算符是被删除的。你无法写出这样的代码:

std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = p1;  // 编译错误!

为什么会这样?因为如果允许拷贝,那就意味着两个 unique_ptr 指向同一块内存,那到底谁负责释放?这就乱套了。所以 C++ 标准委员会干脆一刀切:不能拷贝,只能移动。

基本用法:从创建到释放

咱们先看最基础的创建方式:

#include <memory>

std::unique_ptr<int> ptr(new int(100));
std::cout << *ptr << std::endl;  // 输出 100

这里 ptr 管理着一个 int 对象。当 ptr 离开作用域时,它会自动 delete 这个 int。你不需要写任何 delete 语句。

但说实话,直接用 new 来构造 unique_ptr,我个人不太推荐。为什么?因为如果构造函数抛出异常,可能会造成内存泄漏。更好的做法是用 make_unique。

make_unique:更安全、更简洁

C++14 引入了 std::make_unique。它是个工厂函数,帮你创建 unique_ptr。用法如下:

auto ptr = std::make_unique<int>(100);
// 等价于 std::unique_ptr<int> ptr(new int(100));

用 make_unique 的好处有三个:

  • 异常安全:不会因为参数求值顺序导致内存泄漏
  • 代码更短:不用写 new,也不用写类型名两次
  • 语义更清晰:一看就知道是在创建独占指针

我在项目中遇到过一个问题:有个同事用 new 创建 unique_ptr,结果构造函数抛异常,内存就泄漏了。后来全组统一改用 make_unique,再没出过类似问题。

小技巧:如果你用的是 C++11,没有 make_unique 怎么办?自己写一个!网上有很多实现,几行代码的事。或者直接用 new,但要确保构造函数不会抛异常。

移动语义:所有权转移

unique_ptr 虽然不能拷贝,但可以移动。移动意味着所有权从一个指针转移到另一个指针。原来的指针变成空。

auto p1 = std::make_unique<int>(42);
auto p2 = std::move(p1);  // p1 现在为空,p2 拥有资源

if (!p1) {
    std::cout << "p1 是空的" << std::endl;
}
std::cout << *p2 << std::endl;  // 输出 42

你想想看,这就像一把钥匙。你把它交给别人,自己手里就没有了。这就是独占所有权的精髓。

作为函数参数和返回值

unique_ptr 经常被用作函数参数和返回值。这里有几个常见模式:

作为返回值:工厂函数

std::unique_ptr<MyClass> createMyClass() {
    return std::make_unique<MyClass>();
}

auto obj = createMyClass();  // 所有权转移到 obj

这种模式很常见。函数内部创建对象,然后把所有权交给调用者。调用者负责管理对象的生命周期。

作为参数:传递所有权

void takeOwnership(std::unique_ptr<MyClass> ptr) {
    // 函数内部拥有 ptr 的所有权
    ptr->doSomething();
    // 函数结束时,ptr 自动释放
}

auto obj = std::make_unique<MyClass>();
takeOwnership(std::move(obj));  // 所有权转移给函数
// 此时 obj 为空

我曾经在重构一个老项目时,发现很多地方用裸指针传递所有权,结果到处是内存泄漏。改成 unique_ptr 后,代码清晰多了,谁拥有资源一目了然。

自定义删除器

unique_ptr 默认用 delete 释放资源。但有时候你需要自定义删除行为,比如释放文件句柄、关闭 socket 等。这时候可以传入自定义删除器:

auto fileDeleter = [](FILE* fp) {
    if (fp) {
        fclose(fp);
        std::cout << "文件已关闭" << std::endl;
    }
};

std::unique_ptr<FILE, decltype(fileDeleter)> 
    filePtr(fopen("test.txt", "r"), fileDeleter);

注意,自定义删除器会影响 unique_ptr 的大小。默认情况下,unique_ptr 和裸指针一样大(8 字节)。但如果用了函数对象作为删除器,可能会变大。无状态的 lambda 不会增加大小,但函数指针会。

注意:自定义删除器的类型是 unique_ptr 模板参数的一部分。所以两个删除器类型不同的 unique_ptr,即使管理同一类型资源,也是不同的类型,不能互相赋值。

知识结构图

下面这张图总结了 unique_ptr 的核心知识点:

unique_ptr 知识结构 unique_ptr 独占所有权模型 基本用法:创建、访问、释放 make_unique:安全创建 移动语义:所有权转移 自定义删除器 函数参数与返回值

避坑指南

最后,分享几个我踩过的坑:

  • 不要用 unique_ptr 管理数组:默认的 unique_ptr 用 delete 而不是 delete[]。如果你要管理数组,用 std::unique_ptr<int[]> 或者直接用 vector。
  • 不要混用裸指针和 unique_ptr:如果你把一个裸指针传给 unique_ptr,然后又用裸指针 delete 它,就会 double free。记住:资源只能有一个所有者。
  • 不要在容器里存 unique_ptr 的引用:容器要求元素可拷贝,但 unique_ptr 不能拷贝。如果你需要容器管理独占资源,用 std::vector<std::unique_ptr<T>> 是可以的,因为移动操作是允许的。

我曾经在一个项目里,看到有人把 unique_ptr 的引用存到 vector 里,然后程序崩溃得一塌糊涂。后来改成直接存 unique_ptr 的值,问题就解决了。

好了,unique_ptr 的上半部分就讲到这里。记住它的核心:独占、移动、安全。下节课咱们继续聊 unique_ptr 的高级用法和性能优化。


公众号:蓝海资料掘金营,微信deep3321