在C++系统编程里,文件描述符是整数类型的句柄,由操作系统内核分配,必须通过close系统调用归还。它和堆内存不同,没有构造函数与析构函数,因此普通的new与delete机制完全派不上用场。很多开发者尝试用智能指针管理这类资源时,第一反应是能不能直接把int放进unique_ptr,答案是否定的,因为默认删除器会调用delete,对整数做delete是未定义行为。标准库给出的解法是自定义删除器,让智能指针在销毁时执行我们指定的释放逻辑。

为什么默认智能指针不能管理文件描述符
unique_ptr与shared_ptr的默认删除器针对的是通过new分配的对象,底层调用delete或delete[]释放内存。文件描述符本质是一个int,由open、socket等系统调用获得,释放必须调用close。如果把int当作普通指针对象交给默认删除器,不仅不会关闭描述符,还可能因错误释放内存导致程序崩溃。这种资源与内存的生命周期管理方式完全不同,被称为非内存资源。
另一个容易被忽视的问题是,文件描述符属于进程级有限资源,Linux默认单进程上限往往只有1024或4096。若因为异常分支忘记close,短时间内大量创建socket或打开文件就会触发EMFILE错误,使服务不可用。因此用确定性的RAII机制托管描述符,比在每段逻辑里手写close更可靠,也更容易审查。
unique_ptr的自定义删除器用法
unique_ptr的删除器是类型的一部分,需要作为第二个模板参数传入。我们可以定义一个可调用对象,接受int参数并调用close。由于删除器类型参与unique_ptr类型推导,不同的删除器会导致unique_ptr类型不兼容,但这也带来了零开销抽象,删除器通常能被编译器内联。
下面示例展示如何用unique_ptr管理通过open获得的文件描述符,并在作用域结束时自动关闭:
#include <memory>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
// 自定义删除器,接受int类型的文件描述符
struct FdDeleter {
void operator()(int fd) const {
if (fd >= 0) {
close(fd);
std::cout << "fd closed: " << fd << std::endl;
}
}
};
// unique_ptr的第二个模板参数是删除器类型
using UniqueFd = std::unique_ptr<int, FdDeleter>;
UniqueFd open_file(const char* path) {
int fd = open(path, O_RDONLY);
if (fd < 0) {
return UniqueFd(nullptr, FdDeleter{});
}
// 用fd的地址构造,管理这个整数
return UniqueFd(new int(fd), FdDeleter{});
}
int main() {
UniqueFd fd = open_file("/tmp/test.txt");
if (fd && *fd >= 0) {
// 通过*fd访问真实的文件描述符
read(*fd, nullptr, 0);
}
// 离开作用域自动调用FdDeleter关闭描述符
return 0;
}
上面的代码用new int(fd)保存描述符值,虽然多一次堆分配,但能直接复用unique_ptr的接口。如果不想分配堆内存,也可以把int放进包装结构体,再让删除器从该结构体取出fd。无论哪种方式,重点在于删除器类型明确,编译器能在编译期确定释放动作。
使用unique_ptr管理描述符时,要注意不能把同一个fd赋给两个unique_ptr,否则会double close。转移所有权必须用std::move,且原指针会变为空。对于需要共享描述符的场景,unique_ptr并不合适,此时应考虑shared_ptr。
shared_ptr的擦除删除器方案
shared_ptr的删除器不属于类型的一部分,它在构造时保存删除器,并在引用计数归零时调用,这种机制称为类型擦除。这意味着不同删除方式的shared_ptr可以放在同一个容器里,例如vector<shared_ptr<int>>既能存内存指针也能存文件描述符,只要构造时传入对应删除器。
下面的例子演示shared_ptr如何用lambda作为自定义删除器托管socket描述符:
#include <memory>
#include <unistd.h>
#include <sys/socket.h>
#include <iostream>
std::shared_ptr<int> make_shared_fd(int fd) {
// lambda作为删除器,类型不写入shared_ptr模板参数
return std::shared_ptr<int>(
new int(fd),
[](int* p) {
if (p && *p >= 0) {
close(*p);
std::cout << "shared fd closed" << std::endl;
}
delete p;
}
);
}
void use_fd(std::shared_ptr<int> fd) {
if (fd && *fd >= 0) {
send(*fd, "ping", 4, 0);
}
}
int main() {
auto fd = make_shared_fd(socket(AF_INET, SOCK_STREAM, 0));
use_fd(fd);
// 引用计数为0时自动close并delete
return 0;
}
shared_ptr的删除器在控制块中分配,带来极小的额外开销,但换来了灵活的所有权模型。多个模块可以安全地持有同一个fd的shared_ptr,不用担心谁最后负责关闭。不过也正因如此,要小心循环引用,虽然文件描述符本身不涉及循环,但包含它的对象图可能产生shared_ptr环。
当使用shared_ptr管理非内存资源时,建议把资源包装成小而简单的结构体,并在删除器里只做close这类系统调用,不要把复杂业务逻辑塞进删除器,否则会让资源释放顺序变得难以追踪。
自定义删除器封装系统资源的通用模式
除了文件描述符,诸如互斥锁、数据库连接、GPU句柄等都可以用同样的思路封装。核心步骤只有三步:明确资源获取函数、明确资源释放函数、把释放函数写成删除器并绑定到智能指针。这样所有提前返回、异常抛出都不会漏掉释放动作。
我们可以用一张表对比两种智能指针在管理文件描述符时的差异:
| 特性 | unique_ptr | shared_ptr |
|---|---|---|
| 删除器是否影响类型 | 是,必须写进模板参数 | 否,类型擦除 |
| 所有权模型 | 独占 | 共享引用计数 |
| 额外开销 | 几乎无 | 控制块与原子计数 |
| 适用场景 | 单一作用域或明确转移 | 多模块共享生命周期 |
在实际工程中,如果仅仅是在一个函数内打开文件并读取,用unique_ptr加自定义删除器最轻量;如果描述符要传给异步回调或者放到全局对象池,shared_ptr更安全。无论哪种,都比在代码里散落close调用要清晰得多。
常见误区与注意事项
一个典型误区是认为智能指针只能管理new出来的内存,从而放弃用它托管fd。实际上只要提供正确的删除器,任何需要配对申请与释放的资源都能托管。另一个误区是在删除器里抛出异常,close等系统调用失败时应记录日志而不是直接抛,因为析构路径中抛异常会导致程序终止。
还要注意文件描述符在fork之后的行为,子进程会继承父进程的描述符,如果用shared_ptr管理,父子进程的引用计数相互独立,可能一方关闭后另一方仍在用。这类跨进程资源问题不是智能指针能解决的,需要配合dup或Unix域套接字传递句柄。智能指针解决的是单进程内的确定释放,别把它当成进程间资源管理工具。
总结来说,智能指针完全可以用于管理文件描述符,关键在于提供调用close的自定义删除器。根据是否需要共享所有权,选择unique_ptr或shared_ptr,并把资源获取与释放严格配对,就能写出更安全的系统级C++代码。
smart_pointerfile_descriptorcustom_deleter修改时间:2026-07-31 19:24:35