RAII 封装文件描述符需用自定义类而非 unique_ptr:Linux 用 int fd 构造并析构调 close(),禁拷贝、实现移动语义;Windows 用 HANDLE 封装,判 INVALID_HANDLE_VALUE、调 CloseHandle();均须默认设 O_CLOEXEC/!bInheritHandle 防句柄泄露。

用 RAII 封装 int 类型的文件描述符
Linux/macOS 下用 open() 返回的 int 文件描述符,本身没有析构行为,裸用极易泄漏。C++ 的解法不是加个“记得关”的注释,而是让关闭动作绑定到对象生命周期——即用 RAII(Resource Acquisition Is Initialization)自动管理。
核心思路:写一个轻量 wrapper 类,在构造时接管 fd,在析构时调用 close()。注意必须显式禁用拷贝(避免两个对象析构同一个 fd),移动语义则要转移所有权:
class FileDescriptor {
int fd_ = -1;
public:
explicit FileDescriptor(int fd) : fd_(fd) {}
~FileDescriptor() { if (fd_ != -1) close(fd_); }
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
FileDescriptor(FileDescriptor&& other) noexcept : fd_(other.fd_) {
other.fd_ = -1;
}
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
if (this != &other) {
if (fd_ != -1) close(fd_);
fd_ = other.fd_;
other.fd_ = -1;
}
return *this;
}
int get() const { return fd_; }
explicit operator bool() const { return fd_ != -1; }
};
这样 FileDescriptor fd(open("data.txt", O_RDONLY)); 离开作用域就自动关,不用手动调 close()。
Windows 上封装 HANDLE 要注意 CloseHandle() 和错误值
Windows 的 CreateFile() 返回 HANDLE,它本质是 void*,不能直接和 int 混用。关键点有三个:无效句柄是 INVALID_HANDLE_VALUE(不是 nullptr),关闭必须用 CloseHandle()(不是 close()),且失败时不设 errno 而要查 GetLastError()。
立即学习“C++免费学习笔记(深入)”;
封装时需适配这些差异:
- 构造函数检查是否等于
INVALID_HANDLE_VALUE,是则置为nullptr - 析构调用
CloseHandle()前先判非空 - 移动后原对象句柄必须置为
nullptr,否则二次析构会 crash
示例片段:
class WinHandle {
HANDLE h_ = nullptr;
public:
explicit WinHandle(HANDLE h) : h_(h == INVALID_HANDLE_VALUE ? nullptr : h) {}
~WinHandle() { if (h_) CloseHandle(h_); }
WinHandle(const WinHandle&) = delete;
WinHandle& operator=(const WinHandle&) = delete;
WinHandle(WinHandle&& other) noexcept : h_(other.h_) {
other.h_ = nullptr;
}
// ... 移动赋值同理
};
别用 std::unique_ptr 直接包裸指针或 int
有人想偷懒用 std::unique_ptr<int, decltype(&close)> 管理 fd,这不行——close() 接受的是 int,不是 int*;传地址进去会关错东西甚至崩溃。
更安全的做法是自定义 deleter 闭包,捕获 fd 值而非指针:
auto closer = [](int fd) { if (fd != -1) close(fd); };
std::unique_ptr<int, decltype(closer)> fd_ptr(new int(ret), closer);
但这就绕远了:你得 new 一个 int 存 fd,又多一次堆分配。不如直接写 RAII 类——栈上对象、零开销、语义清晰。
同理,对 HANDLE 也不能用 std::unique_ptr<HANDLE>,因为 CloseHandle() 不接受 HANDLE*。
跨平台封装要考虑 O_CLOEXEC 和 FILE_FLAG_INHERIT
子进程继承父进程句柄是个常见坑。Linux 下 open 时加 O_CLOEXEC 可避免,Windows 下 CreateFile() 要传 FALSE 给 bInheritHandle 参数,并配合 SetHandleInformation(h, HANDLE_FLAG_INHERIT, 0) 补漏。
如果你的封装类支持跨平台,建议在构造时默认启用“不继承”行为:
- Linux:
open(path, flags | O_CLOEXEC) - Windows:
CreateFile(..., ..., ..., ..., ..., FALSE, ...)
否则 fork 或 CreateProcess 后,子进程可能意外持有不该持有的句柄,导致父进程无法删文件、端口被占等诡异问题。
RAII 类不是写完就完事——析构时机、移动语义、平台差异、继承控制,每一步漏掉都可能让“自动关闭”变成“自动出 bug”。


















