29、C++17 核心特性:文件系统库,跨平台操作文件和目录。
说实话,在 C++17 之前,跨平台操作文件这件事,一直是我心头的一根刺。
Windows 上用 CreateFile、FindFirstFile,Linux 上用 open、readdir。每次写文件遍历,都得写两套代码,再用 #ifdef 包起来。代码丑不说,还容易漏掉边界情况。我记得有一次,一个跨平台工具因为路径分隔符没处理好,在 Linux 上硬生生把 /tmp/ 拼成了 /tmp/\,排查了一下午。
嗯,C++17 的 <filesystem> 库,就是来终结这种痛苦的。
核心概念:路径、目录条目、文件系统错误
文件系统库的核心,说白了就三个东西:std::filesystem::path、std::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_iterator 和 recursive_directory_iterator。
前者只遍历当前目录一层,后者会递归进入子目录。我在项目中遇到过一个问题:用 recursive_directory_iterator 遍历一个包含符号链接的目录,结果死循环了。后来发现,默认情况下它不会跳过符号链接。你需要用 directory_options::skip_permission_denied 或者自己检查 is_symlink。
#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_status 和 file_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 获取具体信息,再通过一系列自由函数完成操作。
跨平台注意事项
虽然文件系统库号称跨平台,但实际用起来,还是有几个坑要小心。
- 路径分隔符: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++ 带来的实实在在的好处。