14、目录操作基础:opendir、readdir、closedir函数详解、目录遍历示例

说到文件操作,很多人第一反应就是读写文件内容。但实际项目中,我们经常需要跟目录打交道——比如扫描某个文件夹下的所有日志文件、批量处理配置文件、或者做一个简单的文件管理器。

我刚开始做嵌入式开发时,就踩过一个坑:设备上电后需要读取 /data/logs/ 下所有 .bin 文件,结果我硬编码了文件名列表。后来日志文件一多,程序就崩了。嗯,从那以后,我老老实实学会了用 opendir、readdir、closedir 这套 API。

说白了,目录操作就是让你能像遍历数组一样,遍历一个文件夹里的所有条目。Linux 下这套接口非常稳定,POSIX 标准,跨平台性也不错。

14.1 三个核心函数:打开、读取、关闭

目录操作和文件操作非常像。你想想看:打开文件用 fopen,关闭用 fclose。目录也一样——opendir 打开,closedir 关闭。中间多了一个 readdir,用来一条一条读取目录项。

先看函数原型:

#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);

这里有个关键类型:DIR。它是一个结构体指针,你可以把它理解成目录的“文件描述符”。opendir 成功返回一个非空指针,失败返回 NULL。

struct dirent 是 readdir 返回的结构体,里面最重要的字段就是 d_name——文件名。注意,它只包含文件名,不包含路径。

函数 作用 返回值 常见错误
opendir 打开目录流 成功返回 DIR*,失败返回 NULL 目录不存在、权限不足
readdir 读取下一个目录项 成功返回 struct dirent*,读完或出错返回 NULL 目录流已关闭、内存不足
closedir 关闭目录流 成功返回 0,失败返回 -1 传入空指针、重复关闭
注意:readdir 返回的指针指向内部静态存储区,不要 free 它。多次调用 readdir 会覆盖之前的内容。如果你需要保存所有文件名,记得用 strdup 复制一份。

14.2 目录遍历的标准套路

我个人的习惯是,写目录遍历代码时,先搭一个骨架:打开 -> 循环读取 -> 关闭。中间加个判断,跳过 "." 和 ".." 这两个特殊目录。

来看一个完整的示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>

int list_directory(const char *path) {
    DIR *dir = opendir(path);
    if (dir == NULL) {
        fprintf(stderr, "无法打开目录 %s: %s\n", path, strerror(errno));
        return -1;
    }

    struct dirent *entry;
    int count = 0;

    while ((entry = readdir(dir)) != NULL) {
        // 跳过当前目录和父目录
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        printf("%s\n", entry->d_name);
        count++;
    }

    closedir(dir);
    return count;
}

int main() {
    int n = list_directory("/tmp");
    printf("共列出 %d 个文件/目录\n", n);
    return 0;
}

这段代码很简单,但很实用。你想想看,实际项目中往往需要在这个基础上加过滤条件——比如只显示 .txt 文件,或者只显示目录。

小技巧:判断 entry->d_type 可以知道当前条目是文件还是目录。d_type == DT_DIR 表示目录,DT_REG 表示普通文件。但不是所有文件系统都支持这个字段(比如某些 FUSE 文件系统),保险起见可以用 stat 系统调用。

14.3 避坑指南:我踩过的几个坑

我曾经在一个项目里,用 readdir 遍历一个很大的目录(几万个文件),结果程序跑着跑着就卡死了。排查了半天,发现是有人在遍历过程中删除了文件,导致目录流内部指针错乱。

嗯,这里要记住:readdir 不是线程安全的。如果你在多线程环境下遍历目录,一定要加锁,或者每个线程用独立的 DIR*。

还有一个坑:readdir 返回的 d_name 长度是有限的。POSIX 标准规定 NAME_MAX 是 255,但实际文件系统可能支持更长的文件名。如果你要处理极端情况,建议用 readdir_r(可重入版本),不过它已经被标记为废弃了。现代做法是用 readdir 配合 strdup 复制。

核心要点:
  • opendir 失败时,用 errno 或 perror 查看具体原因
  • readdir 返回 NULL 时,可能是读完了,也可能是出错了。检查 errno 是否为 0
  • closedir 失败也要处理,虽然很少见
  • 不要假设目录项的顺序——readdir 返回的顺序取决于文件系统

14.4 知识体系:目录操作的核心逻辑

下面这张图,是我自己总结的目录操作流程。你看一眼就能明白整个脉络:

目录操作核心流程 开始 opendir(path) → DIR* 成功? 返回 -1 / 报错 while(readdir) 处理 entry->d_name closedir(dir)

这个流程其实很直观:打开目录 -> 判断是否成功 -> 循环读取每个条目 -> 处理 -> 读完关闭。我在实际项目中,经常在这个基础上加一个递归调用——如果遇到子目录,就继续遍历下去。

14.5 实战:递归遍历目录树

有时候我们需要遍历整个目录树,包括所有子目录。比如查找某个文件,或者统计所有文件大小。这时候递归就派上用场了。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>

void traverse_directory(const char *base_path, int depth) {
    DIR *dir = opendir(base_path);
    if (dir == NULL) {
        fprintf(stderr, "无法打开 %s: %s\n", base_path, strerror(errno));
        return;
    }

    struct dirent *entry;
    struct stat statbuf;
    char full_path[1024];

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

        // 构造完整路径
        snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name);

        // 获取文件信息
        if (stat(full_path, &statbuf) == -1) {
            fprintf(stderr, "stat 失败: %s\n", full_path);
            continue;
        }

        // 打印缩进
        for (int i = 0; i < depth; i++) {
            printf("  ");
        }

        if (S_ISDIR(statbuf.st_mode)) {
            printf("[DIR]  %s\n", entry->d_name);
            traverse_directory(full_path, depth + 1);
        } else {
            printf("[FILE] %s (%ld bytes)\n", entry->d_name, statbuf.st_size);
        }
    }

    closedir(dir);
}

int main() {
    traverse_directory(".", 0);
    return 0;
}

这段代码里,我用 stat 来判断文件类型,而不是依赖 d_type。为什么?因为 stat 更可靠,几乎所有文件系统都支持。代价就是多一次系统调用,性能稍微差一点。但在我做过的项目中,这点开销完全可以接受。

性能提示:如果你遍历的目录非常深(比如超过 100 层),递归可能会导致栈溢出。这时候可以考虑用栈数据结构模拟递归,或者限制最大深度。我在嵌入式设备上遇到过这个问题——栈空间只有 8KB,递归几层就崩了。

14.6 总结一下

目录操作其实不复杂,但细节很多。我个人觉得,掌握好 opendir、readdir、closedir 这三个函数,再加上 stat 判断文件类型,就能应对 90% 的场景了。

记住几个关键点:

  • 一定要跳过 "." 和 ".."
  • readdir 返回的 d_name 是临时缓冲区,需要就复制
  • 多线程环境下注意加锁
  • 递归遍历时注意栈深度

嗯,这些经验都是我在实际项目中一点点积累出来的。希望你能少走一些弯路。


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