C++中如何遍历文件夹下的所有文件?递归遍历目录实现方法

来源:编程学习作者:北京网站建设头衔:草根站长
导读:本期聚焦于北京网站建设创作的《C++中如何遍历文件夹下的所有文件?递归遍历目录实现方法》,敬请观看详情。想用C++统计整个目录树里的文件数量,简单循环往往只能拿到当前层,子文件夹里的内容完全不出现。这个现象背后其实是目录的树形结构问题:根目录下有文件也有子目录,子目录里还可能继续嵌套,只遍历一层必然遗漏。C++17标准库引入std::filesystem之后,递归遍历有了统一且跨平台的方案,其中recursive_directory_iterator只需几行代码就能展开全部嵌套目录。但迭代器默认行为并不完美,遇到权限不足会抛异常,碰到目录符号链接还可能重复计数甚至死循环。本文从目录遍历的搜索方式讲起,分别给出基于std::filesystem、Windows API和POSIX接口的三套递归实现,对比它们对符号链接、异常处理、中文路径的支持差异,并补充按扩展名过滤、跳过隐藏目录等实用技巧,方便直接用于批量改名、代码扫描和目录体积统计。

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

C++中如何遍历文件夹下的所有文件?递归遍历目录实现方法

理解这个流程后,标准库和操作系统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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0922/60267.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。