🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

文件系统操作 — stat、目录遍历

C语言学习教程

文件系统操作 — stat、目录遍历

你已经会用 fopenfprintffgets 读写文件内容了。但文件不止有”内容”——每个文件还有大小、权限、创建时间、所有者等元信息。目录本身也是一种特殊的文件。本章带你深入文件系统的内部世界。

为什么要深入文件系统?

想象你写了一个文件管理器,或者一个简单的备份脚本。你需要知道:

这些就是本章要解决的问题。


1. stat — 文件的”身份证”

在 Linux/POSIX 系统中,stat 系统调用能获取文件的全部元信息。

stat 结构体总览

#include <sys/stat.h>

struct stat {
    dev_t     st_dev;         // 设备 ID
    ino_t     st_ino;         // inode 编号(文件的"身份证号")
    mode_t    st_mode;        // 文件类型 + 权限位
    nlink_t   st_nlink;       // 硬链接数
    uid_t     st_uid;         // 所有者用户 ID
    gid_t     st_gid;         // 所有者组 ID
    dev_t     st_rdev;        // 设备号(如果是设备文件)
    off_t     st_size;        // 文件大小(字节)
    blksize_t st_blksize;     // 文件系统 I/O 块大小
    blkcnt_t  st_blocks;      // 分配的 512 字节块数量
    time_t    st_atime;       // 最后访问时间
    time_t    st_mtime;       // 最后修改时间
    time_t    st_ctime;       // 最后状态改变时间
};

我们不需要全部记住,重点关注:

字段含义使用场景
st_mode类型 + 权限判断是文件还是目录
st_size文件大小(字节)检查文件是否为空、是否太大
st_mtime最后修改时间增量备份(只复制有改动的文件)

第一个 stat 程序

#include <stdio.h>
#include <sys/stat.h>
#include <time.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "用法: %s <文件路径>\n", argv[0]);
        return 1;
    }

    struct stat st;
    if (stat(argv[1], &st) != 0) {
        perror("stat 失败");
        return 1;
    }

    printf("========== 文件信息 ==========\n");
    printf("  路径:      %s\n", argv[1]);
    printf("  大小:      %lld 字节", (long long)st.st_size);

    // 人性化大小显示
    if (st.st_size > 1024 * 1024) {
        printf(" (%.2f MB)\n", st.st_size / (1024.0 * 1024.0));
    } else if (st.st_size > 1024) {
        printf(" (%.2f KB)\n", st.st_size / 1024.0);
    } else {
        printf("\n");
    }

    printf("  inode:     %llu\n", (unsigned long long)st.st_ino);
    printf("  硬链接数:  %llu\n", (unsigned long long)st.st_nlink);
    printf("  权限模式:  %o (八进制)\n", st.st_mode & 07777);

    // 最后修改时间
    char time_buf[64];
    struct tm *tm_info = localtime(&st.st_mtime);
    strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info);
    printf("  修改时间:  %s\n", time_buf);

    // 判断文件类型
    printf("\n========== 类型判断 ==========\n");
    printf("  普通文件?   %s\n", S_ISREG(st.st_mode) ? "是" : "否");
    printf("  目录?       %s\n", S_ISDIR(st.st_mode) ? "是" : "否");
    printf("  符号链接?   %s\n", S_ISLNK(st.st_mode) ? "是" : "否");
    printf("  字符设备?   %s\n", S_ISCHR(st.st_mode) ? "是" : "否");
    printf("  块设备?     %s\n", S_ISBLK(st.st_mode) ? "是" : "否");
    printf("  FIFO管道?   %s\n", S_ISFIFO(st.st_mode) ? "是" : "否");
    printf("  Socket?     %s\n", S_ISSOCK(st.st_mode) ? "是" : "否");

    return 0;
}

运行示例:

$ gcc -o finfo stat-demo.c
$ ./finfo /etc/passwd
========== 文件信息 ==========
  路径:      /etc/passwd
  大小:      2890 字节 (2.82 KB)
  inode:     1048712
  硬链接数:  1
  权限模式:  644 (八进制)
  修改时间:  2026-06-10 08:15:42

========== 类型判断 ==========
  普通文件?
  目录?
  符号链接?
  字符设备?
  ...

$ ./finfo /tmp
========== 文件信息 ==========
  路径:      /tmp
  大小:      4096 字节 (4.00 KB)
  ...
========== 类型判断 ==========
  普通文件?
  目录?
  ...

类型判断宏全家桶

S_ISREG(m)   // Regular file — 普通文件
S_ISDIR(m)   // Directory — 目录
S_ISLNK(m)   // Symbolic link — 符号链接(软链接)
S_ISCHR(m)   // Character device — 字符设备(如 /dev/tty)
S_ISBLK(m)   // Block device — 块设备(如磁盘)
S_ISFIFO(m)  // FIFO (named pipe) — 命名管道
S_ISSOCK(m)  // Socket — 套接字

stat 系列函数对比

#include <sys/stat.h>

// stat: 如果路径是符号链接,追随到底,返回目标文件的信息
int stat(const char *path, struct stat *buf);

// lstat: 如果路径是符号链接,返回链接自身的信息(不追随)
int lstat(const char *path, struct stat *buf);

// fstat: 通过已打开的文件描述符获取信息
int fstat(int fd, struct stat *buf);

图解:

 有一个符号链接:  link.txt → real.txt

 stat("link.txt")    → 返回 real.txt 的信息
 lstat("link.txt")   → 返回 link.txt 自身的信息(是一个链接)

2. fseek 与 ftell — 文件的”时光机”

读文件不只是从头到尾流水账。fseek 让你在文件中随意跳跃,就像视频播放器的进度条。

文件位置指针

每个打开的文件都有一个当前位置指针。当执行 fgetc() 时,读取当前位置的字符,然后指针自动前进一格。

打开文件 "ABCDEFGHIJ" 时:
位置:  0   1   2   3   4   5   6   7   8   9
内容:  A   B   C   D   E   F   G   H   I   J

       fgetc() 读 'A',指针移到 1

       fseek(fp, 5, SEEK_SET) 后:
       A   B   C   D   E   F   G   H   I   J

       fgetc() 读 'F'

fseek 的三个”锚点”

int fseek(FILE *stream, long offset, int whence);
whence 常量含义通俗解释
SEEK_SET从文件开头算”跳到第 offset 个字节”
SEEK_CUR从当前位置算”往后(或往前)跳 offset 个”
SEEK_END从文件末尾算”倒数第 offset 个字节”

完整示例

#include <stdio.h>
#include <string.h>

int main() {
    // 创建一个测试文件
    const char *filename = "jump_test.txt";
    FILE *fp = fopen(filename, "w+");
    if (!fp) { perror("fopen"); return 1; }

    fprintf(fp, "0123456789ABCDEFGHIJ");
    // 文件内容(20个字符):
    // 位置:  0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19
    // 字符:  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  G  H  I  J

    printf("===== 跳跃实验 =====\n\n");

    // 实验 1:从头跳到第 10 个字节
    fseek(fp, 10, SEEK_SET);
    printf("fseek(10, SEEK_SET):  读到 '%c' (应该是 A)\n", fgetc(fp));

    // 实验 2:从当前位置跳 -5 个(往回跳)
    fseek(fp, -5, SEEK_CUR);
    printf("fseek(-5, SEEK_CUR):  读到 '%c' (应该是 7)\n", fgetc(fp));

    // 实验 3:文件末尾倒数第 3 个字节
    fseek(fp, -3, SEEK_END);
    printf("fseek(-3, SEEK_END):  读到 '%c' (应该是 H)\n", fgetc(fp));

    // 实验 4:获取文件大小
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    printf("\n文件大小: %ld 字节\n", size);

    // 实验 5:跳到开头
    rewind(fp);  // 等价于 fseek(fp, 0, SEEK_SET)
    printf("rewind 后第一个字符: '%c'\n", fgetc(fp));

    fclose(fp);
    remove(filename);   // 清理测试文件
    return 0;
}

输出:

===== 跳跃实验 =====

fseek(10, SEEK_SET):  读到 'A' (应该是 A)
fseek(-5, SEEK_CUR):  读到 '7' (应该是 7)
fseek(-3, SEEK_END):  读到 'H' (应该是 H)

文件大小: 20 字节
rewind 后第一个字符: '0'

实用技巧:读取文件最后几行

#include <stdio.h>
#include <stdlib.h>

// 从文件末尾向前读,寻找倒数第 N 个换行
void tail(FILE *fp, int n) {
    fseek(fp, 0, SEEK_END);
    long pos = ftell(fp);
    int lines = 0;

    // 从后往前扫描
    while (pos > 0 && lines <= n) {
        pos--;
        fseek(fp, pos, SEEK_SET);
        if (fgetc(fp) == '\n') lines++;
    }
    if (pos > 0) pos += 2;  // 跳过那个换行

    // 从 pos 读到文件末尾
    fseek(fp, pos, SEEK_SET);
    char buf[4096];
    while (fgets(buf, sizeof(buf), fp)) {
        printf("%s", buf);
    }
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "用法: %s <文件> [行数]\n", argv[0]);
        return 1;
    }

    FILE *fp = fopen(argv[1], "r");
    if (!fp) { perror("fopen"); return 1; }

    int n = (argc >= 3) ? atoi(argv[2]) : 10;
    printf("=== 最后 %d 行 ===\n", n);
    tail(fp, n);

    fclose(fp);
    return 0;
}

3. 目录遍历 — 走进文件夹迷宫

核心 API

#include <dirent.h>

DIR *opendir(const char *path);            // 打开目录
struct dirent *readdir(DIR *dir);          // 读取下一个条目
int closedir(DIR *dir);                    // 关闭目录
void rewinddir(DIR *dir);                  // 重置到开头

dirent 结构体:

struct dirent {
    ino_t          d_ino;       // inode 号
    off_t          d_off;       // 目录流中的偏移
    unsigned short d_reclen;    // 本条记录的长度
    unsigned char  d_type;      // 文件类型(不是所有文件系统都支持)
    char           d_name[];    // 文件名(以 '\0' 结尾)
};

列出目录内容(基础版)

#include <stdio.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    const char *path = (argc >= 2) ? argv[1] : ".";

    DIR *dir = opendir(path);
    if (dir == NULL) {
        perror("opendir 失败");
        return 1;
    }

    printf("目录 \"%s\" 的内容:\n", path);
    printf("──────────────────────────────────\n");

    struct dirent *entry;
    int count = 0;

    while ((entry = readdir(dir)) != NULL) {
        count++;

        // 文件类型图标
        const char *icon = "";
        switch (entry->d_type) {
            case DT_DIR:  icon = "📁"; break;
            case DT_REG:  icon = "📄"; break;
            case DT_LNK:  icon = "🔗"; break;
            case DT_FIFO: icon = "📡"; break;
            case DT_SOCK: icon = "🔌"; break;
            case DT_CHR:  icon = "⌨️";  break;
            case DT_BLK:  icon = "💾"; break;
            default:      icon = "❓"; break;
        }
        printf("%3d. %s %s\n", count, icon, entry->d_name);
    }

    printf("──────────────────────────────────\n");
    printf("共 %d 个条目\n", count);

    closedir(dir);
    return 0;
}

递归遍历目录树(类 tree 命令)

这是一个”上档次”的程序——自动深入所有子目录:

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

void walk_dir(const char *path, int depth) {
    DIR *dir = opendir(path);
    if (dir == NULL) {
        // 打不开就跳过(权限问题等)
        return;
    }

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

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

        // 构建完整路径
        char full_path[1024];
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", path, entry->d_name);

        // 获取文件信息
        struct stat st;
        if (stat(full_path, &st) != 0) {
            printf("├── %s (无法访问)\n", entry->d_name);
            continue;
        }

        if (S_ISDIR(st.st_mode)) {
            printf("├── 📁 %s/\n", entry->d_name);
            // 递归!
            walk_dir(full_path, depth + 1);
        } else {
            printf("├── %s  (%lld 字节)\n",
                   entry->d_name, (long long)st.st_size);
        }
    }
    closedir(dir);
}

int main(int argc, char *argv[]) {
    const char *root = (argc >= 2) ? argv[1] : ".";

    struct stat st;
    if (stat(root, &st) != 0 || !S_ISDIR(st.st_mode)) {
        fprintf(stderr, "错误: \"%s\" 不是一个有效目录\n", root);
        return 1;
    }

    printf("%s/\n", root);
    walk_dir(root, 1);
    return 0;
}

输出效果像这样:

./
├── 📁 src/
│   ├── main.c  (1250 字节)
│   ├── utils.c  (843 字节)
│   └── 📁 tests/
│       ├── test1.c  (567 字节)
│       └── test2.c  (432 字节)
├── Makefile  (156 字节)
└── README  (2048 字节)

跳过 . 和 .. 的必要性

每个目录都有两个特殊的”虚拟条目”:

名称含义
.当前目录自身
..父目录

如果不跳过它们,递归遍历会进入无限循环(...…)或者”跑出”起始目录(.... → … 一直到根目录)。

4. 临时文件

有些数据只需要在程序运行期间存在——比如中间计算结果、下载缓冲。tmpfile()tmpnam() 就是干这个的。

tmpfile — 全自动临时文件

#include <stdio.h>

int main() {
    // tmpfile() 创建一个临时文件,以 "wb+" 模式打开
    // 文件夹闭时自动删除,程序 crash 也会被系统清理
    FILE *tmp = tmpfile();
    if (tmp == NULL) {
        perror("tmpfile 失败");
        return 1;
    }

    // 写入一些数据
    fprintf(tmp, "第一行:敏感数据\n");
    fprintf(tmp, "第二行:临时计算结果=42\n");
    fprintf(tmp, "第三行:用完即弃\n");

    // 读取刚写入的内容
    rewind(tmp);
    char buf[256];
    while (fgets(buf, sizeof(buf), tmp)) {
        printf("读取: %s", buf);
    }

    // fclose 后文件自动消失
    fclose(tmp);
    printf("(临时文件已自动删除)\n");
    return 0;
}

tmpnam — 自己管理生命周期

#include <stdio.h>

int main() {
    char name[L_tmpnam];

    // 生成一个唯一的临时文件名
    tmpnam(name);
    printf("临时文件名: %s\n", name);

    // 自己打开、写入、关闭
    FILE *fp = fopen(name, "w");
    fprintf(fp, "手动管理的临时数据\n");
    fclose(fp);

    // 验证文件存在
    printf("检查文件是否存在...\n");
    fp = fopen(name, "r");
    if (fp) {
        printf("文件存在!内容: ");
        char buf[100];
        fgets(buf, sizeof(buf), fp);
        printf("%s", buf);
        fclose(fp);
    }

    // 手动删除
    remove(name);
    printf("已手动删除临时文件\n");

    return 0;
}

5. 一个综合工具:迷你 ls

把本章学的知识拼起来:

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

void print_perms(mode_t mode) {
    printf((mode & S_IRUSR) ? "r" : "-");
    printf((mode & S_IWUSR) ? "w" : "-");
    printf((mode & S_IXUSR) ? "x" : "-");
    printf((mode & S_IRGRP) ? "r" : "-");
    printf((mode & S_IWGRP) ? "w" : "-");
    printf((mode & S_IXGRP) ? "x" : "-");
    printf((mode & S_IROTH) ? "r" : "-");
    printf((mode & S_IWOTH) ? "w" : "-");
    printf((mode & S_IXOTH) ? "x" : "-");
}

int main(int argc, char *argv[]) {
    const char *path = (argc >= 2) ? argv[1] : ".";

    DIR *dir = opendir(path);
    if (dir == NULL) {
        perror("opendir");
        return 1;
    }

    printf("%-10s %8s  %s\n", "权限", "大小", "名称");
    printf("──────────────────────────────────────────\n");

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_name[0] == '.') continue;  // 跳过隐藏文件

        char full[1024];
        snprintf(full, sizeof(full), "%s/%s", path, entry->d_name);

        struct stat st;
        if (stat(full, &st) != 0) continue;

        // 类型 + 权限
        printf("%c", S_ISDIR(st.st_mode) ? 'd' : '-');
        print_perms(st.st_mode);

        // 大小
        printf(" %8lld", (long long)st.st_size);

        // 名字(目录后加 /)
        printf("  %s%s\n", entry->d_name,
               S_ISDIR(st.st_mode) ? "/" : "");
    }

    closedir(dir);
    return 0;
}

输出:

权限           大小  名称
──────────────────────────────────────────
-rw-r--r--     1250  main.c
-rw-r--r--      843  utils.c
drwxr-xr-x     4096  src/
-rw-r--r--      156  Makefile

本章掌握清单


下一站

你已经掌握了文件系统的核心操作。接下来的两章是项目实战——我们会用本章和前几章的知识,从零开始构建一个迷你 Shell 和一个文本处理器。还记得你运行 lsgrep 这些命令时那种”它们到底是怎么工作的”好奇心吗?马上你就会亲手实现它们!