12. unique_ptr:自定义删除器、数组支持
unique_ptr 可以说是现代 C++ 里我最常用的智能指针了。它轻量、高效,而且语义清晰——「独占所有权」。但很多人用着用着就发现,有些场景下默认的 delete 不够用,或者需要管理数组。今天我们就来聊聊这两个进阶话题。
为什么需要自定义删除器?
默认情况下,unique_ptr 析构时调用 delete。但现实世界没那么简单。我在项目中遇到过好几次这样的情况:
- 对象是通过
malloc分配的,得用free释放 - 资源来自 C 库的
fopen,需要fclose - 自定义内存池分配的对象,需要归还给池子
这时候,自定义删除器就派上用场了。
自定义删除器的基本用法
删除器可以是一个函数指针、一个 lambda,或者一个函数对象。我个人习惯用 lambda,因为代码更紧凑。
// 方式一:函数指针
void close_file(FILE* fp) {
if (fp) fclose(fp);
}
std::unique_ptr<FILE, decltype(&close_file)>
file_ptr(fopen("test.txt", "r"), close_file);
// 方式二:lambda(推荐)
auto file_deleter = [](FILE* fp) {
if (fp) {
fclose(fp);
std::cout << "文件已关闭\n";
}
};
std::unique_ptr<FILE, decltype(file_deleter)>
file_ptr2(fopen("test.txt", "r"), file_deleter);
// 方式三:函数对象
struct FreeDeleter {
void operator()(void* p) const {
free(p);
std::cout << "内存已释放\n";
}
};
std::unique_ptr<int, FreeDeleter>
int_ptr(static_cast<int*>(malloc(sizeof(int))));
删除器类型对 unique_ptr 大小的影响
嗯,这里有个容易被忽略的点——删除器的类型会影响 unique_ptr 的大小。
| 删除器类型 | unique_ptr 大小 | 说明 |
|---|---|---|
| 默认删除器(default_delete) | 8 字节(64位) | 无额外开销 |
| 无状态函数对象(如 lambda 无捕获) | 8 字节 | 空基类优化(EBO)生效 |
| 有状态函数对象(如 lambda 捕获了变量) | 8 + 捕获变量大小 | 额外存储捕获的数据 |
| 函数指针 | 16 字节 | 指针本身占 8 字节 |
为什么会这样?因为 unique_ptr 内部把删除器作为成员存储。无状态的函数对象可以通过空基类优化压缩掉,但函数指针不行——它实实在在占 8 个字节。
数组支持:unique_ptr<T[]>
很多人不知道,unique_ptr 对数组有专门的支持。默认的 unique_ptr<T> 调用 delete,而 unique_ptr<T[]> 调用 delete[]。这两者不能混用。
// 错误用法:用 delete 释放数组
std::unique_ptr<int> bad(new int[10]); // 未定义行为!
// 正确用法
std::unique_ptr<int[]> good(new int[10]);
// 访问元素
good[0] = 42; // 支持下标访问
good[1] = 100;
// 不支持 * 和 -> 操作符
// *good; // 编译错误
我记得有一次代码审查,看到同事用 unique_ptr<int> 管理数组,当时就提醒他改成了 unique_ptr<int[]>。这种错误很难在运行时发现,但后果可能是内存泄漏或崩溃。
数组版本的独特之处
unique_ptr<T[]> 和 unique_ptr<T> 有几个关键区别:
- 下标操作符:
ptr[i]直接可用,不用先解引用 - 没有 * 和 ->:因为数组不是单个对象
- 删除器默认是 delete[]:自动匹配
- 自定义删除器同样支持:但签名要匹配数组释放
// 数组 + 自定义删除器
struct ArrayDeleter {
void operator()(int* p) const {
std::cout << "释放数组,元素个数: "
<< _msize(p) / sizeof(int) << "\n";
delete[] p;
}
};
std::unique_ptr<int[], ArrayDeleter>
arr(new int[100]);
避坑指南:我曾经踩过的坑
我曾经在项目里犯过一个低级错误——用 unique_ptr<T> 管理通过 new T[N] 分配的内存。当时代码跑得好好的,直到某次压力测试,程序随机崩溃。查了两天才发现是 delete 和 delete[] 不匹配导致的堆损坏。
从那以后,我给自己定了个规矩:
- 看到
new T[N],立刻用unique_ptr<T[]> - 看到
malloc,立刻用自定义删除器调用free - 看到 C 库资源(文件、socket),立刻封装成 RAII 类或用 unique_ptr 管理
std::make_unique<T[]>(n) 的初始化列表语法。C++20 之前,你需要手动 new T[n]。C++20 引入了 std::make_unique<T[]>(n),但注意它会对元素进行值初始化。
知识体系图
下面这张图总结了 unique_ptr 在自定义删除器和数组支持方面的核心逻辑:
实际项目中的最佳实践
说了这么多,总结几条我在项目中实际遵循的原则:
- 优先用 make_unique:除非你需要自定义删除器或数组
- 数组一律用 unique_ptr<T[]>:别给自己挖坑
- 删除器用无捕获 lambda:性能好,代码也清晰
- 复杂资源管理考虑封装成 RAII 类:如果删除逻辑太复杂,不如写个专门的类
// 一个实际例子:管理 OpenGL 纹理
auto texture_deleter = [](GLuint* id) {
if (id) {
glDeleteTextures(1, id);
delete id;
}
};
std::unique_ptr<GLuint, decltype(texture_deleter)>
texture(new GLuint(0), texture_deleter);
glGenTextures(1, texture.get());
// 使用纹理...
// 离开作用域时自动释放
你看,自定义删除器让 unique_ptr 能管理各种资源,不仅仅是堆内存。这也是我为什么说 unique_ptr 是现代 C++ 资源管理的基石——它足够灵活,又足够安全。
template<typename T> using unique_ptr_free = std::unique_ptr<T, FreeDeleter>;这样用起来就跟普通 unique_ptr 一样方便了。
公众号:蓝海资料掘金营,微信deep3321