C++智能指针的默认释放动作是调用delete或delete[],这对于直接由new创建的对象没有问题。但如果资源来自fopen、malloc、socket或者某个第三方库的分配函数,就必须使用对应的释放函数。自定义删除器就是用来指定智能指针析构时执行哪一段清理逻辑。
自定义删除器在unique_ptr和shared_ptr中的实现思路并不相同:前者把删除器类型作为模板参数的一部分,后者则把删除器放入控制块,做到运行时类型擦除。本文通过多个可编译示例展示常见写法,并分析它们对对象大小、灵活性和异常安全的影响。

为什么默认删除器不够用
标准库为std::unique_ptr<T>提供了默认删除器std::default_delete<T>,它内部只做一件事:delete ptr。如果T是数组类型,则特化版本会调用delete[] ptr。这意味着std::unique_ptr<FILE>在离开作用域时会执行delete file,而不是fclose。FILE指针一般来自C标准库的fopen,用delete释放属于未定义行为。
类似问题也出现在Windows编程中。例如HANDLE通常通过CloseHandle关闭,HMODULE通过FreeLibrary卸载,socket描述符通过close或closesocket关闭。如果把这类资源直接交给智能指针默认管理,要么编译不过,要么运行期崩溃。自定义删除器可以统一管理这些资源的生命周期,不需要手写每个类的析构函数。
另一个典型场景是自定义内存池。池中的对象不能简单用delete,而需要调用池提供的归还接口。把归还动作交给删除器后,调用方就可以像使用普通智能指针一样安全地使用池对象。删除器的引入让智能指针从内存管理器扩展为通用的RAII资源管理器。
unique_ptr自定义删除器的三种实现
std::unique_ptr的第二个模板参数是删除器类型,默认是std::default_delete<T>。当需要自定义删除器时,通常有三种写法:函数指针、lambda表达式和函数对象。三者都可以让unique_ptr在析构时执行指定清理动作,但对象大小和调用开销不同。
#include <cstdio>
#include <memory>
struct FileCloser {
void operator()(FILE* f) const noexcept {
if (f) {
std::fclose(f);
}
}
};
int main() {
std::unique_ptr<FILE, FileCloser> file(std::fopen("data.txt", "r"));
return 0;
}
上面的函数对象FileCloser不包含任何成员变量,并且在unique_ptr内部通常可以享受空基类优化,因此不会增大unique_ptr的体积。如果删除器是有状态的,例如需要记录日志或统计释放次数,那么这些状态会直接存储在unique_ptr对象中。
函数指针写法最直接,但会增加一个指针的存储开销:
std::unique_ptr<FILE, decltype(&std::fclose)> file(
std::fopen("data.txt", "r"),
&std::fclose
);
这里decltype(&std::fclose)推导出函数指针类型,删除器作为第二个构造参数传入。由于函数指针无法携带额外状态,灵活性较低。无捕获lambda可以转换为函数指针,但作为删除器类型时仍会退化成类似函数指针的大小;有捕获lambda则会把捕获变量保存到对象中。通常情况下,函数对象是最推荐的选择,既没有额外运行期开销,又能自由携带状态。
对于数组资源,std::unique_ptr提供了T[]特化版本,默认调用delete[]。如果要用自定义释放函数,注意删除器的参数仍然是元素指针。例如:
auto arrayDeleter = [](int* p) { delete[] p; };
std::unique_ptr<int[], decltype(arrayDeleter)> arr(new int[10], arrayDeleter);
arr[0] = 42;
该写法与普通对象类似,但访问元素时可以直接使用operator[]。如果仍然写成std::unique_ptr<int>却传入new int[10],删除器就会被错误调用,这是常见的坑。
shared_ptr的删除器机制与类型擦除
与unique_ptr不同,std::shared_ptr的删除器不是模板类型参数,而是保存在控制块里的可调用对象。因此shared_ptr<FILE>无论使用何种删除器,类型都相同。这让多个资源指针可以放入同一个容器,也允许在运行期动态决定释放策略。
std::shared_ptr<FILE> file(
std::fopen("data.txt", "r"),
[](FILE* f) {
if (f) {
std::fclose(f);
}
}
);
删除器随控制块一起被引用计数管理,即使某个shared_ptr副本先析构,控制块也不会提前释放,最终最后一个shared_ptr销毁时才会调用删除器。这种类型擦除的代价是控制块分配和额外函数调用,但换来的是更高的设计灵活性。例如可以在不改变接口的情况下,让同一个shared_ptr<T>类型分别管理来自不同分配器的资源。
shared_ptr管理数组时,需要主动使用delete[],否则默认仍是delete。C++17引入了std::shared_ptr<T[]>,配合操作符[]访问元素,行为更清晰:
std::shared_ptr<int[]> sp(
new int[10],
[](int* p) { delete[] p; }
);
sp[0] = 1;
注意C++17之前的shared_ptr没有数组特化,operator*和operator->对数组意义不大,只能通过get()获取原始指针再索引。如果代码库需要兼容C++14及更早版本,建议用std::vector代替动态数组,再配合智能指针。
另外,shared_ptr的删除器不会改变shared_ptr对象自身的大小,始终是控制块指针加数据指针的两倍大小。如果追求极致性能和内存占用,unique_ptr加函数对象是更好的选择;如果需要容器存储、跨模块传递或运行期绑定释放逻辑,shared_ptr更加合适。
实战场景与常见避坑点
自定义删除器的典型应用之一是封装C风格文件接口。假如一个类需要打开多个文件,传统做法是手动在析构函数里循环关闭,容易漏掉异常路径。用unique_ptr管理FILE句柄后,析构自动完成,代码更简洁:
#include <cstdio>
#include <memory>
#include <vector>
struct FileCloser {
void operator()(FILE* f) const noexcept {
if (f) std::fclose(f);
}
};
int main() {
std::vector<std::unique_ptr<FILE, FileCloser>> files;
files.emplace_back(std::fopen("a.txt", "r"));
files.emplace_back(std::fopen("b.txt", "r"));
// 不需要手动 fclose
return 0;
}
这里std::vector存储unique_ptr<FILE, FileCloser>,每个元素析构时会自动检查并关闭文件。即使中途抛出异常,已构造的文件句柄也会被正确释放。注意不要在删除器中吃掉异常,也不要删除空指针前不判断:大多数C标准库函数对空指针不友好,增加判空是必要的。
另一个高频场景是管理动态库句柄。Windows下HMODULE通过FreeLibrary卸载,Linux下dlopen返回的void*通过dlclose释放。可以分别定义平台相关的删除器:
#ifdef _WIN32
#include <windows.h>
using ModulePtr = std::unique_ptr<HMODULE, decltype(&FreeLibrary)>;
inline ModulePtr make_module(const char* name) {
HMODULE h = LoadLibraryA(name);
return ModulePtr(h, &FreeLibrary);
}
#else
#include <dlfcn.h>
using ModulePtr = std::unique_ptr<void, decltype(&dlclose)>;
inline ModulePtr make_module(const char* name) {
void* h = dlopen(name, RTLD_LAZY);
return ModulePtr(h, &dlclose);
}
#endif
这个例子同时展示了如何把平台差异封装在工厂函数中,对外只暴露统一的ModulePtr。如果删除器可能为空或平台函数有特定调用约定,最好再包一层函数对象,避免依赖原始函数指针。
常见注意事项:删除器应尽量为noexcept,因为析构函数默认不应抛出异常;unique_ptr自定义删除器后类型会随删除器改变,跨接口传递时可能带来签名变化;shared_ptr虽然类型稳定,但需要注意循环引用和线程安全。此外不要用std::function作为unique_ptr的删除器类型,因为它会额外引入堆分配和间接调用,除非确实需要运行期更换删除器。
C++智能指针自定义删除器shared_ptr修改时间:2026-09-18 02:51:09