目录遍历与搜索
前两篇我们学会了用 path 处理路径、用文件操作函数管理文件和目录。但在实际项目中,最常见的需求其实是"在某个目录下找到我想要的文件"。比如:收集所有 .cpp 文件送给编译器,在资源目录里找到所有纹理图片,或者统计项目代码的总行数。
C++17 提供了两个迭代器来完成目录遍历:directory_iterator 做单层遍历,recursive_directory_iterator 做递归遍历。这一篇我们从基本用法到性能优化,再到错误处理,把目录遍历彻底搞透。
学习目标
- 完成本章后,你将能够:
- [ ] 使用
directory_iterator和recursive_directory_iterator遍历目录- [ ] 理解
directory_entry的缓存优势- [ ] 编写带过滤条件的文件搜索器
- [ ] 处理遍历过程中的权限错误和其他异常
环境说明
和前两篇一样,C++17 标准,GCC 13+ / Clang 15+ / MSVC 2022。头文件 <filesystem>,命名空间 namespace fs = std::filesystem;。
directory_iterator:单层遍历
fs::directory_iterator 是一个输入迭代器,遍历指定目录下的直接子项(不递归进入子目录)。每次解引用返回一个 fs::directory_entry 对象,它包含了文件名和基本状态信息。
最基本的用法是在 range-based for 循环中直接使用:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path dir = "/usr/local/bin";
for (const auto& entry : fs::directory_iterator(dir)) {
std::cout << entry.path().filename().string();
if (entry.is_directory()) {
std::cout << "/";
}
std::cout << "\n";
}
return 0;
}可能的输出(截取部分):
gcc
g++
cmake
python3/
pip就这么简单——一个 range-based for 循环,遍历目录下所有项,输出文件名。如果目录是空的,循环体不会执行。如果目录不存在或没有读取权限,构造迭代器时就会抛出 filesystem_error 异常。
⚠️ directory_iterator 遍历的顺序是未指定的——不保证按字母序、不保证按创建时间、不保证任何特定顺序。如果你需要排序,就把结果收集到 vector 里然后 std::sort。
过滤文件
在实际项目中,我们通常只对特定类型的文件感兴趣。最简单的过滤方式是在循环体内加条件判断:
void find_cpp_files(const fs::path& dir) {
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_regular_file() &&
entry.path().extension() == ".cpp") {
std::cout << entry.path() << "\n";
}
}
}如果你熟悉 C++20 的 ranges,可以结合 views 做更函数式的过滤(但那需要 C++20 支持)。在 C++17 中,lambda + std::copy_if 是一个不错的替代方案:
#include <vector>
#include <algorithm>
std::vector<fs::path> collect_files(const fs::path& dir,
const std::string& ext) {
std::vector<fs::path> result;
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_regular_file() &&
entry.path().extension() == ext) {
result.push_back(entry.path());
}
}
std::sort(result.begin(), result.end());
return result;
}recursive_directory_iterator:递归遍历
如果你需要遍历一个目录树下所有的文件(包括子目录、子目录的子目录...),就需要 fs::recursive_directory_iterator。它的工作方式类似于 find 命令——从起始目录开始,深度优先地递归进入每一个子目录。
void list_all_files(const fs::path& dir) {
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
std::cout << entry.path();
if (entry.is_directory()) {
std::cout << "/";
}
std::cout << "\n";
}
}可能的输出:
/home/user/project/src/
/home/user/project/src/main.cpp
/home/user/project/src/utils/
/home/user/project/src/utils/helper.cpp
/home/user/project/src/utils/helper.h
/home/user/project/CMakeLists.txt深度控制
recursive_directory_iterator 提供了 depth() 方法,返回当前递归深度(从 0 开始)。你可以用它来限制遍历深度:
void list_with_depth_limit(const fs::path& dir, int max_depth) {
for (auto it = fs::recursive_directory_iterator(dir);
it != fs::recursive_directory_iterator(); ++it) {
if (it.depth() > max_depth) {
it.disable_recursion_pending(); // 跳过该子目录
continue;
}
std::cout << std::string(it.depth() * 2, ' ')
<< it->path().filename().string() << "\n";
}
}输出示例(max_depth = 1):
src/
main.cpp
utils/
CMakeLists.txt⚠️ 注意 depth() 返回的是当前条目相对于起始目录的深度,不是相对于根目录。起始目录下的直接子项深度为 0,子目录下的子项深度为 1,以此类推。如果你在遍历过程中需要跳过某个子目录(不想递归进去),可以调用迭代器的 disable_recursion_pending() 方法——下一篇我们会展示具体用法。
directory_options:控制遍历行为
构造 recursive_directory_iterator 时可以传入 directory_options 来控制遍历行为。常用的选项有:
fs::directory_options::none(默认)——遇到权限拒绝的目录时抛异常。
fs::directory_options::skip_permission_denied——遇到权限拒绝的目录时跳过,不抛异常。这个选项在实际项目中非常有用,因为你经常会遇到系统目录(如 /proc、/sys)没有读取权限的情况。
fs::directory_options::follow_directory_symlink——遇到指向目录的符号链接时,跟随链接递归进去。默认不跟随(因为可能导致无限循环)。
// 安全的递归遍历:跳过无权限的目录
for (const auto& entry : fs::recursive_directory_iterator(
dir, fs::directory_options::skip_permission_denied)) {
// 处理 entry...
}笔者强烈建议在遍历用户文件系统(尤其是从根目录或 home 目录开始遍历)时,始终加上 skip_permission_denied。否则,一旦遇到一个没权限的子目录,整个遍历就会中断,已经遍历了一半的结果也丢了。
directory_entry:不只是 path
每次解引用目录迭代器时,你得到的不是 path 对象,而是 directory_entry 对象。directory_entry 是 path 的"加强版"——它不仅保存了路径,还缓存了文件状态信息。
缓存的优势
directory_entry 可能会缓存文件状态信息(类型、大小等),以减少系统调用次数。当你在遍历过程中多次调用 is_regular_file()、is_directory()、file_size() 等方法时,可以直接从缓存读取,避免重复的 stat() 调用。⚠️ 注意:缓存行为是实现定义的(implementation-defined),标准不保证一定会缓存或缓存何时失效。
for (const auto& entry : fs::directory_iterator(dir)) {
// 这些调用使用缓存值,不触发额外的系统调用
auto name = entry.path().filename().string();
auto is_file = entry.is_regular_file();
auto is_dir = entry.is_directory();
auto size = entry.file_size(); // 仅对普通文件有效
std::cout << name << " "
<< (is_file ? "file" : "dir")
<< " " << size << "\n";
}⚠️ directory_entry 的缓存是在迭代器构造时获取的。如果在遍历过程中文件被修改或删除,缓存可能已经过期。如果你需要实时状态,可以调用 entry.refresh() 强制刷新,或者直接用 fs::status(entry.path()) 获取最新状态。不过这种情况比较少见——大多数遍历场景下,缓存数据是足够准确的。
遍历时过滤:按扩展名、大小、时间
我们把前面的知识组合起来,写一个支持多维度过滤的文件搜索函数。它可以根据扩展名、最小文件大小、最大文件大小来过滤结果:
#include <filesystem>
#include <vector>
#include <algorithm>
#include <iostream>
#include <chrono>
namespace fs = std::filesystem;
struct SearchFilter {
std::string extension; // 目标扩展名,空表示不过滤
std::uintmax_t min_size = 0; // 最小文件大小
std::uintmax_t max_size = UINTMAX_MAX; // 最大文件大小
int max_depth = -1; // 最大递归深度,-1 表示不限
};
std::vector<fs::path> search_files(const fs::path& root,
const SearchFilter& filter) {
std::vector<fs::path> results;
std::error_code ec;
auto options = fs::directory_options::skip_permission_denied;
for (auto it =
fs::recursive_directory_iterator(root, options, ec);
it != fs::recursive_directory_iterator(); ++it) {
if (ec) {
std::cerr << "遍历错误: " << ec.message() << "\n";
ec.clear();
continue;
}
// 深度过滤
if (filter.max_depth >= 0 &&
it.depth() > filter.max_depth) {
it.disable_recursion_pending();
continue;
}
const auto& entry = *it;
// 只处理普通文件
if (!entry.is_regular_file()) {
continue;
}
// 扩展名过滤
if (!filter.extension.empty()) {
if (entry.path().extension() != filter.extension) {
continue;
}
}
// 文件大小过滤
auto size = entry.file_size();
if (size < filter.min_size || size > filter.max_size) {
continue;
}
results.push_back(entry.path());
}
std::sort(results.begin(), results.end());
return results;
}使用示例:
int main() {
SearchFilter filter;
filter.extension = ".cpp";
filter.min_size = 100; // 至少 100 字节
filter.max_size = 1000000; // 不超过 1MB
auto files = search_files("/home/user/project", filter);
std::cout << "找到 " << files.size() << " 个文件:\n";
for (const auto& f : files) {
std::cout << " " << f << "\n";
}
return 0;
}这个搜索函数展示了 recursive_directory_iterator 的典型使用模式:构造时加上 skip_permission_denied,在循环体内用 directory_entry 的缓存方法做过滤,最后收集结果。这种"遍历 + 过滤 + 收集"的模式在实际项目中非常常见。
性能考量
目录遍历的性能取决于两个因素:目录的大小和系统调用的次数。directory_entry 的缓存已经帮我们减少了很多不必要的 stat() 调用,但还有一些其他因素需要注意。
符号链接处理
默认情况下,recursive_directory_iterator 不跟随符号链接。这是正确的默认行为——跟随链接可能导致无限循环(A 指向 B,B 指向 A),也可能导致同一个文件被访问多次。如果你确实需要跟随符号链接,加上 follow_directory_symlink 选项,但一定要确保没有循环链接。
深度控制
递归遍历一个深层嵌套的目录结构可能会消耗大量时间和内存。如果你的目标只是浅层搜索,用 depth() 限制递归深度是很有必要的。在笔者的测试中,遍历整个 /usr 目录树大约需要 5 秒,但限制深度为 2 时只需要 0.3 秒。
与手动递归的性能对比
有时候你会看到有人手动写递归来遍历目录(用 directory_iterator 在每个子目录中递归调用)。这种方式的性能通常比 recursive_directory_iterator 差——因为 recursive_directory_iterator 在内部做了优化(比如批量读取目录项),而手动递归每次都要构造新的迭代器。所以优先使用 recursive_directory_iterator。
实战:代码统计工具
作为本篇的收尾,我们来写一个实用的代码统计工具。它递归地遍历指定目录,统计每种源代码文件的文件数量和总行数:
#include <filesystem>
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include <algorithm>
#include <iomanip>
namespace fs = std::filesystem;
struct FileStats {
int file_count = 0;
int total_lines = 0;
};
/// @brief 统计单个文件的行数
/// @param path 文件路径
/// @return 行数(失败返回 0)
int count_lines(const fs::path& path) {
std::ifstream file(path);
if (!file) return 0;
int lines = 0;
std::string line;
while (std::getline(file, line)) {
++lines;
}
return lines;
}
/// @brief 统计目录下的代码文件
/// @param root 根目录
void code_stats(const fs::path& root) {
std::unordered_map<std::string, FileStats> stats;
std::error_code ec;
auto options = fs::directory_options::skip_permission_denied;
for (const auto& entry :
fs::recursive_directory_iterator(root, options, ec)) {
if (ec) {
ec.clear();
continue;
}
if (!entry.is_regular_file()) continue;
auto ext = entry.path().extension().string();
// 只统计常见源代码文件
if (ext != ".cpp" && ext != ".h" && ext != ".hpp" &&
ext != ".c" && ext != ".py" && ext != ".java" &&
ext != ".rs" && ext != ".go") {
continue;
}
// 跳过隐藏目录和 build 目录
bool skip = false;
for (const auto& component : entry.path()) {
auto s = component.string();
if (s == ".git" || s == "build" || s == "cmake-build-*"
|| (s.size() > 1 && s[0] == '.')) {
// 简单的跳过逻辑
}
}
// 完整版本应该用 disable_recursion_pending() 处理
// 这里简化处理
auto lines = count_lines(entry.path());
stats[ext].file_count++;
stats[ext].total_lines += lines;
}
// 输出结果
int total_files = 0;
int total_lines = 0;
std::cout << std::left << std::setw(8) << "扩展名"
<< std::setw(10) << "文件数"
<< std::setw(12) << "总行数" << "\n";
std::cout << std::string(30, '-') << "\n";
for (const auto& [ext, stat] : stats) {
std::cout << std::left << std::setw(8) << ext
<< std::setw(10) << stat.file_count
<< std::setw(12) << stat.total_lines << "\n";
total_files += stat.file_count;
total_lines += stat.total_lines;
}
std::cout << std::string(30, '-') << "\n";
std::cout << std::left << std::setw(8) << "合计"
<< std::setw(10) << total_files
<< std::setw(12) << total_lines << "\n";
}
int main() {
code_stats(".");
return 0;
}可能的输出:
扩展名 文件数 总行数
------------------------------
.cpp 12 4856
.h 15 2340
.hpp 3 892
.py 2 340
------------------------------
合计 32 8428这个工具综合运用了本篇和前两篇的所有知识:recursive_directory_iterator 做递归遍历,directory_entry::is_regular_file() 做类型过滤,path::extension() 做扩展名过滤,path 的迭代器做目录名过滤。在实际项目中,你可以扩展它来统计空行数、注释行数、代码行数等更细粒度的指标。
小结
这一篇我们学习了 directory_iterator 和 recursive_directory_iterator 的用法。directory_iterator 做单层遍历,适合已知目录结构的场景。recursive_directory_iterator 做深度优先递归遍历,适合需要搜索整个目录树的场景。directory_entry 的缓存机制避免了不必要的 stat() 调用,在遍历大型目录时有显著的性能优势。
关于错误处理,始终使用 skip_permission_denied 选项来避免遍历被权限错误中断。关于性能,限制递归深度、避免跟随符号链接、优先使用 recursive_directory_iterator 而不是手动递归。实战部分我们写了代码统计工具和批量重命名工具,它们综合运用了本系列三篇文章的所有知识。
到这里,<filesystem> 库的核心内容我们就讲完了。从 path 的语法处理,到文件操作的状态查询与修改,再到目录遍历与搜索——这套 API 让 C++ 终于有了标准化的文件系统操作能力,不用再依赖 POSIX API 或第三方库了。