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 结构体里还有 size、attrib 等字段,可以获取文件大小和属性。
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 名字和参数。
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 虽然长得不一样,但逻辑完全一致。你只要记住:
- 永远跳过
"."和".." - 递归遍历时注意栈深度
- 跨平台代码用条件编译隔离
嗯,掌握了这些,目录操作这块你就基本拿下了。下次遇到批量处理文件的需求,直接上代码就行。