18、C++17 文件系统库:std::filesystem 详解、路径操作、目录遍历、文件属性查询

说实话,在 C++17 之前,我一直觉得 C++ 的标准库在文件操作这块是个短板。你想啊,你要遍历个目录、查个文件大小,要么用 C 标准库的 stat,要么上平台 API——Windows 用 FindFirstFile,Linux 用 opendir。代码写出来五花八门,跨平台更是噩梦。

直到 C++17 带来了 std::filesystem,我才算真正松了口气。这个库说白了就是 Boost.Filesystem 的标准化版本,把路径、文件、目录这些操作统一成了一组跨平台的接口。我个人习惯是,只要项目允许用 C++17,文件操作一律用它,再也不用跟平台宏打交道了。

18.1 路径操作:别再用字符串拼接了

很多新手喜欢用字符串拼接来构造路径,比如 std::string path = "/home/user/" + filename;。嗯,这里要注意,不同平台的路径分隔符不一样——Windows 是反斜杠,Linux 是正斜杠。你写死分隔符,代码就废了。

std::filesystem::path 就是专门解决这个问题的。它内部会处理平台差异,你只管用 / 运算符拼接就行:

#include <filesystem>
namespace fs = std::filesystem;

fs::path base = "/home/user";
fs::path full = base / "docs" / "report.txt";
// 在 Windows 上输出: /home/user\docs\report.txt
// 在 Linux 上输出:  /home/user/docs/report.txt
std::cout << full << std::endl;

你看,/ 运算符会自动选择正确的分隔符。我在项目中遇到过有人用 + 拼接路径,结果 Windows 上跑出来全是正斜杠,程序直接崩溃。用 path 就永远不会出这种问题。

路径对象还提供了很多实用的分解方法:

方法 说明 示例(full = "/home/user/docs/report.txt")
root_name() 根名(Windows 盘符) 空(Linux)或 "C:"(Windows)
root_directory() 根目录 "/"
parent_path() 父路径 "/home/user/docs"
filename() 文件名 "report.txt"
stem() 不带扩展名的文件名 "report"
extension() 扩展名 ".txt"
小技巧: 判断路径是否存在可以用 fs::exists(p),判断是文件还是目录用 fs::is_regular_file(p)fs::is_directory(p)。这些函数在后续的文件属性查询中会频繁用到。

18.2 目录遍历:递归与非递归

遍历目录是文件操作里最常用的场景之一。C++17 提供了两种迭代器:directory_iterator(非递归)和 recursive_directory_iterator(递归)。

先看非递归的,只遍历当前目录一层:

fs::path dir = "/home/user/projects";
for (const auto& entry : fs::directory_iterator(dir)) {
    std::cout << entry.path() << std::endl;
}

递归遍历就更有用了。我记得有一次要统计一个项目目录下所有 .cpp 文件的总行数,用 recursive_directory_iterator 几行代码就搞定了:

size_t total_lines = 0;
for (const auto& entry : fs::recursive_directory_iterator("/home/user/project")) {
    if (entry.is_regular_file() && entry.path().extension() == ".cpp") {
        // 打开文件统计行数
        std::ifstream file(entry.path());
        total_lines += std::count(std::istreambuf_iterator<char>(file),
                                  std::istreambuf_iterator<char>(), '\n');
    }
}
std::cout << "Total lines: " << total_lines << std::endl;

这里有个坑——recursive_directory_iterator 默认会进入所有子目录,包括符号链接指向的目录。如果你不想跟随符号链接,可以传 directory_options::skip_permission_denied 或者自己过滤。

我曾经踩过的坑: 在遍历过程中删除当前条目会导致迭代器失效。正确的做法是先收集路径列表,遍历完再统一删除。另外,权限不足的目录会抛出 filesystem_error 异常,记得用 try-catch 包一下。

18.3 文件属性查询:大小、时间、权限

文件属性查询是 filesystem 的另一个强项。你可以通过 file_status 对象获取文件类型和权限,也可以通过 directory_entry 直接查询。

常用的属性查询函数:

  • file_size(p):文件大小(字节),对目录调用会抛异常
  • last_write_time(p):最后修改时间,返回 file_time_type
  • status(p):返回 file_status,包含文件类型和权限
  • permissions(p):直接获取或设置权限位
  • space(p):查询磁盘容量信息(总容量、可用容量等)

来看一个综合示例,打印某个文件的所有属性:

fs::path p = "/home/user/data.txt";

if (!fs::exists(p)) {
    std::cerr << "File not found!" << std::endl;
    return;
}

// 文件大小
std::cout << "Size: " << fs::file_size(p) << " bytes" << std::endl;

// 最后修改时间
auto ftime = fs::last_write_time(p);
std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
std::cout << "Last write: " << std::ctime(&cftime);

// 文件类型
auto status = fs::status(p);
if (fs::is_regular_file(status))
    std::cout << "Type: regular file" << std::endl;
else if (fs::is_directory(status))
    std::cout << "Type: directory" << std::endl;
else if (fs::is_symlink(status))
    std::cout << "Type: symlink" << std::endl;

// 权限
auto perms = status.permissions();
std::cout << "Permissions: "
          << ((perms & fs::perms::owner_read) != fs::perms::none ? "r" : "-")
          << ((perms & fs::perms::owner_write) != fs::perms::none ? "w" : "-")
          << ((perms & fs::perms::owner_exec) != fs::perms::none ? "x" : "-")
          << std::endl;

你想想看,以前用 stat 结构体,你得手动处理 st_mode 的位掩码,现在直接调用成员函数,代码可读性高多了。

18.4 知识体系总览

下面这张图总结了 std::filesystem 的核心功能模块,方便你快速回顾:

std::filesystem 核心功能 路径操作 • path 构造与拼接 • 分解:stem/extension • 规范化:lexically_normal • 相对/绝对路径转换 目录遍历 • directory_iterator • recursive_directory_iterator • 过滤文件类型 • 跳过权限不足的目录 文件属性 • file_size / last_write_time • file_status / permissions • space(磁盘容量) • 类型判断:is_regular_file 文件操作 • copy / copy_file • rename / remove • create_directory / create_directories • 错误处理:filesystem_error 符号链接 • create_symlink • read_symlink • is_symlink 判断 • 注意循环链接检测 磁盘信息 • space(path) • 返回 space_info 结构体 • capacity / free / available • 跨平台统一接口

18.5 实用技巧与避坑

最后分享几个我在实际项目中积累的经验:

  • 路径比较用 equivalent() 而不是 ==:两个路径可能指向同一个文件但字符串不同(比如有符号链接或相对路径),equivalent() 会解析后再比较。
  • 批量操作注意性能recursive_directory_iterator 遍历大量文件时,每次迭代都会做系统调用。如果只需要文件名,用 directory_options::skip_permission_denied 可以避免权限异常中断。
  • 临时文件用 temp_directory_path():获取系统临时目录,再配合 unique_path() 生成不重复的文件名,比手动拼接安全得多。
  • 异常处理别偷懒:文件操作随时可能失败——磁盘满了、权限不够、文件被占用。我建议在关键操作周围加上 try-catch,捕获 fs::filesystem_error 并打印 what() 信息,方便排查问题。
一句话总结: std::filesystem 让 C++ 的文件操作终于有了「现代感」。路径用 path 对象、遍历用迭代器、属性查询用成员函数——跨平台、类型安全、异常友好。如果你还在用 C 标准库或者平台 API,是时候升级了。

公众号:蓝海资料掘金营,微信deep3321