19、文件目录操作:使用dirent.h遍历目录、创建/删除目录

说到文件操作,很多人第一反应就是读写文件内容。但实际项目中,你往往需要先搞定目录——也就是文件夹。我刚开始做嵌入式开发时,就踩过这个坑:程序跑起来发现日志写不进去,查了半天,原来是日志目录压根没创建。

目录操作在C语言里,主要靠 dirent.h 这个头文件。它提供了一套POSIX标准的接口,用来遍历目录、创建目录、删除目录。说白了,就是让你能在代码里像在终端敲 lsmkdirrmdir 一样操作文件夹。

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 知识体系总览

下面这张图,帮你把本章的核心逻辑串起来:

目录操作核心流程 opendir() readdir() 循环 处理每个条目 closedir() mkdir() 创建目录 rmdir() 删除目录 stat() 确认文件类型 ⚠ 跳过 . 和 .. ⚠ d_type 可能不可靠 💡 权限参数 0755 💡 rmdir 只能删空目录

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 返回值,结果程序静默失败,日志全丢了。从那以后,我每次创建目录都会检查返回值,并打印错误日志。

好了,目录操作就这些。说白了就是打开、读取、关闭三步走,加上创建和删除两个辅助操作。你写代码时多注意边界情况,基本不会出大问题。