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_typestatus(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 的核心功能模块,方便你快速回顾:
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,是时候升级了。