C++ STL之filesystem文件系统库详解
一、从 Boost 到标准库:filesystem 的前世今生
C++17 之前,跨平台文件操作是噩梦。Windows 用 GetFileAttributesW、Linux 用 stat、Mac 用 NSFileManager——每套 API 各有千秋,但无一适用于跨平台场景。多数项目转向 boost::filesystem,但 Boost 毕竟不是标准,团队引入需额外依赖、配置构建链。
C++17 正式将 std::filesystem 纳入标准库,头文件 <filesystem>,命名空间 std::filesystem。它统一了路径分隔符(/ vs \)、文件属性提取、目录遍历、磁盘容量查询等操作,且无第三方依赖。
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path p = "/home/user/file.txt";
bool exists = fs::exists(p);
return 0;
}
二、path 类与正规化
path 是 filesystem 库的核心类型,负责跨平台路径表示。它不检查路径是否真实存在,只做字符串操作。
常见操作
| 操作 | 说明 |
|---|---|
p.root_name() | 返回根名(Windows 的 C:) |
p.root_directory() | 返回根目录(/ 或 \) |
p.parent_path() | 父路径 |
p.filename() | 文件名(含后缀) |
p.stem() | 不带后缀的文件名 |
p.extension() | 后缀(含 .) |
正规化:lexically_normal
移除路径中的 .、.. 和多余分隔符,返回逻辑上"干净"的路径。
fs::path p = "a/./b/../c/file.txt";
fs::path norm = p.lexically_normal();
// 结果: "a/c/file.txt"
如果路径中有 . 表示当前目录,.. 表示上一级父目录,lexically_normal 会将它们约简。注意这是纯字符串操作,不会访问磁盘。
make_preferred
将路径分隔符转为当前平台惯用格式:Windows 转 \,POSIX 转 /。
fs::path p = "a/b/c";
p.make_preferred(); // Windows: a\b\c, Linux: a/b/c
三、directory_entry 的缓存机制
directory_entry 封装了文件系统的一条记录,包括路径和属性。它的核心设计是缓存:首次访问文件属性后,结果会被内部缓存,后续访问不再触发系统调用。
fs::directory_entry entry("/home/user/file.txt");
auto size1 = entry.file_size(); // 触发 stat
auto size2 = entry.file_size(); // 返回缓存,无系统调用
entry.refresh(); // 强制刷新缓存
auto size3 = entry.file_size(); // 重新 stat
这在遍历大量文件时能显著减少 I/O:一次 directory_iterator 操作自动填充 directory_entry,后续取 file_size、last_write_time、file_type 等都走缓存。
四、目录遍历:directory_iterator 与 recursive_directory_iterator
两套 API 对应两种遍历范围:
| 特性 | directory_iterator | recursive_directory_iterator |
|---|---|---|
| 遍历深度 | 仅当前目录 | 递归子目录 |
是否含 . .. | 否 | 否 |
| 排序 | 未定义 | 未定义 |
| 跳过子目录 | 不适用 | disable_recursion_pending() |
void list_all(const fs::path& dir) {
fs::directory_iterator end;
for (auto it = fs::directory_iterator(dir); it != end; ++it) {
// 只有第一层
}
for (auto it = fs::recursive_directory_iterator(dir); it != fs::recursive_directory_iterator(); ++it) {
// 递归全部
if (it->is_directory() && skip(*it))
it.disable_recursion_pending();
}
}
两种迭代器返回的都是 directory_entry,可直接调用 is_regular_file()、file_size()、path() 等。
五、space_info:磁盘容量查询
std::filesystem::space() 返回 space_info 结构体:
struct space_info {
uintmax_t capacity; // 总容量
uintmax_t free; // 剩余容量(给调用者的配额)
uintmax_t available; // 可用容量(含非特权用户的配额限制)
};
fs::space_info si = fs::space("/");
std::cout << "总容量: " << si.capacity / 1e9 << " GB\n";
std::cout << "剩余: " << si.free / 1e9 << " GB\n";
std::cout << "可用: " << si.available / 1e9 << " GB\n";
free 与 available 的区别:POSIX 系统上,free 是文件系统层未使用的块数,available 还要扣除 root 预留块。非 root 用户看到 available 小于 free。
六、copy 与 copy_options
fs::copy 复制文件或目录,搭配 copy_options 控制行为:
| 选项 | 效果 |
|---|---|
none | 默认,已存在时报错 |
skip_existing | 跳过已有文件,不报错 |
overwrite_existing | 覆盖已有文件 |
update_existing | 仅目标更旧时才覆盖 |
recursive | 递归复制子目录 |
directories_only | 仅复制目录结构 |
create_symlinks | 复制为符号链接 |
void backup(const fs::path& src, const fs::path& dst) {
fs::copy_options opts =
fs::copy_options::recursive |
fs::copy_options::overwrite_existing |
fs::copy_options::update_existing;
fs::copy(src, dst, opts);
}
注意 fs::copy 在目录上默认不递归,必须显式加 copy_options::recursive 才能复制子目录。
七、面试题精选
1. lexically_normal 会访问磁盘吗?
不会。它是纯字符串操作,只处理 ./ 和 ../ 路径分量,不检查文件是否存在。如果需要解析符号链接或相对路径的绝对化,用 canonical 或 weakly_canonical(会访问磁盘)。
2. directory_iterator 返回的条目有顺序保证吗?
没有。遍历顺序由底层文件系统决定,不可预测。若需有序遍历,须手动收集到 std::vector 再排序。
3. directory_entry 缓存什么时间生效?如何刷新?
首次调用 file_size()、last_write_time()、status() 等触发系统调用后缓存结果。文件在外部被修改时缓存变脏,需调用 refresh() 刷新。缓存不自动感知外部变更。
4. copy_options::recursive 和 copy_options::directories_only 可以同时使用吗?
可以。组合后会在目标递归创建完整的目录结构,但不复制任何文件。常用于备份前建好骨架。
5. space_info::free 和 space_info::available 实际有什么区别?
free 是文件系统层空闲块,available 还要排除特权预留(ext4 默认 5% 给 root)。用户态程序应优先读 available,否则写的字节可能被 ENOSPC 拒绝。
6. path 的 operator/= 做了什么?与字符串拼接有何不同?
operator/= 会自动插入平台分隔符(/ 或 \),且不会重复。字符串拼接可能导致 a//b 或 a\b 混用。优先用 operator/= 而不是 fs::path(a.string() + "/" + b.string())。
7. recursive_directory_iterator 如何跳过某个子目录?
调用 disable_recursion_pending(),析构后迭代器会跳过当前条目(须是目录)下的所有内容。
for (auto it = fs::recursive_directory_iterator("."); it != fs::recursive_directory_iterator(); ++it) {
if (it->path().filename() == ".git")
it.disable_recursion_pending();
else
process(*it);
}
8. 如何判断一个 path 是绝对路径?
p.is_absolute():检查是否绝对路径(POSIX 以/开头,Windows 含根名+根目录)p.is_relative():上述取反
Windows 上 C:/foo 是绝对路径,. 是相对路径,/foo 仅含根目录但无根名,按 Windows 规则也是相对路径。
289



被折叠的 条评论
为什么被折叠?



