29、C++17 核心特性:文件系统库,跨平台操作文件和目录。

说实话,在 C++17 之前,跨平台操作文件这件事,一直是我心头的一根刺。

Windows 上用 CreateFileFindFirstFile,Linux 上用 openreaddir。每次写文件遍历,都得写两套代码,再用 #ifdef 包起来。代码丑不说,还容易漏掉边界情况。我记得有一次,一个跨平台工具因为路径分隔符没处理好,在 Linux 上硬生生把 /tmp/ 拼成了 /tmp/\,排查了一下午。

嗯,C++17 的 <filesystem> 库,就是来终结这种痛苦的。

核心概念:路径、目录条目、文件系统错误

文件系统库的核心,说白了就三个东西:std::filesystem::pathstd::filesystem::directory_entry,以及无处不在的 std::filesystem::filesystem_error

  • path:路径的抽象。它不关心你是 Windows 的 C:\Users\ 还是 Linux 的 /home/。你只管往里塞字符串,它自动处理分隔符。
  • directory_entry:目录里的一个条目。它封装了文件状态,比如大小、修改时间、是否是目录。
  • filesystem_error:操作失败时抛出的异常。当然,你也可以用 std::error_code 走无异常路径。

我个人习惯,在性能敏感的场景下,尽量用 std::error_code 版本。为什么呢?因为异常在频繁调用的循环里,开销还是有点明显的。

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path p = "/home/user/docs";
    std::cout << "文件名: " << p.filename() << "\n";
    std::cout << "父路径: " << p.parent_path() << "\n";
    std::cout << "扩展名: " << p.extension() << "\n";
    return 0;
}

遍历目录:从递归到迭代器

遍历目录,是文件系统库最常用的场景之一。C++17 提供了两种迭代器:directory_iteratorrecursive_directory_iterator

前者只遍历当前目录一层,后者会递归进入子目录。我在项目中遇到过一个问题:用 recursive_directory_iterator 遍历一个包含符号链接的目录,结果死循环了。后来发现,默认情况下它不会跳过符号链接。你需要用 directory_options::skip_permission_denied 或者自己检查 is_symlink

注意: 递归遍历时,如果目录结构很深(比如超过 1000 层),可能会栈溢出。建议在深度未知的情况下,自己用队列实现广度优先遍历。
#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void list_files(const fs::path& dir) {
    for (const auto& entry : fs::recursive_directory_iterator(dir)) {
        if (entry.is_regular_file()) {
            std::cout << entry.path() << " - " << entry.file_size() << " bytes\n";
        }
    }
}

常用操作:创建、复制、移动、删除

文件系统库把常见的文件操作都封装成了函数。你想想看,以前要复制一个目录,你得自己递归创建子目录、复制文件、处理权限。现在一行 fs::copy 搞定。

操作 函数 说明
创建目录 create_directory 创建单层目录,父目录必须存在
创建多级目录 create_directories 自动创建所有不存在的父目录
复制文件/目录 copy 支持多种复制选项(覆盖、递归等)
移动/重命名 rename 跨文件系统移动时可能失败
删除文件 remove 删除单个文件或空目录
删除目录树 remove_all 递归删除目录及其所有内容

我曾经在写一个日志清理工具时,用了 remove_all。结果因为路径拼接错误,差点把整个 /tmp 给删了。嗯,这里要注意:remove_all 不会确认,也不会进回收站。删了就没了。

小技巧: 在调用 remove_all 之前,先用 fs::equivalent 检查一下目标路径是不是系统关键目录。比如 if (fs::equivalent(p, "/")),直接返回错误。

文件状态与权限

文件系统库还提供了查询文件状态的能力。比如文件大小、修改时间、权限、类型等。这些信息通过 file_statusfile_info 获取。

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void print_status(const fs::path& p) {
    std::error_code ec;
    auto status = fs::status(p, ec);
    if (ec) {
        std::cerr << "获取状态失败: " << ec.message() << "\n";
        return;
    }

    std::cout << "类型: ";
    if (fs::is_regular_file(status)) std::cout << "普通文件";
    else if (fs::is_directory(status)) std::cout << "目录";
    else if (fs::is_symlink(status)) std::cout << "符号链接";
    else std::cout << "其他";
    std::cout << "\n";

    auto perms = status.permissions();
    std::cout << "权限: "
              << ((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" : "-")
              << "\n";
}

知识体系:文件系统库的核心逻辑

下面这张图,是我自己梳理的文件系统库核心逻辑。你可以看到,所有操作都围绕 path 展开,然后通过 directory_entry 获取具体信息,再通过一系列自由函数完成操作。

std::filesystem::path 字符串 / 字面量 当前路径 (current_path) 迭代器遍历结果 directory_entry file_status / file_info 操作函数 (copy/remove等) 错误处理:filesystem_error / error_code

跨平台注意事项

虽然文件系统库号称跨平台,但实际用起来,还是有几个坑要小心。

  • 路径分隔符:Windows 上 path 会使用 \\,但 string() 返回时可能带转义。建议用 make_preferred() 统一格式。
  • 权限模型:Windows 的权限和 POSIX 权限不完全对应。比如 owner_exec 在 Windows 上可能没有意义。
  • 符号链接:Windows 上创建符号链接需要管理员权限(或开发者模式)。在 Linux 上则没有这个限制。
  • 文件大小file_size 返回 uintmax_t,在 32 位系统上可能溢出。大文件场景建议用 file_size 配合 if (value == static_cast<uintmax_t>(-1)) 检查错误。
核心建议: 写跨平台代码时,永远不要假设路径分隔符是 /\\。用 path/ 运算符拼接路径,它会自动处理。比如 fs::path("dir") / "subdir" / "file.txt"

好了,关于 C++17 文件系统库,核心内容就这些。它不是什么黑魔法,但确实让跨平台文件操作变得优雅了很多。你想想看,以前要写几十行的目录遍历,现在一个循环加几行判断就搞定了。这就是现代 C++ 带来的实实在在的好处。

专注资料整理