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,派生类 Circle、Rectangle,用 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 与删除器、容器的关系
下面这张图帮你理清本章的知识结构:
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