把文件系统看成一棵树:根目录是根节点,普通文件是叶子节点,子目录是中间节点。遍历文件夹下的所有文件,其实就是从根节点出发,沿着目录项依次访问所有叶子。递归实现时,函数每遇到一个子目录就调用自身,直到不再包含子目录为止,这正好对应深度优先搜索。递归版本的逻辑可以概括为打开当前目录、枚举目录项、遇到文件就处理、遇到子目录就拼接新路径继续递归,最后关闭目录句柄。边界条件通常有两个:当前目录不可读时直接返回,以及跳过 . 和 .. 两个特殊项,否则会形成无限递归。

理解这个流程后,标准库和操作系统API只是同一套遍历逻辑的不同封装。C++17的 std::filesystem 提供了最省事的迭代器,而Windows API和POSIX接口则在旧标准或特殊平台下仍然有使用价值。下面分别展开。
一、目录遍历本质上是深度优先搜索
目录的嵌套关系天然适合用树来描述,遍历所有文件的过程就是在遍历这棵树。深度优先搜索的特点是先沿着一个子目录走到最深处,处理完所有文件后再返回上一层继续处理下一个子目录。递归函数天然匹配这个执行顺序,因此绝大多数目录遍历实现都采用递归写法。
递归函数需要明确三件事:当前正在处理哪个目录、如何获取该目录下的所有条目、如何区分文件和子目录。拿到一个条目后,如果是普通文件就执行需要的操作,比如打印路径、统计数量或读取内容;如果是子目录,就把当前目录路径和子目录名拼成新路径,交给下一层递归。为了防止无限递归,必须过滤掉 . 和 ..,它们在每个目录中都存在,但不代表真实的下一层目录。
目录不可读、权限不足或路径拼错都会导致递归中断,所以每次递归调用前最好检查目录是否有效。对于Windows API,句柄返回 INVALID_HANDLE_VALUE 就说明打开失败;对于POSIX接口,opendir 返回空指针表示失败。标准库则通常通过异常来报告错误。
二、使用 std::filesystem 递归遍历
C++17标准库提供的 std::filesystem 把目录遍历封装成了迭代器。recursive_directory_iterator 会在构造时接收一个根路径,然后通过 operator++ 自动进入子目录。只需要一个范围for循环,就能依次拿到所有目录项。下面这段代码会输出根目录下所有普通文件的完整路径。
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
void list_all_files(const fs::path& root) {
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (entry.is_regular_file()) {
std::cout << entry.path().string() << '\n';
}
}
}
这种写法足够简洁,但在目录树很大或权限复杂的环境下,默认行为会带来两个问题。第一,遇到不可读的子目录会直接抛出 filesystem_error 异常,如果不捕获,整个遍历会中断。第二,目录符号链接默认不会被跟随,这反而安全;如果为了某些场景手动加上 follow_directory_symlink 选项,就需要额外记录已访问路径,否则链接可能形成环。下面代码展示了跳过无权限目录并保持符号链接安全的写法。
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
void list_files_safe(const fs::path& root) {
fs::recursive_directory_iterator it(
root,
fs::directory_options::skip_permission_denied
);
for (const auto& entry : it) {
if (entry.is_regular_file()) {
std::cout << entry.path().string() << '\n';
}
}
}
如果只需要遍历某一层目录,可以使用 directory_iterator;需要递归时再换用 recursive_directory_iterator。二者接口基本一致,只是遍历深度不同。对于大多数跨平台项目,优先考虑这套标准库方案,因为它不用关心Windows与Unix之间的API差异,代码可读性和维护成本都更低。
三、Windows API 手动递归实现
如果项目仍在使用C++14或更早标准,或者需要精确控制Win32路径和文件属性,可以用 FindFirstFileW 和 FindNextFileW 组合。这个方案的思路和标准库一致,只是API更底层。递归时,当前目录路径需要拼接通配符 \*,结果中的 . 和 .. 必须跳过,否则会无限递归。
#include <windows.h>
#include <string>
#include <iostream>
void list_files_recursive(const std::wstring& dir) {
WIN32_FIND_DATAW data;
HANDLE hFind = FindFirstFileW((dir + L"\\*").c_str(), &data);
if (hFind == INVALID_HANDLE_VALUE) {
return;
}
do {
std::wstring name = data.cFileName;
if (name == L"." || name == L"..") {
continue;
}
std::wstring full = dir + L"\\" + name;
if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
list_files_recursive(full);
} else {
std::wcout << full << std::endl;
}
} while (FindNextFileW(hFind, &data));
FindClose(hFind);
}
宽字符串版本能够较好地处理中文目录名和中文文件名,避免使用窄字符时出现的编码问题。每次递归前使用 FindFirstFileW 打开目录,返回的句柄必须用 FindClose 关闭,否则会泄漏系统资源。判断目录时使用 FILE_ATTRIBUTE_DIRECTORY 属性,这比根据文件名猜测更可靠。
与标准库方案相比,Windows API代码量更大,错误处理也要手动调用 GetLastError 查看失败原因。但它的优势在于可以访问更多文件属性,例如隐藏文件、系统文件、只读标记等,这些在标准库中不一定直接暴露。如果只是扫描普通文件,标准库足够;如果需要处理特殊文件,底层API仍然不可替代。
四、POSIX 平台的递归实现
在Linux或macOS环境下,标准做法是使用 opendir、readdir 和 closedir。它们通过 DIR 结构和 dirent 条目枚举目录。相比Windows API,POSIX接口更简单,但路径拼接习惯使用正斜杠,句柄是 DIR* 而不是 HANDLE。
#include <dirent.h>
#include <sys/stat.h>
#include <string>
#include <iostream>
void list_dir(const std::string& path) {
DIR* dir = opendir(path.c_str());
if (!dir) {
return;
}
dirent* entry = nullptr;
while ((entry = readdir(dir)) != nullptr) {
std::string name = entry->d_name;
if (name == "." || name == "..") {
continue;
}
std::string full = path + "/" + name;
struct stat st;
if (stat(full.c_str(), &st) == -1) {
continue;
}
if (S_ISDIR(st.st_mode)) {
list_dir(full);
} else if (S_ISREG(st.st_mode)) {
std::cout << full << std::endl;
}
}
closedir(dir);
}
这里使用 stat 判断文件类型,因为某些文件系统的 d_type 可能为 DT_UNKNOWN,不能完全依赖目录项自带的信息。软链接处理默认情况下 stat 会跟随链接目标,如果链接指向目录,递归时可能产生循环。要避免这种情况,可以改用 lstat 判断链接本身,或者显式检查 S_ISLNK 后跳过符号链接。
POSIX版本的递归深度通常受栈大小限制。普通项目遍历几百层目录没有问题,但如果目录层级极深,建议改为显式栈迭代。和Windows API一样,这种手动实现的优势在于可控性,开发人员可以根据业务需求灵活加入文件类型过滤、权限判断和错误回调。
五、递归遍历中的错误处理和过滤技巧
无论使用哪种实现,生产代码都不应该只打印路径,还需要处理权限错误、路径过长、文件被删除等异常。一个稳妥的封装会把遍历函数拆成两层:外层负责统一捕获异常或错误码,内层负责纯递归逻辑。这样即使某个子目录无法访问,也只跳过该目录,不会中断整棵树的遍历。
过滤文件类型、按扩展名筛选、统计目录大小是常见的组合需求。可以给递归函数增加一个回调参数,例如 std::function<void(const fs::path&)>,遍历到普通文件时调用回调。这样批量改名、扫描代码文件、计算磁盘占用都能复用同一套遍历逻辑。需要跳过隐藏目录时,在进入子目录前检查目录名是否以点开头即可;需要限制深度时,增加一个整数参数,每递归一层加一,超过阈值就返回。
性能方面,recursive_directory_iterator 通常足够快,瓶颈主要在磁盘I/O和路径字符串构建。如果只是统计文件数量,可以避免构造完整路径,直接使用 entry.path().filename() 或迭代器自带的相对路径,减少内存分配。Windows API方案在目录层级很深时容易触发栈增长,递归深度超过几千层需要考虑改成显式栈,但普通项目一般不会遇到这个量级。
C++遍历文件夹递归遍历目录std::filesystem修改时间:2026-09-22 01:05:02