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 | 传入空指针、重复关闭 |
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 文件,或者只显示目录。
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 知识体系:目录操作的核心逻辑
下面这张图,是我自己总结的目录操作流程。你看一眼就能明白整个脉络:
这个流程其实很直观:打开目录 -> 判断是否成功 -> 循环读取每个条目 -> 处理 -> 读完关闭。我在实际项目中,经常在这个基础上加一个递归调用——如果遇到子目录,就继续遍历下去。
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 更可靠,几乎所有文件系统都支持。代价就是多一次系统调用,性能稍微差一点。但在我做过的项目中,这点开销完全可以接受。
14.6 总结一下
目录操作其实不复杂,但细节很多。我个人觉得,掌握好 opendir、readdir、closedir 这三个函数,再加上 stat 判断文件类型,就能应对 90% 的场景了。
记住几个关键点:
- 一定要跳过 "." 和 ".."
- readdir 返回的 d_name 是临时缓冲区,需要就复制
- 多线程环境下注意加锁
- 递归遍历时注意栈深度
嗯,这些经验都是我在实际项目中一点点积累出来的。希望你能少走一些弯路。
公众号:蓝海资料掘金营,微信deep3321