必须显式启用异常掩码并预检路径,否则构造失败会导致半初始化对象和未定义行为;移动后需置空源流以确保RAII安全。

为什么不能直接用 std::ifstream / std::ofstream 做异常安全封装
因为默认情况下,std::basic_ios::exceptions() 是关闭的 —— 即使底层系统调用失败(如文件不存在、权限不足、磁盘满),std::ifstream 也不会抛异常,只会置位 failbit 或 badbit,而你得手动检查 .good() 或 operator bool()。不显式开启异常掩码,所谓“异常安全”就是空谈。
更关键的是:构造函数里打开文件失败时,若没开启异常、又没做检查,对象就处于半初始化状态;析构时若再尝试关闭一个未成功打开的流,可能触发未定义行为。
- 必须在构造函数中调用
stream.exceptions(std::ios_base::failbit | std::ios_base::badbit) - 若打开失败(比如
open()内部调用fopen失败),会立即抛std::ios_base::failure,确保构造不成功则对象不存活 - 不要在构造函数里只调
open()就返回——必须同步设置异常掩码,且顺序不能错:先设掩码,再 open
FileInputStream 和 FileOutputStream 的最小可行实现要点
不需要继承 std::ifstream / std::ofstream,用组合 + 移动语义更安全、更可控。重点不是“看起来像标准流”,而是“打开即有效、析构即释放、异常不泄漏资源”。
- 内部持有
std::ifstream或std::ofstream成员,声明为mutable仅当需支持 const 成员函数中调用.read()等(但通常不建议) - 构造函数参数应包含
std::ios_base::openmode,并透传给底层流的open() - 移动构造/赋值必须显式定义:把源对象的流成员
std::move()过来,并将源流置为std::ifstream{}(C++11 起支持空流) - 析构函数无需显式
.close()——std::ifstream析构时自动关闭,只要它被正确构造过
示例片段:
立即学习“C++免费学习笔记(深入)”;
class FileInputStream {
std::ifstream stream_;
public:
explicit FileInputStream(const std::string& path)
: stream_(path, std::ios_base::in | std::ios_base::binary) {
stream_.exceptions(std::ios_base::failbit | std::ios_base::badbit);
if (!stream_.is_open()) {
throw std::runtime_error("Failed to open input file: " + path);
}
}
FileInputStream(FileInputStream&& other) noexcept
: stream_(std::move(other.stream_)) {}
FileInputStream& operator=(FileInputStream&& other) noexcept {
if (this != &other) {
stream_.close(); // 安全起见,先关旧的(即使已关也无害)
stream_ = std::move(other.stream_);
}
return *this;
}
};
如何处理 std::ios_base::failure 异常的层级与信息丢失问题
std::ios_base::failure 只带一条字符串消息(通常是 "basic_ios::clear" 或 "iostream::seekoff"),不包含 errno 或系统错误码。这意味着你无法区分“文件不存在”和“拒绝访问”,对调试和用户提示很不友好。
- 在构造或打开前,可用
std::filesystem::status()预检路径是否存在、是否为常规文件、是否有读/写权限(C++17) - 若预检失败,抛出自定义异常(如
FileOpenError),附带std::filesystem::file_status和errno(通过std::system_category().message(errno)) - 若预检通过但
open()仍失败,说明是竞态条件(如文件被其他进程瞬间删除),此时捕获std::ios_base::failure并重新包装,至少保留原始 what() 和当前路径 - 不要全局 catch(...) ——
std::ios_base::failure是派生自std::system_error的,可精准捕获
移动后原对象的状态与 RAII 边界容易被忽略的点
移动后原对象是否还“拥有”资源?答案是:不拥有,但它的 std::ifstream 成员可能仍处于 opened 状态(如果没正确 move 或没置空),导致双重 close 或析构时静默失败。
- C++ 标准规定
std::ifstream移动后进入“valid but unspecified state”,不保证已关闭;必须显式.close()或赋值为空流(std::ifstream{}) - 移动赋值操作符里,不能只写
stream_ = std::move(other.stream_),必须先确保当前stream_已关闭(或已析构) - 析构函数里不要假设 “只有我负责关” —— 如果类支持复制(不推荐),或有人绕过接口直接访问成员,就得更保守:加个
if (stream_.is_open()) stream_.close(); - 最稳妥做法:所有资源管理逻辑只集中在构造、移动、析构三处,且移动后立即将源对象的流置为
std::ifstream{}
异常安全的核心不在“多抛几个异常”,而在“每个对象生命周期的起点和终点都明确、可验证”。一旦移动发生,原对象就该退化为“空壳”,不能再参与任何 I/O。



















