C++ 遍历文件夹

当一个文件夹里存放了几十上百个文件时,我们不可能在代码里把每个文件名都逐一写出来。像统计文件数量、批量读取配置,或者筛选所有后缀为 .txt 的文档,都需要让程序自动把文件夹扫描一遍。

在 C++ 的 <filesystem> 标准库中,遍历文件夹主要用到 2 个目录迭代器。如果只扫描当前这一层目录,我们使用 directory_iterator;如果需要自动进入所有子文件夹扫描,则使用 recursive_directory_iterator。

遍历单层文件夹:directory_iterator

如果我们只关心某个文件夹下面直接存放了哪些文件和子目录,不需要进入子目录内部,可以使用 fs::directory_iterator,并配合范围 for 循环依次访问里面的每一个条目。

语法:

for (const auto& entry : fs::directory_iterator(dir)) {
    // 循环体
}

说明:

这里的 fs 是 std::filesystem 的常用别名。在每次循环中,entry 是一个 fs::directory_entry(目录条目)对象,代表文件夹里的某一个文件或子目录。

由于这个类型名比较长,我们平时常配合 auto 写成 const auto&,既能让编译器自动推导类型,又能省去每次循环复制对象的开销。拿到 entry 之后,调用 entry.path() 就能获取它对应的 fs::path 路径对象。

示例 1:遍历单层目录

#include <filesystem>
#include <fstream>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path dir = "docs";
    fs::remove_all(dir);

    // 准备测试目录与文件
    fs::create_directories(dir / "sub");
    std::ofstream f1(dir / "a.txt");
    std::ofstream f2(dir / "b.txt");
    std::ofstream f3(dir / "sub" / "inner.txt");
    f1.close();
    f2.close();
    f3.close();

    // 遍历 docs 当前这一层的条目
    for (const auto& entry : fs::directory_iterator(dir)) {
        std::cout << entry.path().generic_string() << '\n';
    }

    return 0;
}

运行结果如下。

docs/sub
docs/a.txt
docs/b.txt

分析:

在这个例子中,我们先建好测试目录并创建了 3 个测试文件,接着使用 fs::directory_iterator(dir) 扫描 docs 目录。程序输出了 a.txt、b.txt 以及子目录 sub,但没有输出 sub 里面的 inner.txt,因为该迭代器只扫描当前这一层目录,不会进入子文件夹内部。

另外小伙伴们要注意,遍历目录时操作系统会自动跳过代表当前目录的 "." 和上一级目录的 "..",并且不同系统下条目的默认输出顺序可能略有差异。

在遍历时区分文件与子目录

在扫描文件夹的过程中,我们经常需要区分当前拿到的 entry 到底是一个普通文件,还是一个子目录。

虽然可以调用前面介绍的 fs::is_regular_file() 函数来判断,但 entry 对象自身就已经提供了判断类型和查询大小的成员函数。由于系统在扫描目录时通常已经缓存了这些信息,直接调用 entry 的成员函数不需要再去磁盘查第 2 遍,效率更高。

entry 对象常用的成员函数如下表所示。

目录条目常用函数
成员函数 作用说明
entry.path() 获取当前条目的 fs::path 路径对象
entry.is_regular_file() 判断当前条目是否为普通文件
entry.is_directory() 判断当前条目是否为目录
entry.file_size() 获取普通文件的字节大小,仅对普通文件调用

下面我们来看一个结合类型判断与大小查询的例子。

示例 2:区分文件与子目录

#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>

namespace fs = std::filesystem;

int main() {
    fs::path dir = "assets";
    fs::remove_all(dir);

    // 创建子目录 images 与 7 字节文件 note.txt
    fs::create_directories(dir / "images");
    std::ofstream outFile(dir / "note.txt");
    outFile << "lvyenet";
    outFile.close();

    for (const auto& entry : fs::directory_iterator(dir)) {
        std::string name = entry.path().filename().string();

        if (entry.is_directory()) {
            std::cout << "[目录] " << name << '\n';
        } else if (entry.is_regular_file()) {
            auto size = entry.file_size();
            std::cout << "[文件] " << name << " (" << size << " 字节)\n";
        }
    }

    return 0;
}

运行结果如下。

[目录] images
[文件] note.txt (7 字节)

分析:

在循环内部,entry.path().filename().string() 表示去掉前面的父目录前缀,只提取出当前条目本身的名字。

随后我们通过 entry.is_directory() 和 entry.is_regular_file() 将子目录和普通文件分开处理,确认是普通文件后再调用 entry.file_size() 输出它的字节数。

递归遍历文件夹:recursive_directory_iterator

如果一个文件夹下面包含多层嵌套的子目录,我们希望把所有层级里的文件全部找出来,只需要把迭代器换成 fs::recursive_directory_iterator 即可。其中,recursive 是英文 “递归” 的意思。

语法:

for (const auto& entry : fs::recursive_directory_iterator(dir)) {
    // 循环体
}

说明:

当 fs::recursive_directory_iterator 遇到子目录时,不仅会访问这个子目录本身,还会自动进入子目录内部,把里面的内容也一层层全部扫描完。在实际开发中,我们经常使用它递归扫描整个项目目录,筛选出特定扩展名的文件。

示例 3:递归筛选 .txt 文件

#include <filesystem>
#include <fstream>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path dir = "project";
    fs::remove_all(dir);

    // 准备多层目录与不同后缀的文件
    fs::path guideDir = dir / "docs" / "guide";
    fs::create_directories(dir / "src");
    fs::create_directories(guideDir);

    std::ofstream f1(dir / "readme.txt");
    std::ofstream f2(dir / "src" / "main.cpp");
    std::ofstream f3(guideDir / "intro.txt");
    f1.close();
    f2.close();
    f3.close();

    std::cout << "找到的 .txt 文件如下:\n";
    for (const auto& entry : fs::recursive_directory_iterator(dir)) {
        fs::path p = entry.path();
        if (entry.is_regular_file() && p.extension() == ".txt") {
            std::cout << p.generic_string() << '\n';
        }
    }

    return 0;
}

运行结果如下。

找到的 .txt 文件如下:
project/docs/guide/intro.txt
project/readme.txt

分析:

从运行结果可以看出,fs::recursive_directory_iterator(dir) 自动进入了 src 和 docs/guide 等所有子目录,把藏在多层子目录里的 intro.txt 也找了出来。

在循环内部,我们先通过 entry.is_regular_file() 确认当前条目是普通文件,再判断 p.extension() 是否等于 ".txt"。只有两个条件同时成立时才输出路径,这样就过滤掉了文件夹以及后缀为 .cpp 的代码文件。

对于上面这个例子,小伙伴们可以把条件里的 ".txt" 改成 ".cpp" 试一下,看看筛选出来的结果会发生什么变化。

提示 扫描真实磁盘上的大目录时,如果遇到没有访问权限的系统子文件夹,默认的递归遍历会直接抛出异常中断。大家可以在第 2 个参数传入 fs::directory_options::skip_permission_denied,让迭代器自动跳过无权限目录继续扫描。

C++ 遍历文件夹的注意事项

在实际项目中使用目录迭代器时,还有 2 个容易踩坑的细节需要小伙伴们特别注意。

1. 对遍历结果按名称排序

无论是 directory_iterator 还是 recursive_directory_iterator,它们访问文件的先后顺序都是由底层文件系统决定的,默认并不保证按文件名从 A 到 Z 排列。

如果业务上要求按文件名顺序依次处理,我们可以先在循环中把遍历到的路径收集到 std::vector 容器里,再调用 std::sort() 函数排好序即可。

示例 4:按文件名升序排列

#include <algorithm>    // 提供 std::sort()
#include <filesystem>
#include <fstream>
#include <iostream>
#include <vector>

namespace fs = std::filesystem;

int main() {
    fs::path dir = "sort_demo";
    fs::remove_all(dir);
    fs::create_directory(dir);

    std::ofstream f1(dir / "c.txt");
    std::ofstream f2(dir / "a.txt");
    std::ofstream f3(dir / "b.txt");
    f1.close();
    f2.close();
    f3.close();

    // 1. 将遍历到的路径存入 vector 容器
    std::vector<fs::path> files;
    for (const auto& entry : fs::directory_iterator(dir)) {
        files.push_back(entry.path());
    }

    // 2. 按路径升序排列后依次输出
    std::sort(files.begin(), files.end());
    for (const auto& p : files) {
        std::cout << p.filename().string() << '\n';
    }

    return 0;
}

运行结果如下。

a.txt
b.txt
c.txt

分析:

在上面的代码中,我们先通过 files.push_back(entry.path()); 把目录下的所有文件路径收集到 std::vector<fs::path> 容器中,再调用 std::sort(files.begin(), files.end()); 按路径字典序升序排列。

这样一来,无论底层文件系统的默认扫描顺序是什么,程序最终都能稳定按 a.txt、b.txt、c.txt 的顺序依次输出。

2. 遍历时安全增删文件

在遍历某个文件夹的 for 循环内部,千万不要直接向该文件夹新建文件,也不要直接调用 fs::remove() 删除当前正在遍历的文件。因为一边扫描目录、一边修改目录结构,很容易导致底层目录迭代器失效或漏扫文件。

如果确实需要批量清理满足条件的文件,我们可以分两步来处理:① 先在遍历循环里把待删除的文件路径收集到 std::vector<fs::path> 中;② 等目录遍历循环彻底结束后,再遍历这个容器逐一执行删除。参考代码如下:

// 1. 先收集所有 .tmp 临时文件路径
std::vector<fs::path> toDelete;
for (const auto& entry : fs::directory_iterator(dir)) {
    fs::path p = entry.path();
    if (entry.is_regular_file() && p.extension() == ".tmp") {
        toDelete.push_back(p);
    }
}

// 2. 遍历结束后再统一删除
for (const auto& filePath : toDelete) {
    fs::remove(filePath);
}

注意 如果传给目录迭代器的路径在磁盘上不存在,或者指向的是普通文件而非目录,程序运行时会直接抛出异常。在遍历外部传入的路径之前,小伙伴们可以先调用 fs::is_directory() 确认目录存在,或者向迭代器第 2 个参数传入 std::error_code 对象避免抛出异常。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号