C++directory_iterator遍历目录
C++里遍历目录,别再手写递归了:directory_iterator 的真实用法与避坑指南
上周帮同事看一段代码,他想遍历一个日志目录下的所有 .log 文件,结果写了半页递归 + FindFirstFile/FindNextFile(Windows)又加了一堆宏判断平台。我问他:“C++17 不是自带 std::filesystem::directory_iterator 了吗?”他挠头:“试过,但一跑就崩溃,报 filesystem error: No such file or directory——明明路径是对的啊。”
这其实不是个例。很多人把 directory_iterator 当成“for 循环版的 opendir”,直接拿来就用,却忽略了它背后几个沉默但关键的前提条件。
directory_iterator 不是万能遥控器,它是个“门童”:只负责带你进门、报名字、指方向,但不保证门开着,也不检查你有没有钥匙。
最常踩的坑,就是忽略路径有效性验证。比如这样写:
for (const auto& entry : std::filesystem::directory_iterator("logs/")) {
if (entry.is_regular_file() && entry.path().extension() == ".log") {
std::cout << entry.path().filename() << "\n";
}
}
看着干净?但如果 "logs/" 根本不存在,或者权限不足(比如 Linux 下无 x 权限),构造 directory_iterator 对象时就会抛 std::filesystem::filesystem_error 异常——而且不是在 for 循环里抛,是在初始化时就崩了。
所以第一件事:先确认路径可访问,再迭代。
namespace fs = std::filesystem;
fs::path target = "logs/";
if (!fs::exists(target) || !fs::is_directory(target)) {
std::cerr << "目标路径不存在或非目录:" << target << "\n";
return;
}
if (!fs::is_readable(target)) {
std::cerr << "无读取权限:" << target << "\n";
return;
}
// 此时再放心构造 iterator
for (const auto& entry : fs::directory_iterator(target)) {
// ……
}
注意:is_readable() 在某些平台(如 macOS)可能返回 false 即使能遍历,更稳妥的做法是捕获异常并友好提示:
try {
for (const auto& entry : fs::directory_iterator(target)) {
// 处理逻辑
}
} catch (const fs::filesystem_error& e) {
std::cerr << "遍历失败:" << e.what() << "(路径:" << target << ")\n";
}
另一个被低估的细节:directory_iterator 默认不递归。它只扫一层,类似 ls logs/,不是 ls -R logs/。有人误以为加个 recursive_directory_iterator 就万事大吉,但忘了它和普通 iterator 的行为差异:
directory_iterator是前向迭代器(forward iterator),不可多次遍历,用完即失效;recursive_directory_iterator是输入迭代器(input iterator),同样不可重复使用,且会跳过符号链接指向的目录(除非显式启用follow_symlinks)。
如果你真需要递归扫描,别硬套循环嵌套,直接用 recursive_directory_iterator,但记得控制深度——否则遇到挂载点或循环软链可能卡死:
fs::recursive_directory_iterator iter(target), end;
while (iter != end) {
const auto& entry = *iter;
if (entry.is_regular_file() && entry.path().extension() == ".log") {
std::cout << entry.path() << "\n";
}
// 深度限制:只进两层子目录
if (iter.depth() >= 2) {
iter.pop(); // 跳出当前子树
} else {
++iter;
}
}
iter.depth() 是它的隐藏利器,比手动计数 path.string().substr(...).count('/') 直观得多。
还有一点容易被忽略:路径拼接别用字符串拼。新手常这么干:
std::string full_path = "logs/" + entry.path().filename().string(); // ❌ 危险!
错在哪?entry.path() 返回的是完整路径(如 "logs/app.log"),filename() 只是 "app.log",但 entry.path() 本身已经包含父路径。更糟的是,跨平台时 / 和 \ 混用会导致路径失效。
正确姿势永远是用 fs::path 的 / 重载:
fs::path full = entry.path(); // 已经是完整路径
// 或者从父目录出发:
fs::path full = target / entry.path().filename(); // ✅ 自动处理分隔符
顺便提一句:entry.path() 返回的是 fs::path 对象,不是 std::string。想打印路径,用 .string();想跨平台兼容,优先用 .generic_string()(统一用 /)。
最后说个实用技巧:过滤常见干扰项。实际项目里,.git、__pycache__、.DS_Store 这类目录/文件大概率要跳过。与其在循环里一堆 if 判断,不如提前建个忽略列表:
const std::unordered_set<std::string> ignore_list = {
".git", "__pycache__", ".DS_Store", "node_modules"
};
for (const auto& entry : fs::directory_iterator(target)) {
auto name = entry.path().filename().string();
if (ignore_list.find(name) != ignore_list.end()) continue;
// ……
}
如果忽略规则更复杂(比如通配符),再上 std::regex,但简单场景,哈希集合查起来快得多。
directory_iterator 不是什么黑科技,它只是把系统调用包装得更 C++ 一点。用得好,省去平台判断和错误码翻译;用得莽撞,反而比手写 opendir 更难调试。
关键不是记住 API,而是理解它在帮你做什么、又没帮你做什么。就像借朋友车开——他知道怎么启动、换挡,但不会替你避开路上的坑。
真正省时间的,从来不是抄一段代码,而是搞懂那行 for (auto& e : ...) 背后,操作系统正悄悄做了什么。


还没有评论,来说两句吧...