19、文件目录操作:使用dirent.h遍历目录、创建/删除目录
说到文件操作,很多人第一反应就是读写文件内容。但实际项目中,你往往需要先搞定目录——也就是文件夹。我刚开始做嵌入式开发时,就踩过这个坑:程序跑起来发现日志写不进去,查了半天,原来是日志目录压根没创建。
目录操作在C语言里,主要靠 dirent.h 这个头文件。它提供了一套POSIX标准的接口,用来遍历目录、创建目录、删除目录。说白了,就是让你能在代码里像在终端敲 ls、mkdir、rmdir 一样操作文件夹。
19.1 核心头文件与数据结构
先看看我们要用到的家伙们:
#include <dirent.h>
#include <sys/stat.h> // 创建目录需要
#include <unistd.h> // rmdir 需要
核心数据结构就两个:
| 类型/函数 | 说明 |
|---|---|
DIR * |
目录流指针,类似 FILE* 之于文件 |
struct dirent |
目录项结构体,存着文件名、类型等信息 |
opendir() |
打开一个目录,返回 DIR* |
readdir() |
读取下一个目录项,返回 struct dirent* |
closedir() |
关闭目录流 |
mkdir() |
创建目录(在 sys/stat.h 里) |
rmdir() |
删除空目录 |
struct dirent 里最常用的两个成员:
d_name[]—— 文件名,字符串d_type—— 文件类型,比如DT_DIR表示目录,DT_REG表示普通文件
小提示: d_type 不是所有系统都支持(比如某些老版Linux或网络文件系统)。我一般会配合 stat() 来确认类型,更稳妥。
19.2 遍历目录:一个完整的例子
遍历目录,说白了就是打开一个文件夹,然后一条一条读里面的条目。来看代码:
#include <stdio.h>
#include <dirent.h>
void list_dir(const char *path) {
DIR *dir = opendir(path);
if (dir == NULL) {
perror("opendir 失败");
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
// 跳过 . 和 ..
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
if (entry->d_type == DT_DIR) {
printf("[目录] %s\n", entry->d_name);
} else if (entry->d_type == DT_REG) {
printf("[文件] %s\n", entry->d_name);
} else {
printf("[其他] %s\n", entry->d_name);
}
}
closedir(dir);
}
int main() {
list_dir(".");
return 0;
}
这里有几个点要注意:
opendir()返回 NULL 时,用perror()打印错误原因,方便调试readdir()返回 NULL 表示读完了,不是出错- 一定要跳过
.和..,否则你会看到当前目录和父目录
我踩过的坑: 有一次遍历目录做文件清理,忘了跳过 ".",结果递归删除时把自己当前目录给删了……嗯,从那以后我写遍历必加跳过逻辑。
19.3 创建与删除目录
创建目录用 mkdir(),删除用 rmdir()。看例子:
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
// 创建目录,权限 0755
if (mkdir("my_data", 0755) == 0) {
printf("目录创建成功\n");
} else {
perror("mkdir 失败");
}
// 删除目录(必须是空目录)
if (rmdir("my_data") == 0) {
printf("目录删除成功\n");
} else {
perror("rmdir 失败");
}
return 0;
}
关于权限参数,我习惯用 0755:
- 7 —— 所有者:读+写+执行
- 5 —— 同组用户:读+执行
- 5 —— 其他用户:读+执行
注意: rmdir() 只能删除空目录。如果目录里有文件,会返回 -1,errno 设置为 ENOTEMPTY。我曾经在清理临时文件时,以为删掉了目录,结果磁盘空间一直没释放……后来才发现是目录里还有隐藏文件。
19.4 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
19.5 实战:递归遍历目录树
很多时候,你需要把子目录里的文件也找出来。比如我做过一个项目,要统计某个目录下所有 .log 文件的总大小。这时候就得递归遍历。
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
void walk_dir(const char *base_path, int depth) {
DIR *dir = opendir(base_path);
if (dir == NULL) {
perror("opendir");
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
// 拼出完整路径
char full_path[1024];
snprintf(full_path, sizeof(full_path),
"%s/%s", base_path, entry->d_name);
// 缩进显示层级
for (int i = 0; i < depth; i++) {
printf(" ");
}
printf("|-- %s\n", entry->d_name);
// 如果是目录,递归进去
if (entry->d_type == DT_DIR) {
walk_dir(full_path, depth + 1);
}
}
closedir(dir);
}
int main() {
walk_dir(".", 0);
return 0;
}
这里有个细节:拼接路径时用 snprintf() 而不是 sprintf(),防止路径过长导致缓冲区溢出。我见过有人因为这个崩溃,排查了半天才发现是路径超了。
我的习惯: 递归深度最好加个限制,比如最多 10 层。否则遇到符号链接循环,你的程序就死循环了。我曾经在测试服务器上没加限制,结果遍历到一个循环链接目录,CPU 直接飙到 100%……
19.6 常见错误与避坑
- 权限不足: mkdir() 在只读目录下会失败。检查 errno 是否为 EACCES。
- 目录已存在: mkdir() 返回 -1,errno 为 EEXIST。创建前可以先 stat() 检查一下。
- rmdir 非空目录: 需要先遍历删除所有文件,再删目录。或者用
ftw()/nftw()函数(我一般自己写递归,更可控)。 - 路径分隔符: Windows 用反斜杠
\,Linux 用斜杠/。跨平台代码要注意。
我曾经犯过的错: 在嵌入式设备上,文件系统是只读的,但我没检查 mkdir 返回值,结果程序静默失败,日志全丢了。从那以后,我每次创建目录都会检查返回值,并打印错误日志。
好了,目录操作就这些。说白了就是打开、读取、关闭三步走,加上创建和删除两个辅助操作。你写代码时多注意边界情况,基本不会出大问题。