14、目录操作:opendir/readdir/closedir(POSIX)、_findfirst/_findnext(Windows)、目录遍历

说到文件操作,很多人第一反应就是读写文件内容。但实际项目中,你经常需要先搞清楚「目录里有什么」——比如批量处理日志文件、遍历配置文件、或者查找特定后缀的图片。这时候,目录操作就派上用场了。

我个人习惯把目录操作分成两派:POSIX 派(Linux/macOS)和 Windows 派。它们思路差不多,但 API 名字和用法完全不同。今天咱们就把这两套都捋一遍。

14.1 POSIX 目录操作:opendir / readdir / closedir

这套接口是 Unix 世界的老祖宗了。你想想看,从 70 年代 Unix 诞生到现在,这套 API 几乎没变过。稳定得让人感动。

14.1.1 核心函数

函数 作用 返回值
opendir() 打开一个目录,返回目录流指针 DIR*NULL
readdir() 读取目录中的下一个条目 struct dirent*NULL
closedir() 关闭目录流 0 成功,-1 失败

这里有个关键点:readdir() 返回的 struct dirent 里,最重要的字段是 d_name(文件名)。它不包含路径,只有文件名本身。

避坑指南:我曾经在嵌入式设备上遍历目录时,忘了检查 d_name 是不是 ".""..",结果递归遍历时死循环了。嗯,这两个特殊目录一定要跳过。

14.1.2 完整示例:遍历目录并打印文件名

#include <stdio.h>
#include <dirent.h>

int main() {
    DIR *dir;
    struct dirent *entry;

    dir = opendir("/home/user/logs");
    if (dir == NULL) {
        perror("opendir 失败");
        return 1;
    }

    while ((entry = readdir(dir)) != NULL) {
        // 跳过当前目录和父目录
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        printf("找到文件: %s\n", entry->d_name);
    }

    closedir(dir);
    return 0;
}

这段代码我用了好多年,基本没改过。唯一要注意的是:opendir() 的参数必须是绝对路径或相对路径,不能是空字符串。

14.2 Windows 目录操作:_findfirst / _findnext

Windows 的目录操作 API 长得跟 POSIX 完全不一样。我第一次从 Linux 转到 Windows 开发时,找了半天才找到对应的函数。

14.2.1 核心函数

函数 作用 返回值
_findfirst() 查找第一个匹配的文件/目录 句柄(intptr_t)或 -1
_findnext() 查找下一个匹配的文件/目录 0 成功,-1 失败
_findclose() 关闭查找句柄 0 成功,-1 失败

Windows 这套 API 有个特点:它用「通配符」来匹配文件名。比如 "*.txt" 只找文本文件,"*.*" 找所有文件。POSIX 的 readdir() 则没有这个功能,你得自己用 fnmatch() 或正则来过滤。

小技巧:Windows 下 _findfirst() 返回的是一个句柄(不是指针),类型是 intptr_t。别把它当成普通 int 用,否则在 64 位系统上会出问题。我踩过这个坑。

14.2.2 完整示例:遍历目录并打印文件名

#include <stdio.h>
#include <io.h>

int main() {
    struct _finddata_t fileinfo;
    intptr_t handle;

    handle = _findfirst("C:\\logs\\*.*", &fileinfo);
    if (handle == -1) {
        perror("_findfirst 失败");
        return 1;
    }

    do {
        // 跳过当前目录和父目录
        if (strcmp(fileinfo.name, ".") == 0 ||
            strcmp(fileinfo.name, "..") == 0) {
            continue;
        }
        printf("找到文件: %s\n", fileinfo.name);
    } while (_findnext(handle, &fileinfo) == 0);

    _findclose(handle);
    return 0;
}

注意 Windows 路径用的是反斜杠 \\,在 C 字符串里要写成 "C:\\logs\\*.*"。另外 _finddata_t 结构体里还有 sizeattrib 等字段,可以获取文件大小和属性。

14.3 目录遍历:递归与广度优先

实际项目中,你往往需要遍历整个目录树——也就是子目录里的子目录。这时候有两种思路:

  • 深度优先(DFS):用递归,先处理子目录,再处理兄弟目录。代码简单,但嵌套太深可能栈溢出。
  • 广度优先(BFS):用队列,一层一层处理。不会栈溢出,但需要额外内存。

我个人习惯用递归,因为嵌入式系统里目录深度一般不超过 10 层,栈空间够用。但如果你在服务器上处理用户上传的压缩包,那还是用 BFS 稳妥。

14.3.1 递归遍历示例(POSIX)

void traverse_dir(const char *path) {
    DIR *dir;
    struct dirent *entry;
    char fullpath[1024];

    dir = opendir(path);
    if (dir == NULL) return;

    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        // 拼接完整路径
        snprintf(fullpath, sizeof(fullpath),
                 "%s/%s", path, entry->d_name);

        // 判断是否是目录(Linux 下用 d_type)
        if (entry->d_type == DT_DIR) {
            traverse_dir(fullpath);  // 递归
        } else {
            printf("文件: %s\n", fullpath);
        }
    }
    closedir(dir);
}

注意d_type 字段在有些文件系统(比如 XFS)上可能不支持,返回 DT_UNKNOWN。保险的做法是用 stat() 来判断。我在一个嵌入式 Linux 项目上就遇到过这个问题,当时排查了半天。

14.4 知识体系总览

下面这张图把目录操作的核心逻辑串起来了。你可以看到,无论 POSIX 还是 Windows,流程都是「打开 → 循环读取 → 关闭」。区别只在于 API 名字和参数。

目录操作核心流程 开始遍历目录 POSIX: opendir() Windows: _findfirst() 循环: readdir() / _findnext() 跳过 "." 和 ".." 是目录 → 递归遍历 (DFS 深度优先) 是文件 → 处理文件 (打印/复制/分析) closedir() / _findclose()

14.5 跨平台兼容建议

如果你写的代码要在 Linux 和 Windows 上都能跑,我建议用条件编译。说白了就是:

#ifdef _WIN32
    // Windows 代码
    #include <io.h>
    #define DIR_SEPARATOR '\\'
#else
    // POSIX 代码
    #include <dirent.h>
    #include <sys/stat.h>
    #define DIR_SEPARATOR '/'
#endif

这样一套代码,两个平台都能编译。我在一个跨平台工具库里就是这么干的,用了五六年没出过问题。

个人经验:Windows 下 _findfirst() 的通配符匹配是大小写不敏感的,而 Linux 下文件名大小写敏感。如果你在 Windows 上写 "*.TXT",它会匹配到 .txt 文件。但在 Linux 上,你得自己处理大小写转换。

14.6 总结

目录操作其实就三件事:打开、读取、关闭。POSIX 和 Windows 的 API 虽然长得不一样,但逻辑完全一致。你只要记住:

  • 永远跳过 "."".."
  • 递归遍历时注意栈深度
  • 跨平台代码用条件编译隔离

嗯,掌握了这些,目录操作这块你就基本拿下了。下次遇到批量处理文件的需求,直接上代码就行。


公众号:蓝海资料掘金营,微信deep3321