11. 自定义删除器:为智能指针定制资源释放行为
智能指针的默认删除行为,说白了就是调用 delete 或 delete[]。但现实中的资源远不止堆内存——文件句柄、socket 连接、数据库游标、甚至是你自己写的某种池化对象。这时候,默认的删除器就不够用了。
我最早遇到这个问题是在做一个网络库的时候。当时用 shared_ptr 管理 socket 描述符,结果程序退出时直接调了 delete,而不是 close()。嗯,那场面挺尴尬的。从那以后,我就养成了一个习惯:只要资源不是 new 出来的,就一定带上自定义删除器。
为什么需要自定义删除器?
你想想看,unique_ptr 和 shared_ptr 的析构函数里,默认执行的是 delete ptr。但如果你管理的是:
- 通过
fopen()打开的文件 → 需要fclose() - 通过
malloc()分配的内存 → 需要free() - 操作系统句柄 → 需要
CloseHandle() - 自定义对象池中的资源 → 需要归还到池中
这些场景下,默认删除器就是一颗定时炸弹。自定义删除器的作用,就是告诉智能指针:「嘿,别用默认方式,按我的规则来释放。」
核心原则:智能指针只负责「所有权」和「生命周期」,至于「怎么释放」,那是删除器的事。
自定义删除器的三种写法
我个人习惯把删除器的写法分成三类。咱们一个一个来看。
1. 函数指针 / 普通函数
最直接的方式。适合删除逻辑简单、不需要状态的场景。
// 自定义删除函数
void CloseFile(FILE* fp) {
if (fp) {
fclose(fp);
std::cout << "文件已关闭\n";
}
}
int main() {
// unique_ptr 使用函数指针作为删除器
std::unique_ptr<FILE, decltype(&CloseFile)>
up(fopen("test.txt", "w"), CloseFile);
// shared_ptr 同样支持
std::shared_ptr<FILE> sp(fopen("test.txt", "w"), CloseFile);
return 0;
}
注意 unique_ptr 的模板第二个参数必须显式指定删除器类型。而 shared_ptr 的删除器是类型擦除的,你只需要在构造函数里传进去就行。这也是 shared_ptr 比 unique_ptr 灵活的一个地方——代价是多了虚函数调用的开销。
2. 函数对象(仿函数)
当删除逻辑需要携带一些状态时,函数对象就派上用场了。我在项目中经常用这种方式来记录资源释放的日志。
struct FileDeleter {
// 可以携带状态
bool verbose = true;
void operator()(FILE* fp) {
if (fp) {
fclose(fp);
if (verbose) {
std::cout << "[日志] 文件资源已释放\n";
}
}
}
};
int main() {
// 使用函数对象作为删除器
std::unique_ptr<FILE, FileDeleter> up(
fopen("test.txt", "w"),
FileDeleter{true} // 可以传参
);
// shared_ptr 同样支持
std::shared_ptr<FILE> sp(
fopen("test.txt", "w"),
FileDeleter{false} // 关闭日志
);
return 0;
}
函数对象的优势在于:你可以把删除器设计得跟普通类一样,有成员变量、有构造函数、甚至可以有虚函数。说白了,它就是一个小型的策略类。
3. Lambda 表达式
这是我最推荐的方式。简洁、直观、而且作用域封闭。你想想看,如果删除逻辑只有一两行,专门写个函数或类是不是有点小题大做?
int main() {
// 使用 lambda 作为删除器
auto file_deleter = [](FILE* fp) {
if (fp) {
fclose(fp);
std::cout << "lambda 删除器:文件已关闭\n";
}
};
// unique_ptr 需要 decltype 推导 lambda 类型
std::unique_ptr<FILE, decltype(file_deleter)>
up(fopen("test.txt", "w"), file_deleter);
// shared_ptr 直接传 lambda
std::shared_ptr<FILE> sp(
fopen("test.txt", "w"),
[](FILE* fp) {
if (fp) fclose(fp);
}
);
return 0;
}
我的建议:如果删除器逻辑不超过 5 行,优先用 lambda。如果逻辑复杂或需要复用,用函数对象。函数指针的方式我基本只在 C 风格接口兼容时用。
unique_ptr 与 shared_ptr 的删除器差异
这里有个重要的区别,很多初学者会搞混。我专门画了张图来说明:
简单总结一下:
| 特性 | unique_ptr | shared_ptr |
|---|---|---|
| 删除器类型 | 模板参数,编译期确定 | 类型擦除,运行时确定 |
| 对指针大小的影响 | 有状态删除器会增加大小 | 始终不变(2个指针大小) |
| 性能开销 | 零开销抽象(无状态时) | 一次虚函数调用 |
| 灵活性 | 较低(类型固定) | 较高(运行时可换) |
我曾经踩过的坑:用 unique_ptr 配合 lambda 删除器时,如果 lambda 捕获了变量,删除器就变成了有状态对象。这时候 unique_ptr 的大小会从 1 个指针变成多个指针的大小。如果你在嵌入式系统或对内存布局敏感的场景下,一定要留意这一点。
实战:管理 POSIX 文件描述符
咱们来看一个完整的例子。假设你要管理 POSIX 系统的文件描述符(不是 FILE*,而是 int 类型的 fd)。
// 文件描述符的删除器
struct FdDeleter {
void operator()(int* fd) {
if (fd && *fd >= 0) {
close(*fd);
std::cout << "fd " << *fd << " 已关闭\n";
delete fd; // 别忘了释放 int 本身
}
}
};
// 更优雅的方式:用 unique_ptr 管理 int
using unique_fd = std::unique_ptr<int, FdDeleter>;
unique_fd make_fd(const char* path, int flags) {
int raw_fd = open(path, flags);
if (raw_fd < 0) {
return nullptr;
}
// 注意:这里 new 了一个 int 来存储 fd
return unique_fd(new int(raw_fd));
}
int main() {
auto fd = make_fd("/tmp/test.txt", O_CREAT | O_RDWR);
if (fd) {
write(*fd, "hello", 5);
// 离开作用域时自动 close
}
return 0;
}
嗯,这里有个细节要注意:unique_ptr 管理的是堆上的对象,所以我们需要 new int(fd)。如果你觉得这样有点别扭,也可以用 shared_ptr 配合自定义删除器,直接管理原始 fd:
// shared_ptr 可以直接管理非指针资源
void close_fd(int* fd) {
if (fd && *fd >= 0) {
close(*fd);
}
}
// 但更常见的做法是直接传 int
std::shared_ptr<int> sp_fd(
new int(open("test.txt", O_RDWR)),
[](int* p) {
if (p && *p >= 0) close(*p);
delete p;
}
);
删除器的组合与复用
当项目中有多种资源需要管理时,我习惯把删除器定义成可复用的组件。比如:
// 通用删除器工厂
struct ResourceDeleter {
using func_type = void(*)(void*);
func_type cleanup;
void* context;
template <typename T>
void operator()(T* ptr) {
if (ptr) {
cleanup(ptr);
delete ptr;
}
}
};
// 预定义的删除器
inline constexpr auto file_deleter = [](FILE* fp) {
if (fp) fclose(fp);
};
inline constexpr auto socket_deleter = [](int* sock) {
if (sock && *sock >= 0) close(*sock);
};
// 使用时
std::unique_ptr<FILE, decltype(file_deleter)>
file_ptr(fopen("a.txt", "r"), file_deleter);
我的习惯:在项目的公共头文件里,把常用的删除器定义成 inline constexpr 的 lambda 或函数对象。这样团队里所有人都能直接复用,不会出现「每个人写一套自己的删除器」的情况。
性能考量
最后聊一下性能。很多人担心自定义删除器会带来额外开销。其实:
- unique_ptr + 无状态删除器:零开销。删除器不占空间,调用是直接函数调用。
- unique_ptr + 有状态删除器:增加指针大小,但调用仍然是直接调用。
- shared_ptr + 自定义删除器:增加一次间接调用(通过虚函数或函数指针),但通常可以忽略不计。
说白了,除非你在写实时系统或每秒百万次分配释放的场景,否则不用太担心这点开销。正确性和可维护性远比那几纳秒重要。
好了,自定义删除器就讲到这里。记住一句话:智能指针管理的是「生命周期」,删除器定义的是「释放方式」。把这两件事分开想,你的代码会清晰很多。
核心要点回顾:
- 自定义删除器用于管理非堆内存资源
- 三种写法:函数指针、函数对象、lambda
- unique_ptr 的删除器是类型的一部分,shared_ptr 是类型擦除的
- 优先用 lambda,复杂逻辑用函数对象
- 把常用删除器做成公共组件,方便复用