C++17 std::filesystem 文件系统库:从核心概念到工程实践

发布时间:2026/7/21 4:59:10
C++17 std::filesystem 文件系统库:从核心概念到工程实践 1. 项目概述为什么我们需要一个现代的文件系统库如果你用C写过一些需要处理本地文件的程序比如一个日志分析工具、一个配置文件管理器或者一个简单的资源打包器那你肯定没少和fstream、dirent.h或者Windows的FindFirstFile打交道。这些传统的API用起来是什么感觉繁琐、平台依赖、错误处理麻烦。你要打开一个文件得先检查路径是否存在是文件还是目录权限够不够每一步都可能失败每一行代码都写着“平台特定”几个字。更别提遍历一个目录了那代码量写起来都觉得自己在造轮子而且还是方形的。这就是std::filesystem文件系统库被引入C17标准库的核心原因。它不是一个新发明而是把Boost.Filesystem这个久经考验的库标准化了旨在为C程序员提供一个统一、类型安全、异常安全的方式来操作文件和目录。简单说它把那些跨平台的、脏活累活都封装好了给你一套简洁的、现代C风格的接口。你再也不用为了遍历一个文件夹在Windows上写一套WIN32_FIND_DATA在Linux上又写一套struct dirent了。一个std::filesystem::directory_iterator走天下。这个库的价值远不止于“写起来方便”。在当今软件复杂度的背景下可靠的路径处理、递归文件操作、空间查询等都是基础需求。无论是开发桌面应用、服务器后端、游戏引擎还是构建工具文件系统操作都是绕不开的一环。std::filesystem的出现意味着C在系统级编程的现代化道路上补上了一块至关重要的拼图。它让我们的代码更健壮更好的错误处理、更清晰语义明确的函数名、更易于移植标准保证的行为。2. 核心概念与路径解析在深入具体操作之前我们必须先理解std::filesystem的基石std::filesystem::path类。几乎所有文件系统操作都始于一个路径对象。2.1std::filesystem::path不仅仅是字符串路径不是字符串。这是一个非常重要的观念转变。一个path对象内部存储的是操作系统原生格式的路径在Windows上是宽字符在POSIX上是窄字符但它提供了一系列成员函数来以可移植的方式分解和检查路径。#include filesystem namespace fs std::filesystem; // 常用的命名空间别名 fs::path p1 C:\\Users\\Name\\Documents\\file.txt; // Windows 风格 fs::path p2 /home/name/docs/file.txt; // POSIX 风格 fs::path p3 R(project\src\main.cpp); // 原始字符串避免转义为什么用path而不用std::string可移植性path的/运算符重载能自动处理路径分隔符。p3 / include / header.h在Windows上生成project\src\main.cpp\include\header.h在Linux上生成project/src/main.cpp/include/header.h。你用字符串拼接还得自己判断平台。分解能力你可以轻松获取路径的各个部分。fs::path p /home/user/project/src/main.cpp; std::cout p.root_name() \n; // 在Linux上通常是空在Windows上可能是“C:” std::cout p.root_directory() \n; // “/” std::cout p.parent_path() \n; // “/home/user/project/src” std::cout p.filename() \n; // “main.cpp” std::cout p.stem() \n; // “main” (不带扩展名) std::cout p.extension() \n; // “.cpp”隐式转换path可以隐式转换为std::string或std::wstring方便传递给需要字符串的老API但核心操作应始终使用path对象。注意path的构造函数和赋值操作不会检查路径在文件系统中是否存在。它只是一个路径的“描述符”。存在性检查是后续操作如fs::exists的事情。2.2 路径的规范化和弱规范化这是路径处理中一个容易踩坑的地方。path对象可以进行规范化lexically_normal和弱规范化lexically_relative,lexically_proximate。lexically_normal()移除路径中的.当前目录和..上级目录引用并简化多余的斜杠。这是一个纯词法操作不访问文件系统。fs::path p a/./b/../c/; std::cout p.lexically_normal() \n; // 输出 “a/c/”这非常有用特别是在处理用户输入或配置文件中的路径时可以避免一些因路径格式混乱导致的问题。lexically_relative(base)计算当前路径相对于另一个路径base的词法相对路径。同样不访问文件系统。如果两个路径没有共同的前缀则返回空路径。fs::path p /a/b/c; fs::path base /a/d; std::cout p.lexically_relative(base) \n; // 输出 “../b/c”这在生成相对于项目根目录的路径时特别有用。relative(path, base)和proximate(path, base)这是两个文件系统操作需要路径真实存在。它们尝试返回一个相对于base的路径。区别在于当无法计算相对路径时例如在不同驱动器上relative会抛出异常或返回错误而proximate会返回输入的绝对路径。通常proximate更安全。实操心得在拼接或比较路径前尤其是当路径来源不可控时如用户输入先进行lexically_normal()是一个好习惯。这能消除很多潜在的歧义。对于需要真实文件系统关系的相对路径计算优先考虑使用proximate。3. 文件与目录的查询与属性获取知道了路径下一步就是了解它指向的“东西”。std::filesystem提供了一套丰富的函数来查询文件状态和属性。3.1 存在性与类型判断这是最基本的操作通常是你进行任何其他操作前的安全检查。fs::path p some_file.txt; // 检查是否存在 if (fs::exists(p)) { std::cout Path exists.\n; } // 检查是普通文件还是目录 if (fs::is_regular_file(p)) { std::cout Its a regular file.\n; std::cout File size: fs::file_size(p) bytes.\n; // 获取文件大小 } else if (fs::is_directory(p)) { std::cout Its a directory.\n; } // 其他类型判断 if (fs::is_symlink(p)) { /* 符号链接 */ } if (fs::is_block_file(p)) { /* 块设备 */ } // ... 还有 is_character_file, is_fifo, is_socket 等file_size的坑file_size只对普通文件is_regular_file和符号链接指向普通文件有效。对目录、设备文件等调用file_size会导致错误。最佳实践是在调用file_size前务必用is_regular_file检查一下或者准备好处理可能抛出的filesystem_error异常。3.2 获取详细状态file_status和symlink_statusstatus(path)和symlink_status(path)函数返回一个file_status对象它封装了文件类型和权限信息。两者的区别在于对待符号链接的方式status会跟随解引用符号链接返回链接目标的状态而symlink_status返回符号链接本身的状态。fs::file_status st fs::status(p); fs::perms perm st.permissions(); // 获取权限 // 检查权限 (使用位掩码) using fs::perms; if ((perm perms::owner_read) ! perms::none) { std::cout Owner can read.\n; } if ((perm perms::group_write) perms::none) { std::cout Group cannot write.\n; } // 检查文件类型 switch (st.type()) { case fs::file_type::regular: /*...*/ break; case fs::file_type::directory: /*...*/ break; case fs::file_type::symlink: /*...*/ break; case fs::file_type::none: case fs::file_type::not_found: std::cout File does not exist or cannot be accessed.\n; break; // ... 其他类型 }为什么关心symlink_status在处理可能包含符号链接的目录树时区分链接本身和链接目标至关重要。例如在复制目录时你可能想保留符号链接复制链接本身而不是复制它指向的整个目标这可能造成循环或数据冗余。这时就需要用symlink_status来判断原始条目是否为链接。3.3 其他有用信息最后修改时间与空间信息最后修改时间fs::last_write_time(p)返回一个file_time_type通常是std::chrono::time_point。你可以用它来做增量备份、缓存失效判断等。auto ftime fs::last_write_time(p); // 可以转换为 time_t 以便使用C库函数打印 auto sctp std::chrono::time_point_caststd::chrono::system_clock::duration(ftime - fs::file_time_type::clock::now() std::chrono::system_clock::now()); std::time_t tt std::chrono::system_clock::to_time_t(sctp); std::cout Last modified: std::ctime(tt);注意修改文件时间本身也是可以的使用fs::last_write_time(p, new_time)。空间信息fs::space(p)返回一个space_info结构体包含该路径所在磁盘的总容量、空闲空间和可用空间。fs::space_info si fs::space(/); std::cout Total: si.capacity bytes\n Free: si.free bytes\n Available: si.available bytes\n;在写入大文件或做数据备份前检查磁盘空间是一个很好的防御性编程习惯。4. 目录遍历迭代器的艺术遍历目录是文件系统操作中最常见的任务之一。std::filesystem提供了两种主要的迭代器directory_iterator和recursive_directory_iterator。4.1directory_iterator单层遍历它用于遍历一个目录下的直接子项文件和子目录不会进入子目录。fs::path dir_path ./test_dir; try { for (const auto entry : fs::directory_iterator(dir_path)) { const auto path entry.path(); std::cout Found: path.filename() - ; if (entry.is_regular_file()) { std::cout File, size: entry.file_size() \n; } else if (entry.is_directory()) { std::cout Directory\n; } else if (entry.is_symlink()) { std::cout Symlink\n; // entry.symlink_status() 获取链接本身状态 // fs::read_symlink(path) 读取链接目标 } // entry 还有 status(), path(), file_size() 等方法非常方便 } } catch (const fs::filesystem_error e) { std::cerr Directory iteration error: e.what() \n; }directory_entry对象即迭代器解引用得到的entry缓存了文件状态信息这意味着多次调用entry.is_regular_file()或entry.file_size()是高效的不会重复进行昂贵的系统调用。4.2recursive_directory_iterator递归遍历顾名思义它会递归地遍历目录树。这是实现“查找所有某种文件”功能的利器。fs::path root_path ./project; try { for (const auto entry : fs::recursive_directory_iterator(root_path)) { // 可以在这里进行过滤 if (entry.is_regular_file() entry.path().extension() .cpp) { std::cout C source: entry.path() \n; } // 控制递归深度和是否跟随符号链接 // 默认深度无限制默认不跟随符号链接防止循环 } } catch (const fs::filesystem_error e) { std::cerr Recursive iteration error: e.what() \n; }高级控制recursive_directory_iterator可以通过构造函数选项进行精细控制。// 选项枚举 using fs::directory_options; auto options directory_options::skip_permission_denied // 跳过无权限目录 | directory_options::follow_directory_symlink; // 跟随目录符号链接危险 fs::recursive_directory_iterator it(root_path, options); // 在遍历过程中还可以动态控制 for (; it ! fs::recursive_directory_iterator(); it) { if (it-path().filename() build) { it.disable_recursion_pending(); // 不进入名为“build”的目录 } if (it.depth() 5) { // 限制递归深度 it.pop(); // 跳出当前目录继续遍历同级或上级 } }重要警告follow_directory_symlink选项非常危险如果目录树中存在符号链接环A链接到BB又链接回A启用此选项会导致迭代器陷入无限循环直到耗尽内存或达到路径长度限制。除非你百分百确定目录树中没有循环链接否则不要使用这个选项。一个更安全的做法是默认不跟随当遇到符号链接时手动调用fs::read_symlink判断目标再决定是否进行特殊处理。4.3 遍历中的性能与错误处理遍历大型目录树如整个硬盘是I/O密集型操作可能很慢并且会遇到各种权限问题。性能directory_entry缓存了状态所以尽量使用entry的方法如is_...,file_size而不是重新调用fs::status(entry.path())。错误处理迭代器在遍历时如果遇到一个无法访问的子目录如权限不足默认行为是抛出filesystem_error异常。这会导致整个遍历中断。为了避免这种情况可以使用directory_options::skip_permission_denied选项让迭代器静默跳过这些目录。你需要在构造迭代器时指定这个选项。迭代器失效在遍历过程中如果其他进程或线程修改了正在遍历的目录结构如删除或创建文件可能会导致迭代器失效或行为未定义。对于关键应用可以考虑先收集路径列表再进行处理。5. 文件与目录的操作创建、复制、移动与删除查询和遍历是“读”操作而std::filesystem同样提供了完整的“写”操作。5.1 创建目录创建单个目录或多个层级的目录。fs::path new_dir new_folder; fs::path deep_dir a/b/c/d/e; // 创建单个目录。如果父目录不存在则失败。 if (!fs::create_directory(new_dir)) { std::cout Directory already exists or creation failed.\n; } // 递归创建目录类似于 mkdir -p。如果目录已存在也不会报错。 bool created fs::create_directories(deep_dir);create_directories是原子性的吗不完全是。虽然它试图创建路径上的所有目录但如果中间某一步失败例如在创建a/b后另一个进程创建了a/b/c作为一个文件那么函数会失败并可能留下部分创建的目录。通常你需要处理filesystem_error异常。5.2 复制文件与目录复制操作功能强大可以通过copy_options进行精细控制。fs::path src source.txt; fs::path dst backup/source_copy.txt; // 最简单的复制文件 fs::copy(src, dst); // 如果dst已存在默认会抛出异常 (copy_options::none) // 使用选项 using fs::copy_options; auto options copy_options::overwrite_existing // 覆盖已存在文件 | copy_options::recursive // 如果是目录递归复制 | copy_options::copy_symlinks; // 复制符号链接本身而不是其目标 fs::copy(src, dst, options); // 专门复制目录需要 recursive 选项 fs::path src_dir data; fs::path dst_dir backup/data; fs::copy(src_dir, dst_dir, copy_options::recursive | copy_options::overwrite_existing);copy_options详解overwrite_existing覆盖目标路径已存在的文件。skip_existing跳过已存在的文件。update_existing仅当源文件比目标文件新时覆盖。recursive递归复制子目录和内容。copy_symlinks复制符号链接为符号链接。skip_symlinks忽略符号链接。directories_only只复制目录结构不复制文件内容。create_symlinks/create_hard_links创建指向源文件的符号链接或硬链接而不是复制内容。重要提示fs::copy在复制单个文件时默认不会复制文件的最后修改时间等属性。如果需要保留所有属性类似于cp -p需要使用copy_options::preserve_all它等价于copy_symlinks | copy_directory | preserve_all会保留权限、时间戳等。5.3 移动与重命名移动和重命名在文件系统层面是同一个操作改变文件或目录的路径。fs::path old_name old.txt; fs::path new_name new.txt; fs::path new_location subdir/new.txt; // 重命名文件 fs::rename(old_name, new_name); // 如果new_name已存在行为由操作系统决定通常失败或覆盖。 // 移动文件到另一个目录并可能重命名 fs::rename(file.txt, another_dir/file.txt); // 跨设备/分区移动 // fs::rename 要求源和目标必须在同一个文件系统挂载点上。 // 如果跨设备rename会失败。此时需要先复制再删除。 if (!fs::rename(src, dst)) { // 如果rename失败可能是因为跨设备 fs::copy(src, dst, fs::copy_options::overwrite_existing | fs::copy_options::recursive); fs::remove_all(src); // 小心使用 }fs::rename是原子操作在同一个文件系统内。如果目标已存在其行为是操作系统定义的。在POSIX系统上如果目标是已存在的文件它会被静默替换如果是已存在的非空目录操作会失败。在Windows上如果目标已存在rename通常会失败。为了可移植的“覆盖”行为你应该先检查并删除目标如果存在且允许覆盖然后再调用rename。5.4 删除文件与目录删除操作需要格外小心尤其是递归删除。fs::path file_to_remove temp.txt; fs::path dir_to_remove temp_dir; // 删除单个文件或空目录 bool removed fs::remove(file_to_remove); // 返回是否真的删除了东西 if (!removed) { std::cout File did not exist.\n; } // 递归删除目录及其所有内容 (类似于 rm -rf) std::uintmax_t num_removed fs::remove_all(dir_to_remove); std::cout Removed num_removed files/directories.\n;remove_all的巨坑与最佳实践fs::remove_all功能强大但也极其危险。一旦执行数据几乎无法恢复除非有备份。我亲身踩过的坑在写一个清理临时文件的脚本时因为一个路径构造错误差点把项目源码目录给删了。安全准则绝对不要对用户输入的路径直接使用remove_all。必须先进行严格的验证和规范化。在调用remove_all之前先打印出要删除的路径或者要求二次确认。在调试版本中可以加入此逻辑。考虑使用fs::canonical或fs::weakly_canonical。它们能返回一个绝对且规范化的路径消除.和..的影响并解析符号链接canonical要求路径必须存在。这有助于避免因路径歧义导致的误删。fs::path user_input some/../path; fs::path safe_path fs::weakly_canonical(user_input); // 即使路径不存在也能处理 // 添加安全检查确保要删除的目录在某个“安全区”内 fs::path safe_base /allowed/to/delete/here; if (safe_path.string().find(safe_base.string()) ! 0) { throw std::runtime_error(Deletion path is outside the safe zone!); } // ... 然后才能考虑 remove_all实现“垃圾箱”功能对于重要应用不要直接删除而是先移动到某个临时回收站目录定期清理。这给了操作一个回滚的机会。6. 符号链接与硬链接操作链接是文件系统的高级特性std::filesystem也提供了支持。6.1 创建链接创建符号链接软链接fs::create_symlink(target, link)。target可以是相对或绝对路径存储在链接文件中。// 创建指向文件的符号链接 fs::create_symlink(/original/file.txt, link_to_file); // 创建指向目录的符号链接 fs::create_directory_symlink(/original/dir, link_to_dir);在Windows上创建目录符号链接可能需要管理员权限或者启用开发者模式。创建硬链接fs::create_hard_link(target, link)。target必须是一个已存在的文件不能是目录并且与链接必须在同一个文件系统内。硬链接是同一个文件的另一个名字删除任何一个名字都不会影响文件数据直到最后一个链接被删除。fs::create_hard_link(original.txt, hardlink.txt);6.2 读取链接使用fs::read_symlink(link_path)来获取符号链接指向的目标路径。对于硬链接没有直接的“读取”API因为一个文件可能有多个硬链接它们地位平等。try { fs::path target fs::read_symlink(my_symlink); std::cout Symlink points to: target \n; } catch (const fs::filesystem_error e) { if (e.code() std::errc::invalid_argument) { std::cout The path is not a symlink.\n; } }7. 错误处理与异常和错误码共舞std::filesystem的函数有两种错误处理方式抛出异常这是默认行为。函数失败时会抛出std::filesystem_error异常。这个异常继承自std::system_error包含了详细的错误信息操作类型、路径1、路径2如果有以及底层的std::error_code。使用std::error_code输出参数每个可能抛出异常的函数都有一个重载版本其最后一个参数是一个std::error_code。如果提供这个参数函数会将错误码填入其中而不是抛出异常。7.1 如何选择对于可恢复的、预期的错误使用error_code。例如检查文件是否存在不存在不是错误是正常情况。std::error_code ec; bool exists fs::exists(some_path, ec); if (ec) { // 真正的错误发生了如权限不足 std::cerr Error checking existence: ec.message() \n; } else { // exists 变量反映了真实的存在状态 }对于不可恢复的、非预期的错误或者你希望错误能自动向上传播的场景让异常抛出。例如在程序初始化时必须成功创建一个配置目录失败就应该让程序终止或回滚。try { fs::create_directories(essential_config_path); } catch (const fs::filesystem_error e) { std::cerr Fatal: Could not create config directory: e.what() \n; std::cerr Path1: e.path1() \n; // 可能快速失败退出或尝试恢复 std::exit(1); }7.2 理解std::filesystem_error这个异常非常有用。e.what()会返回一个描述性的字符串。e.code()返回std::error_code。e.path1()和e.path2()返回与此错误相关的路径不是所有错误都有两个路径。try { fs::copy(src, dst); } catch (const fs::filesystem_error e) { std::cerr Filesystem error: e.what() \n; std::cerr Code: e.code() ( e.code().message() )\n; std::cerr Path1: e.path1() \n; if (!e.path2().empty()) { std::cerr Path2: e.path2() \n; } }8. 实战一个简单的项目文件清理工具让我们把上面的知识组合起来写一个实用的小工具清理项目构建目录如build、CMakeFiles、.o文件等。#include filesystem #include iostream #include vector #include string #include set namespace fs std::filesystem; // 定义要删除的目录名和文件扩展名 const std::setstd::string dirs_to_remove {build, CMakeFiles, .vs, x64, Debug, Release}; const std::setstd::string files_to_remove {.o, .obj, .so, .a, .lib, .dll, .exe, .pdb, .ilk, .log, .tlog, .idb}; bool should_remove_dir(const fs::path dir_name) { return dirs_to_remove.find(dir_name.string()) ! dirs_to_remove.end(); } bool should_remove_file(const fs::path file_path) { return files_to_remove.find(file_path.extension().string()) ! files_to_remove.end(); } void clean_project_directory(const fs::path project_root, bool dry_run true) { std::error_code ec; // 使用 recursive_directory_iterator 并跳过无权限目录 auto options fs::directory_options::skip_permission_denied; for (auto it fs::recursive_directory_iterator(project_root, options, ec); it ! fs::recursive_directory_iterator(); it) { if (ec) { std::cerr Warning: Skipping due to error: ec.message() at it-path() \n; ec.clear(); it.disable_recursion_pending(); // 跳过这个有问题的目录 continue; } try { const auto entry *it; if (entry.is_directory()) { if (should_remove_dir(entry.path().filename())) { std::cout (dry_run ? [DRY RUN] Would remove dir: : Removing dir: ) entry.path() \n; if (!dry_run) { // 注意这里删除目录后迭代器会失效。 // 一种方法是先收集路径遍历完再删除。另一种是使用 postfix increment。 // 这里采用先收集再删除的策略更安全为了示例简单我们打印而不实际删除。 // fs::remove_all(entry.path()); } it.disable_recursion_pending(); // 标记不要进入这个即将被删除的目录 } } else if (entry.is_regular_file()) { if (should_remove_file(entry.path())) { std::cout (dry_run ? [DRY RUN] Would remove file: : Removing file: ) entry.path() \n; if (!dry_run) { // fs::remove(entry.path()); } } } } catch (const fs::filesystem_error e) { std::cerr Error processing it-path() : e.what() \n; // 通常选择跳过这个有问题的条目 it.disable_recursion_pending(); } } } int main(int argc, char* argv[]) { fs::path root (argc 1) ? fs::path(argv[1]) : fs::current_path(); root fs::weakly_canonical(root); // 规范化路径 std::cout Cleaning project under: root \n\n; std::cout --- DRY RUN MODE (no actual deletion) ---\n; clean_project_directory(root, true); // 干跑模式 std::cout \nProceed with actual deletion? (yes/no): ; std::string answer; std::cin answer; if (answer yes || answer y) { std::cout \n--- ACTUAL CLEANUP ---\n; clean_project_directory(root, false); } else { std::cout Cleanup cancelled.\n; } return 0; }这个工具的关键点安全第一默认使用“干跑”模式先列出要删除的内容让用户确认。路径规范化使用weakly_canonical处理输入路径避免.和..带来的混淆。错误处理遍历时使用error_code和异常捕获相结合确保遇到个别问题如权限不足时能跳过并继续而不是整个程序崩溃。迭代器控制当决定要删除一个目录时调用disable_recursion_pending()防止迭代器进入其中这是安全的因为我们马上就要删除它了在实际删除版本中。实际删除的复杂性在递归遍历过程中直接删除条目是危险的因为删除目录会使指向它的迭代器失效。更健壮的做法是在干跑模式或第一次遍历时将要删除的文件路径收集到一个std::vectorfs::path中将要删除的空目录路径收集到另一个容器中。遍历结束后先删除所有文件然后按目录深度从深到浅的顺序删除空目录。对于非空目录如build直接对整个目录调用remove_all。这避免了迭代器失效问题。9. 跨平台注意事项与编译器支持虽然std::filesystem是标准库但不同平台和编译器仍有细微差别。C17 支持你需要确保你的编译器支持C17或更高版本并启用了文件系统库。GCC/G: 版本 8.1编译时添加-stdc17 -lstdcfs(GCC 8/9 需要链接库GCC 10 通常不需要)。Clang: 版本 7.0编译时添加-stdc17 -lcfs(使用libc时) 或-lstdcfs(使用libstdc时)。MSVC (Visual Studio): VS 2017 15.7 及以上版本项目属性中设置C Language Standard为ISO C17 Standard或更高。通常不需要额外链接库。路径分隔符尽管path类处理了/和\但在输出或与字符串交互时要注意。path.string()返回原生格式Windows是\Linux是/。如果需要可移植的字符串表示可以使用path.generic_string()它总是使用/作为分隔符。文件权限Windows和POSIX系统的权限模型差异很大。fs::perms在Windows上可能只有少数几位有意义如只读属性。设置权限fs::permissions(p, perms)在跨平台时行为可能不一致。符号链接Windows上创建符号链接可能需要提升的权限以管理员身份运行或启用“开发者模式”。对于普通用户create_symlink可能失败。文件大小类型fs::file_size返回std::uintmax_t通常是64位无符号整数足以处理大文件。Unicode 支持fs::path在Windows上内部使用wchar_tUTF-16在POSIX上使用charUTF-8。用UTF-8字符串构造路径在跨平台时通常是最安全的方式。避免使用本地编码如GBK。我个人在实际项目中的体会是std::filesystem极大地提升了代码的清晰度和可维护性。虽然初期需要适应其错误处理方式和一些跨平台的坑但一旦掌握它就成了处理文件I/O的首选工具。对于更复杂的场景如监控文件变化、内存映射文件等你可能还需要依赖平台特定API或第三方库但对于90%的日常文件操作任务std::filesystem已经完全够用且是最佳选择。最后一个小技巧在编写涉及大量文件操作的代码时善用std::filesystem::path的/运算符来构建路径这比字符串拼接安全、清晰得多。