5. unique_ptr(下):自定义删除器,与容器配合使用

上一章我们聊了 unique_ptr 的基本用法,说白了就是「独占所有权,自动释放」。但实际项目中,哪有那么理想的情况?你总会遇到一些「非标准」的资源释放方式,或者想把 unique_ptr 塞进容器里。嗯,今天我们就来解决这两个问题。

5.1 自定义删除器:不止是 delete

unique_ptr 默认用 delete 释放资源,这没问题。但我在项目中遇到过不少场景,资源根本不是 new 出来的——比如文件句柄、socket、或者从 C 库函数返回的指针。这时候默认的 delete 就不管用了。

举个例子,我记得有个项目用了第三方 C 库,它分配内存用的是 malloc,释放得用 free。如果你直接用 unique_ptr 管理,析构时调用 delete,那就会出大问题——未定义行为,说白了就是程序可能随时崩。

解决方案很简单:给 unique_ptr 传一个自定义删除器。

#include <memory>
#include <cstdio>

// 自定义删除器:用 free 释放
struct FreeDeleter {
    void operator()(void* p) const {
        if (p) {
            std::printf("自定义删除器:调用 free\n");
            std::free(p);
        }
    }
};

int main() {
    // 用 malloc 分配,用 FreeDeleter 释放
    std::unique_ptr<int, FreeDeleter> ptr(
        static_cast<int*>(std::malloc(sizeof(int)))
    );
    *ptr = 42;
    // 离开作用域时,自动调用 FreeDeleter::operator()
    return 0;
}

你可能会问:删除器类型必须写在模板参数里吗?是的,unique_ptr 的第二个模板参数就是删除器类型。这意味着,如果你用了 lambda 作为删除器,类型就得写成 decltype(lambda),有点啰嗦。不过 C++17 之后,你可以用 std::function 或者直接写 lambda 配合 auto 推导,但要注意性能开销。

我的习惯:如果删除器逻辑简单,我倾向于写一个函数对象(仿函数),就像上面的 FreeDeleter。这样类型明确,而且编译器容易内联,性能更好。

5.2 文件句柄管理:一个实战案例

文件操作是自定义删除器的经典场景。C 标准库的 fopen 返回 FILE*,关闭得用 fclose。如果你忘了调用 fclose,文件描述符就会泄漏。用 unique_ptr 包装一下,就再也不用担心了。

#include <memory>
#include <cstdio>

struct FileCloser {
    void operator()(std::FILE* fp) const {
        if (fp) {
            std::fclose(fp);
            std::printf("文件已关闭\n");
        }
    }
};

using FilePtr = std::unique_ptr<std::FILE, FileCloser>;

FilePtr openFile(const char* filename, const char* mode) {
    std::FILE* fp = std::fopen(filename, mode);
    if (!fp) {
        // 打开失败,返回空 unique_ptr
        return FilePtr(nullptr);
    }
    return FilePtr(fp);
}

int main() {
    auto file = openFile("test.txt", "w");
    if (file) {
        std::fprintf(file.get(), "Hello, unique_ptr!\n");
        // 离开作用域时自动 fclose
    }
    return 0;
}

你看,代码变得干净多了。而且 unique_ptr 的 get() 方法可以拿到原始指针,方便传给 C 函数。我曾经在一个日志系统里用了这个模式,再也没出现过文件句柄泄漏的问题。

注意:自定义删除器不能抛出异常。如果删除器抛异常,而 unique_ptr 又在析构函数中调用它,程序会直接 terminate。所以,删除器里别做太复杂的事,尤其是不要抛异常。

5.3 unique_ptr 与容器:存储独占所有权

unique_ptr 不能拷贝,那怎么放进容器里?比如 vector<unique_ptr<int>>,你没法直接 push_back 一个临时对象?其实可以,用移动语义就行。

#include <memory>
#include <vector>
#include <iostream>

int main() {
    std::vector<std::unique_ptr<int>> vec;

    // 方式1:直接 push_back 右值
    vec.push_back(std::make_unique<int>(10));
    vec.push_back(std::make_unique<int>(20));

    // 方式2:用 std::move 转移所有权
    auto ptr = std::make_unique<int>(30);
    vec.push_back(std::move(ptr));  // 此时 ptr 变为空

    // 遍历容器
    for (const auto& p : vec) {
        if (p) {
            std::cout << *p << std::endl;
        }
    }

    // 注意:不能拷贝,只能移动
    // std::unique_ptr<int> copy = vec[0];  // 编译错误!
    std::unique_ptr<int> moved = std::move(vec[0]);  // 可以

    return 0;
}

为什么要把 unique_ptr 放进容器?我个人经验是:当对象生命周期复杂,或者需要多态时,unique_ptr 容器非常有用。比如你有一个基类 Shape,派生类 CircleRectangle,用 vector<unique_ptr<Shape>> 就能轻松管理一堆不同形状的对象。

5.4 容器中的自定义删除器

如果 unique_ptr 用了自定义删除器,放进容器时要注意:所有元素的删除器类型必须相同。因为容器要求元素类型一致。

#include <memory>
#include <vector>

struct MyDeleter {
    void operator()(int* p) const { delete p; }
};

int main() {
    // 所有 unique_ptr 的删除器类型必须一致
    std::vector<std::unique_ptr<int, MyDeleter>> vec;
    vec.push_back(std::unique_ptr<int, MyDeleter>(new int(1)));
    vec.push_back(std::unique_ptr<int, MyDeleter>(new int(2)));
    return 0;
}

如果你想让删除器不同,可以用 std::function 包装,但会带来额外开销。我一般不建议这么做——除非你真的需要运行时多态的删除行为。

5.5 核心逻辑图:unique_ptr 与删除器、容器的关系

下面这张图帮你理清本章的知识结构:

unique_ptr 自定义删除器与容器配合 unique_ptr<T, Deleter> 自定义删除器 函数对象 lambda 容器配合 移动语义 类型一致 核心要点 删除器不抛异常 | 容器元素只能移动 | 类型统一

5.6 避坑指南:我踩过的几个坑

讲几个我实际项目中遇到的教训,希望能帮你少走弯路。

  • 删除器类型不匹配:我曾经把两个不同删除器类型的 unique_ptr 放进同一个 vector,编译报错半天没反应过来。记住:容器要求元素类型完全一致,包括删除器类型。
  • 忘记移动语义:刚开始用 unique_ptr 时,我习惯性地写 vec.push_back(ptr),结果编译失败。后来才意识到要写 std::move(ptr)。嗯,这个习惯得改。
  • 删除器里抛异常:有一次我在删除器里写了日志,日志系统初始化失败抛了个异常。程序直接崩了,查了好久才发现是 unique_ptr 析构时 terminate 了。从那以后,我所有删除器都保证 noexcept。
  • 用 get() 后手动 delete:unique_ptr 的 get() 返回原始指针,但你不能手动 delete 它——unique_ptr 析构时会再次 delete,造成 double free。我见过同事犯这个错,调试了一整天。

总结一下:自定义删除器让 unique_ptr 能管理各种资源,不只是堆内存。容器配合移动语义,让独占所有权也能批量管理。记住三点:删除器不抛异常、容器元素只能移动、类型要统一。做到这三点,unique_ptr 用起来就稳了。


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