33. 文件IO (4) 二进制文件操作与结构体存储 文件路径与目录操作

📘 第五章:二进制文件操作与结构体存储(详细讲解)


一、🧩 什么是二进制文件(Binary File)

🔹 基本定义:

二进制文件 是以字节为单位存储的文件,数据按原始二进制格式保存,而非可见字符形式。

与之对比:

文件类型 特征 读写方式 示例
文本文件 存储字符(ASCII/UTF-8) fprintf() / fscanf() / fgets() .txt .csv .ini
二进制文件 存储原始数据(byte流) fwrite() / fread() .bin .dat .img .binlog

📘 举例说明:

数据 文本文件存储 二进制文件存储
int 100 '1''0''0'(3字节) 0x64(1字节)
float 3.14 '3''.''1''4'(4字节) 0xC3 0xF5 0x48 0x40(IEEE754格式)

🔹 结论:

  • 二进制文件 更紧凑读写更快
  • 不可直接查看
  • 适合存放传感器数据、结构体、EEPROM镜像、日志缓冲等。

二、📤 写入结构体数据 ------ fwrite()

c 复制代码
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:要写入的内存首地址
  • size:每个元素的字节大小
  • nmemb:元素个数
  • stream:文件指针

✅ 示例:写入结构体到二进制文件

c 复制代码
#include <stdio.h>
#include <string.h>

typedef struct {
    int id;
    char name[20];
    float temperature;
} SensorData;

int main(void) {
    FILE *fp = fopen("sensor.bin", "wb");
    if (!fp) return -1;

    SensorData s1 = {1001, "TempSensor1", 36.5};
    SensorData s2 = {1002, "TempSensor2", 38.7};

    fwrite(&s1, sizeof(SensorData), 1, fp);
    fwrite(&s2, sizeof(SensorData), 1, fp);

    fclose(fp);
    return 0;
}

🔹 说明:

  • "wb" 表示写入二进制文件(write binary);
  • fwrite() 会按内存中的原始字节布局写入;
  • 每次写入一个结构体(sizeof(SensorData) 字节)。

📘 输出结果(十六进制查看):

bash 复制代码
xxd sensor.bin

可能显示类似:

复制代码
e9 03 00 00 54 65 6d 70 53 65 6e 73 6f 72 31 00 00 ... 00 42 10

fwrite 是 C 语言标准库中用于二进制数据块写入的核心函数,专门用于将内存中连续的数据块(如数组、结构体、二进制流)写入文件流,其设计不依赖数据的文本格式,仅按字节精确操作,是处理非文本数据(如图片、音频、结构体)的首选工具。

一、函数原型与核心功能

c 复制代码
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

核心功能 :从内存地址 ptr 中,读取 nmemb 个"数据项"(每个数据项大小为 size 字节),并将这些数据写入文件流 stream

  • 本质是"批量字节拷贝":总写入字节数 = size * nmemb(若写入成功)。
  • 不解析数据内容:无论数据中包含 \0\n 等特殊字节,均按普通字节处理(区别于文本函数的格式限制)。

二、参数详解

参数 含义与作用 关键细节
const void *ptr 指向待写入写入数据的内存缓冲区(源数据地址)。 需确保证缓冲区有效且足够大(至少 size * nmemb 字节),const 表示不修改源数据。
size_t size 单个数据项的字节数(如 sizeof sizeof(int)sizeof(Student))。 必须用 sizeof 计算(避免硬编码,确保证跨平台兼容性,如 32/64 位系统的 int 可能不同)。
size_t nmemb 要写入的数据项数量。 总请求写入字节数 = size * nmemb(如 size=4nmemb=5 表示写入 20 字节)。
FILE *stream 目标文件流(需用可写模式打开,如 w/wb/a/ab 等)。 若为文本模式(w),Windows 下可能转换 \n\r\n,破坏二进制数据,建议用 wb 二进制模式

三、返回值:成功写入的"数据项数量"

返回值是成功写入的完整数据项个数(而非字节数),这是判断写入是否成功的核心依据:

  • 若返回值 = nmemb:所有数据项均成功写入(总字节数 = size * nmemb)。
  • 若返回值 < nmemb:写入部分或全部失败(可能因磁盘满、权限不足等),需用 ferror(stream) 判断是否为错误,或 feof(stream) 判断是否因文件结束(罕见,写入时较少出现)。
  • 若返回值 = 0:未写入任何数据(可能参数错误或文件不可写)。

四、典型使用场景

场景 1:写入结构体数组(最常见)

将一个学生结构体数组写入文件,保留完整二进制信息(包括字符串中的 \0 等):

c 复制代码
#include <stdio.h>
#include <string.h>

// 定义学生结构体
typedef struct {
    int id;
    char name[20];  // 固定长度字符串
    float score;
} Student;

int main() {
    // 准备 3 个学生数据
    Student students[3] = {
        {101, "Alice", 92.5},
        {102, "Bob", 88.0},
        {103, "Charlie", 79.5}
    };

    // 打开文件(二进制写模式,避免证数据不被转换)
    FILE *fp = fopen("students.bin", "wb");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    // 写入 3 个 Student 类型数据项,每个大小为 sizeof(Student)
    size_t written = fwrite(students, sizeof(Student), 3, fp);
    if (written != 3) {  // 检查查是否全部写入
        perror("写入失败");
        fclose(fp);
        return 1;
    }
    printf("成功写入 %zu 个学生数据\n", written);  // 输出:3

    fclose(fp);  // 关闭文件,自动刷新缓冲区
    return 0;
}
场景 2:写入二进制数组(如图片像素)

将一个 int 数组(模拟图像像素素)写入文件:

c 复制代码
#include <stdio.h>

int main() {
    int pixels[] = {0xFF0000, 0x00FF00, 0x0000FF};  // 红、绿、蓝像素
    int count = sizeof(pixels) / sizeof(pixels[0]);  // 数组长度:3

    FILE *fp = fopen("pixels.bin", "wb");
    if (fp == NULL) {
        perror("打开失败");
        return 1;
    }

    // 写入整个数组:每个元素 4 字节(sizeof(int)),共 3 个元素
    size_t written = fwrite(pixels, sizeof(int), count, fp);
    if (written == count) {
        printf("成功写入 %zu 个像素(共 %zu 字节)\n", written, written * sizeof(int));
    }

    fclose(fp);
    return 0;
}

五、与其他写入函数的核心区别

函数 核心差异 适用场景
fwrite 按二进制数据块写入,不解析内容,支持任意类型 结构体、数组、二进制文件(图片/音频)
fputs 仅写入以 \0 结尾的字符串,忽略 \0 之后内容 文本字符串写入(如配置文件行)
fputc 逐个个字符写入,效率低 逐字符处理(如加密转换后写入)

六、关键注意事项

  1. 文件模式必须匹配

    写入二进制数据时,必须用 wb(二进制写)或 ab(二进制追加)模式 ,否则 Windows 等系统会自动转换 \n\r\n,导致二进制数据(如图片、结构体)损坏。

  2. 返回依赖依赖文本格式
    fwrite 不识别字符串的 \0 终止符,若写入字符串(如 char str[] = "hello"),会连 \0 一起同写入(而 fputs 会忽略 \0)。如需写入字符串内容(不含 \0),需用 fwrite(str, 1, strlen(str), fp)

  3. 缓冲区与落盘
    fwrite 写入的数据先先存入 C 标准库的用户态缓冲区,需通过 fflush(fp) 刷新到内核缓冲区,或 fsync(fileno(fp)) 强制证写入磁盘(避免程序统崩溃丢失数据)。

  4. 返回值必须检查

    即使返回值 < nmemb,也可能写入了部分数据(需根据返回值处理部分写入的情况),不能直接忽略。

总结

fwrite 是处理二进制数据的"主力函数",其核心优势是按字节精确操作连续内存块,不依赖数据的文本格式,适用于结构体、数组、二进制文件等场景。使用时需注意:

  • 用二进制模式打开文件(wb);
  • 通过返回值判断写入是否完整;
  • 结合 fflush/fsync 确保数据可靠落盘。

它与 fread 配合,是实现二进制数据持久化的核心工具。


三、📥 读取结构体数据 ------ fread()

c 复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 从文件中读取 nmemb 个对象,每个大小为 size 字节;
  • 将数据存入 ptr 指向的内存区域。

✅ 示例:读取结构体

c 复制代码
#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float temperature;
} SensorData;

int main(void) {
    FILE *fp = fopen("sensor.bin", "rb");
    if (!fp) return -1;

    SensorData s;
    while (fread(&s, sizeof(SensorData), 1, fp) == 1) {
        printf("ID=%d, 名称=%s, 温度=%.2f\n", s.id, s.name, s.temperature);
    }

    fclose(fp);
    return 0;
}

📘 结果:

复制代码
ID=1001, 名称=TempSensor1, 温度=36.50
ID=1002, 名称=TempSensor2, 温度=38.70

🔹 注意:

  • 使用 "rb" 模式(read binary);
  • 一次读取一个结构体;
  • 若文件结构与结构体定义不匹配,会出现数据错位。

fread 是 C 语言标准库中用于从文件流读取二进制数据块的核心函数,专门用于将文件中的连续字节数据读取到内存缓冲区(如数组、结构体),其操作不依赖数据的文本格式,仅按字节精确拷贝,是读取非文本数据(如图片、音频、结构体)的首选工具。

一、函数原型与核心功能

c 复制代码
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

核心功能 :从文件流 stream 中读取 nmemb 个"数据项"(每个数据项大小为 size 字节),并将这些数据存储到内存地址 ptr 指向的缓冲区中。

  • 本质是"批量字节拷贝":总尝试读取字节数 = size * nmemb(实际读取字节数 = 成功读取的项数 × size)。
  • 不解析数据内容:无论文件中包含 \0\n 等特殊字节,均按普通字节读取(区别于文本函数的格式解析)。

二、参数详解

参数 含义与作用 关键细节
void *ptr 指向内存缓冲区(用于存储读取到的数据)。 需提前分配足够内存(至少 size * nmemb 字节),避免溢出;支持任意类型指针(如 int*struct*)。
size_t size 单个数据项的字节数(如 sizeof(int)sizeof(Student))。 必须用 sizeof 计算(避免硬编码,确保跨平台兼容性,如 32/64 位系统的 long 长度可能不同)。
size_t nmemb 要读取的数据项数量。 总请求读取字节数 = size * nmemb(如 size=8nmemb=10 表示尝试读取 80 字节)。
FILE *stream 源文件流(需用可读模式打开,如 r/rb/r+ 等)。 若为文本模式(r),Windows 下可能将 \r\n 转换为 \n,破坏二进制数据,建议用 rb 二进制模式

三、返回值:成功读取的"数据项数量"

返回值是成功读取的完整数据项个数(而非字节数),这是判断读取是否成功的核心依据:

  • 若返回值 = nmemb:所有请求的数据项均成功读取(总字节数 = size * nmemb)。
  • 若返回值 < nmemb:可能是文件已到末尾(feof(stream) 为真),或读取错误(ferror(stream) 为真)。
  • 若返回值 = 0:未读取任何数据(可能文件为空、已到末尾,或发生错误)。

四、典型使用场景

场景 1:读取结构体数组(与 fwrite 配合)

读取之前用 fwrite 写入的学生结构体数组,恢复完整数据:

c 复制代码
#include <stdio.h>
#include <string.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main() {
    // 打开文件(二进制读模式,避免数据转换)
    FILE *fp = fopen("students.bin", "rb");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    Student students[3];  // 缓冲区:存储 3 个学生数据
    // 读取 3 个 Student 类型数据项,每个大小为 sizeof(Student)
    size_t read = fread(students, sizeof(Student), 3, fp);
    
    // 判断读取结果
    if (read != 3) {
        if (feof(fp)) {
            printf("文件结束,仅读取到 %zu 个学生\n", read);
        } else if (ferror(fp)) {
            perror("读取错误");
            fclose(fp);
            return 1;
        }
    }

    // 打印读取到的数据
    for (size_t i = 0; i < read; i++) {
        printf("ID: %d, 姓名: %s, 分数: %.1f\n",
               students[i].id,
               students[i].name,
               students[i].score);
    }

    fclose(fp);
    return 0;
}

输出(若文件完整):

复制代码
ID: 101, 姓名: Alice, 分数: 92.5
ID: 102, 姓名: Bob, 分数: 88.0
ID: 103, 姓名: Charlie, 分数: 79.5
场景 2:读取二进制数组(如图片像素)

读取之前写入的像素数组(int 类型):

c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("pixels.bin", "rb");
    if (fp == NULL) {
        perror("打开失败");
        return 1;
    }

    int pixels[3];  // 缓冲区:存储 3 个像素
    // 读取 3 个 int 类型数据项(每个 4 字节)
    size_t read = fread(pixels, sizeof(int), 3, fp);
    if (read == 3) {
        printf("像素值:%#X, %#X, %#X\n", pixels[0], pixels[1], pixels[2]);
    }

    fclose(fp);
    return 0;
}

输出

复制代码
像素值:0XFF0000, 0X00FF00, 0X0000FF

五、与其他读取函数的核心区别

函数 核心差异 适用场景
fread 按二进制数据块读取,不解析内容,支持任意类型 结构体、数组、二进制文件(图片/音频)
fgets 读取字符串(遇 \n 或缓冲区满停止),带 \n 文本行读取(如配置文件行)
fgetc 逐字符读取,效率低 逐字符解析(如语法分析)

六、关键注意事项

  1. 文件模式必须匹配

    读取二进制数据时,必须用 rb(二进制读)模式 ,否则 Windows 等系统会将 \r\n 转换为 \n,导致二进制数据(如结构体、图片)解析错误。

  2. 缓冲区大小需足够

    内存缓冲区 ptr 必须至少能容纳 size * nmemb 字节,否则会导致内存溢出(如用 char buf[5] 读取 size=3, nmemb=2 的 6 字节数据)。

  3. 返回值必须结合 feof/ferror 判断

    当返回值 < nmemb 时,需用 feof(stream) 确认是否因文件结束(正常情况),或 ferror(stream) 确认是否因错误(如磁盘故障)。

  4. 跨平台结构体读取注意对齐

    不同编译器的结构体对齐方式可能不同(如成员间的填充字节),导致跨平台读取 fwrite 写入的结构体时出现数据错位。解决:用 #pragma pack 统一对齐(如 #pragma pack(1) 取消填充)。

  5. 不处理字符串终止符

    若读取的是字符串(如 char 数组),fread 会读取所有字节(包括 \0 前后的内容),不会自动添加 \0 终止符(需手动处理)。

总结

fread 是处理二进制数据读取的"主力函数",其核心优势是按字节精确读取连续数据块,不依赖文本格式,适用于结构体、数组、二进制文件等场景。使用时需注意:

  • 用二进制模式打开文件(rb);
  • 检查返回值并结合 feof/ferror 判断结果;
  • 确保缓冲区大小足够,跨平台时处理结构体对齐问题。

它与 fwrite 成对使用,是实现二进制数据持久化(写入文件→读取恢复)的核心工具。


四、🔄 字节序(Endianness)问题

📘 概念:

字节序 是指多字节数据(如 intfloat)在内存中存储的高低字节顺序。

类型 定义 内存存储顺序(以 0x12345678 为例)
大端(Big Endian) 高位字节存在低地址 12 34 56 78
小端(Little Endian) 低位字节存在低地址 78 56 34 12

📍 说明:

  • PC(x86/x64)通常为小端;
  • 某些 ARM、DSP、网络协议为大端;
  • 二进制文件在跨平台时若字节序不一致,数据会错乱。

✅ 检测系统字节序

c 复制代码
int check_endian() {
    unsigned int x = 0x12345678;
    return (*(unsigned char*)&x == 0x78) ? 0 : 1;
}
// 0 = 小端,1 = 大端

这个 check_endian 函数的作用是检测当前系统的字节序(Endianness),即判断系统是"小端序(Little-Endian)"还是"大端序(Big-Endian)"。

字节序的核心概念

字节序指的是多字节数据在内存中的存储顺序 。对于一个多字节整数(如 32 位的 0x12345678),其高位字节(0x12)和低位字节(0x78)在内存中的存储位置决定了字节序:

  • 小端序(Little-Endian):低位字节存放在内存的低地址处,高位字节存放在高地址处。
  • 大端序(Big-Endian):高位字节存放在内存的低地址处,低位字节存放在高地址处。

函数工作原理

函数通过以下步骤判断字节序:

  1. 定义一个 32 位无符号整数

    c 复制代码
    unsigned int x = 0x12345678;

    这个整数的 4 个字节从高位到低位依次是:0x12(最高位)、0x340x560x78(最低位)。

  2. 将整数地址强制转换为单字节指针

    c 复制代码
    *(unsigned char*)&x
    • &x 取整数 x 的内存首地址(低地址)。
    • (unsigned char*)&x 将地址强制转换为 unsigned char* 类型(单字节指针),此时解引用(*)会只读取该地址处的第一个字节(低地址的字节)。
  3. 判断第一个字节的值

    • 若第一个字节是 0x78(最低位字节):说明低位字节存放在低地址 → 小端序 ,函数返回 0
    • 若第一个字节是 0x12(最高位字节):说明高位字节存放在低地址 → 大端序 ,函数返回 1

内存存储示意图

x = 0x12345678 为例,两种字节序的内存布局(假设内存地址从低到高为 addraddr+1addr+2addr+3):

字节序 addr(低地址) addr+1 addr+2 addr+3(高地址) 解引用 *(unsigned char*)&x 的结果 函数返回值
小端序 0x78 0x56 0x34 0x12 0x78 0
大端序 0x12 0x34 0x56 0x78 0x12 1

总结

  • 函数返回 0 → 系统是小端序(如 x86/x86_64 架构的 PC 机、大多数服务器)。
  • 函数返回 1 → 系统是大端序(如部分嵌入式系统、网络字节序默认是大端序)。

这是检测字节序的经典方法,核心是通过单字节指针访问多字节整数的首地址,根据首字节的值判断高低位的存储顺序。


✅ 解决方案:保存时统一字节序

常见做法:以大端为标准存文件(网络协议中称为 "network byte order")。

使用 <arpa/inet.h>

c 复制代码
uint32_t a = 0x12345678;
uint32_t b = htonl(a); // Host → Network (大端)
uint32_t c = ntohl(b); // Network → Host

📘 嵌入式建议:

  • 保存或传输前统一字节序;
  • 结构体成员不要直接写入文件(可逐字段转换)。

五、⚙️ 嵌入式中的结构体存储与文件镜像

嵌入式系统常常需要:

  • 保存系统配置;
  • 记录传感器数据;
  • 制作 EEPROM / Flash 映像文件。

这些数据通常使用 结构体形式存储 ,然后整体写入 Flash 或文件

✅ 示例:配置文件镜像保存

c 复制代码
typedef struct {
    char device_name[16];
    uint16_t version;
    float calib_value;
    uint8_t reserved[8];
} DeviceConfig;

void save_config(DeviceConfig *cfg) {
    FILE *fp = fopen("config.bin", "wb");
    fwrite(cfg, sizeof(DeviceConfig), 1, fp);
    fclose(fp);
}

void load_config(DeviceConfig *cfg) {
    FILE *fp = fopen("config.bin", "rb");
    fread(cfg, sizeof(DeviceConfig), 1, fp);
    fclose(fp);
}

📘 嵌入式应用:

  • 把配置结构体存放在 EEPROM / Flash;
  • 启动时读取,掉电后仍能恢复;
  • 常用于 Bootloader、参数存储区。

六、📚 二进制文件的调试与可视化

🔍 十六进制查看工具

在 Linux:

bash 复制代码
xxd file.bin
hexdump -C file.bin

在 Windows:

  • HxD
  • WinHex
  • 010 Editor(支持结构体模板)

📘 调试技巧:

  • 验证结构体偏移;
  • 检查字节序;
  • 分析文件头格式;
  • 手动编辑 EEPROM 镜像。

七、🧱 常见陷阱与经验总结

问题 原因 解决方案
文件数据错位 结构体未对齐 #pragma pack(1) 或逐字段写
数据不匹配 结构体定义变化 增加版本字段
字节序不一致 平台差异 使用统一端序(htonl/ntohl)
读取失败返回0 未判断 feof() / ferror() 检查错误状态
文件写不全 缓冲未刷新 fflush() + fsync()

八、🧩 高级扩展:结构体数组与二进制文件

c 复制代码
SensorData sensors[3] = {
    {1001, "Temp1", 36.5},
    {1002, "Temp2", 38.2},
    {1003, "Temp3", 39.8}
};
fwrite(sensors, sizeof(SensorData), 3, fp);

🔹 一次写入多个结构体更高效;

🔹 fread() 时同理读取整个数组。


✅ 小结:何时用二进制文件?

场景 是否推荐 理由
嵌入式系统配置 高效、节省空间
传感器日志 连续结构体流
通信协议文件 无需解析文本
可编辑配置文件 建议文本格式(JSON/INI)


📘 第六章:文件路径与目录操作(嵌入式文件系统相关)


一、📂 文件路径基础:相对路径 vs 绝对路径

🔹 路径是什么?

文件系统的本质是一棵树:

复制代码
/
├── home/
│   └── user/
│       └── main.c
└── etc/
    └── config.txt

C 程序访问文件时,必须告诉操作系统路径(path),即文件在这棵树中的位置。


✅ 1. 绝对路径(Absolute Path)

从根目录 / 开始,写出文件的完整路径。

c 复制代码
FILE *fp = fopen("/home/user/data.txt", "r");
  • 始终唯一确定一个文件
  • 不依赖当前工作目录;
  • 在嵌入式 Linux、RTOS(带文件系统)中使用较多。

✅ 2. 相对路径(Relative Path)

相对于当前工作目录(Current Working Directory)

c 复制代码
FILE *fp = fopen("config/settings.txt", "r");

如果当前目录是 /home/user/

则打开的是 /home/user/config/settings.txt

🔹 可以使用 "../" 返回上一级目录:

c 复制代码
fopen("../log.txt", "w");  // 上级目录

✅ 3. 嵌入式系统中的路径规则

在资源有限的 MCU 文件系统(如 FATFS、SPIFFS)中,路径通常简化:

c 复制代码
f_open(&fp, "data/log.txt", FA_WRITE | FA_CREATE_ALWAYS);
  • 路径区分大小写;
  • 目录深度有限;
  • 没有"根目录 /home/user/"这样的多级树;
  • 文件路径通常限制在几十字节内。

二、🧱 目录创建与删除

1️⃣ 创建目录:mkdir()

c 复制代码
#include <sys/stat.h>

int mkdir(const char *pathname, mode_t mode);
  • pathname:目录路径;
  • mode:权限(Linux 有效,如 0755);
  • 返回 0 表示成功,-1 表示失败。
c 复制代码
#include <sys/stat.h>
#include <stdio.h>

int main(void) {
    if (mkdir("logs", 0755) == 0)
        printf("目录创建成功\n");
    else
        perror("mkdir");
    return 0;
}

2️⃣ 删除空目录:rmdir()

c 复制代码
#include <unistd.h>
int rmdir(const char *pathname);

只能删除 空目录

c 复制代码
rmdir("logs");

若目录非空需先删除内部文件,可使用系统命令或递归遍历(见后)。


3️⃣ 删除文件:remove()

c 复制代码
#include <stdio.h>
remove("data.bin");

可删除文件或空目录(标准库支持)。


4️⃣ 重命名文件/目录:rename()

c 复制代码
#include <stdio.h>
rename("oldname.txt", "newname.txt");
rename("config", "backup_config");

5 删除非空目录

删除非空目录(包含文件或子目录)需要递归删除 :先删除目录内的所有文件和子目录,直到目录为空,再用 rmdir 删除空目录。这也是 rm -r 命令的底层原理。

核心思路

  1. 遍历目录内容:打开目标目录,读取所有条目(文件、子目录、隐藏文件等)。
  2. 区分条目类型
    • 若为文件 (或链接文件):直接用 unlink 删除。
    • 若为子目录:递归执行删除操作(先清空子目录,再删除子目录本身)。
  3. 删除空目录 :当目录内所有内容都被删除后,用 rmdir 删除当前目录。

实现步骤与示例代码

以下是一个递归删除非空目录的函数实现,依赖 POSIX 标准库函数(opendirreaddir 等):

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>

// 递归删除目录(包括非空目录)
int rmdir_recursive(const char *path) {
    DIR *dir = opendir(path);  // 打开目录
    if (dir == NULL) {
        perror("opendir failed");
        return -1;
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {  // 遍历目录条目
        // 跳过 "." 和 ".."(当前目录和父目录)
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        // 拼接完整路径(path + "/" + entry->d_name)
        char full_path[1024];
        snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
        if (strlen(full_path) >= sizeof(full_path)) {
            fprintf(stderr, "路径过长: %s\n", full_path);
            closedir(dir);
            return -1;
        }

        // 获取条目属性(判断是文件还是目录)
        struct stat st;
        if (lstat(full_path, &st) == -1) {  // lstat 不跟随符号链接
            perror("lstat failed");
            closedir(dir);
            return -1;
        }

        // 若为目录:递归删除子目录
        if (S_ISDIR(st.st_mode)) {
            if (rmdir_recursive(full_path) == -1) {
                closedir(dir);
                return -1;
            }
        }
        // 若为文件(或符号链接):直接删除
        else {
            if (unlink(full_path) == -1) {  // unlink 可删除文件/链接
                perror("unlink failed");
                closedir(dir);
                return -1;
            }
        }
    }

    // 检查遍历是否正常结束(非错误导致的退出)
    if (errno != 0) {
        perror("readdir failed");
        closedir(dir);
        return -1;
    }

    closedir(dir);  // 关闭目录

    // 最后删除当前空目录
    if (rmdir(path) == -1) {
        perror("rmdir failed");
        return -1;
    }

    return 0;  // 成功删除
}

// 示例:删除名为 "test_dir" 的非空目录
int main() {
    const char *dir_path = "test_dir";
    if (rmdir_recursive(dir_path) == 0) {
        printf("目录 '%s' 及其内容已全部删除\n", dir_path);
    } else {
        printf("删除目录失败\n");
        return 1;
    }
    return 0;
}

关键函数解析

  1. opendir(const char *path) :打开目录,返回 DIR* 目录流指针(类似文件流的目录句柄)。
  2. readdir(DIR *dir) :读取目录中的下一个条目(文件/子目录),返回 struct dirent*(包含文件名等信息)。
  3. lstat(const char *path, struct stat *st) :获取文件/目录的属性(如是否为目录),lstatstat 的区别是:不跟随符号链接(避免误删链接指向的目标)。
  4. unlink(const char *path):删除文件或符号链接(不能删除目录)。
  5. 递归逻辑 :对每个子目录重复调用 rmdir_recursive,直到所有子目录被清空并删除,最后删除顶层目录。

注意事项

  1. 权限问题 :需要对目录及其所有父目录有执行权限 (进入目录),对目录内的文件有写权限(删除文件)。
  2. 符号链接处理 :示例中用 lstat 而非 stat,避免将符号链接误认为目录(若需删除符号链接本身,而非其指向的目标,这是正确的)。
  3. 路径长度 :示例中 full_path 设为 1024 字节,实际使用时需根据系统最大路径长度(如 PATH_MAX)调整。
  4. 风险提示:递归删除非常危险(误删可能导致数据丢失),实际使用前务必确认路径正确性。

总结

删除非空目录的核心是递归遍历并删除所有内容 ,直到目录为空,再用 rmdir 收尾。上述示例实现了这一逻辑,可根据实际需求(如过滤特定文件、日志输出)扩展。在命令行中,rm -r 目录名 本质就是执行类似的递归删除操作。

✅ 嵌入式移植示例(FATFS)

c 复制代码
f_mkdir("logs");
f_unlink("logs/data.bin");
f_rename("old.bin", "new.bin");

这些函数是 FATFS 对标准C文件函数的替代(后面详细讲)。


三、📜 读取目录内容(遍历目录)

POSIX 接口:

c 复制代码
#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);

✅ 示例:打印当前目录下的所有文件

c 复制代码
#include <stdio.h>
#include <dirent.h>

int main(void) {
    DIR *dir;
    struct dirent *entry;

    dir = opendir(".");
    if (dir == NULL) {
        perror("opendir");
        return -1;
    }

    while ((entry = readdir(dir)) != NULL) {
        printf("%s\n", entry->d_name);
    }

    closedir(dir);
    return 0;
}

📘 输出:

复制代码
main.c
data.bin
config/
Makefile

这段代码的功能是遍历并打印当前目录下的所有文件和子目录名称 ,是一个简单的"目录列表"程序,类似命令行中的 ls 命令(但不包含详细属性,仅输出名称)。

代码逐行解析

1. 头文件包含
c 复制代码
#include <stdio.h>    // 标准输入输出库(提供 printf、perror 等)
#include <dirent.h>   // 目录操作库(提供 DIR、dirent 结构体及目录遍历函数)

dirent.h 是 POSIX 标准中用于目录操作的核心头文件,定义了目录遍历所需的结构体和函数。

2. 变量定义
c 复制代码
DIR *dir;                  // 目录流指针(类似文件流 FILE*,用于表示打开的目录)
struct dirent *entry;      // 目录条目指针(存储单个文件/目录的信息)
  • DIR:是一个内部结构体,由系统维护,用于跟踪目录遍历的状态(如当前位置、目录大小等)。
  • struct dirent:存储单个目录条目的元数据,核心成员是 d_name(文件名/目录名)。
3. 打开目录
c 复制代码
dir = opendir(".");  // 打开当前目录("." 表示当前工作目录)
if (dir == NULL) {   // 检查打开是否失败(如权限不足、目录不存在)
    perror("opendir");  // 打印错误原因(如 "opendir: Permission denied")
    return -1;
}

opendir(const char *path) 函数的作用是"打开指定路径的目录",返回 DIR* 指针(目录流句柄)。若失败(如路径无效、无权限),返回 NULL,并设置 errno 错误码,perror 会根据 errno 打印具体错误。

4. 遍历目录条目
c 复制代码
while ((entry = readdir(dir)) != NULL) {  // 循环读取目录中的每个条目
    printf("%s\n", entry->d_name);       // 打印条目名称(文件名或目录名)
}
  • readdir(DIR *dir):从打开的目录流中读取下一个条目 (文件或子目录),返回 struct dirent* 指针。当所有条目读取完毕后,返回 NULL,循环结束。
  • entry->d_namestruct dirent 中最常用的成员,存储以 null 结尾的文件名/目录名(如 test.txtdocs.bashrc 等)。
5. 关闭目录
c 复制代码
closedir(dir);  // 关闭目录流,释放系统资源(类似 fclose 关闭文件)
return 0;

closedir 必须在目录遍历结束后调用,否则会导致资源泄漏。

运行效果

编译运行后,会输出当前目录下的所有文件和子目录名称,包括:

  • 普通文件(如 main.cdata.txt);
  • 子目录(如 docsimages);
  • 隐藏文件/目录(以 . 开头,如 .git.bash_profile);
  • 特殊目录 .(当前目录)和 ..(父目录)。

例如,在包含 a.txtb_dir.hidden 的目录中运行,输出可能为:

复制代码
.
..
a.txt
b_dir
.hidden

关键说明

  1. 遍历顺序readdir 返回条目的顺序由文件系统决定(通常不是字母序),若需排序,需手动将名称存入数组后排序。
  2. 跨平台性dirent.h 是 POSIX 标准(Linux/macOS/UNIX 支持),Windows 下需使用兼容层(如 MinGW)或替换为 Windows API(如 FindFirstFile)。
  3. 权限限制 :若目录包含无权限访问的条目,readdir 仍会返回其名称(但无法获取详细属性)。

总结

这段代码通过 opendir 打开目录、readdir 遍历条目、closedir 关闭目录,实现了"列出当前目录所有文件和子目录名称"的功能,是目录操作的基础示例,常用于文件管理器、批量处理工具等场景。


struct dirent 结构体内容:

c 复制代码
struct dirent {
    ino_t d_ino;         // 文件节点号
    char d_name[256];    // 文件名
    unsigned char d_type;// 文件类型(某些系统支持)
};

常见类型(非标准):

类型常量 含义
DT_REG 普通文件
DT_DIR 目录
DT_LNK 软链接

✅ 示例:区分文件和目录

c 复制代码
#include <dirent.h>
#include <stdio.h>

int main(void) {
    DIR *dir = opendir(".");
    struct dirent *entry;

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_type == DT_DIR)
            printf("[DIR]  %s\n", entry->d_name);
        else if (entry->d_type == DT_REG)
            printf("[FILE] %s\n", entry->d_name);
    }
    closedir(dir);
}

四、🧠 嵌入式文件系统(FATFS、LittleFS、SPIFFS)

在 MCU 中我们通常没有操作系统(如 Linux),

因此不能直接使用 fopen()opendir() 等系统调用。

需要使用嵌入式文件系统库(Embedded File System)


✅ 1️⃣ FATFS(最常见)

由 Elm-ChaN 开发的开源文件系统,支持 FAT12/16/32。

📍 常用于:

  • SD卡 / U盘;
  • SPI Flash;
  • NOR / NAND。

📘 接口类似标准C:

C标准函数 FATFS对应函数
fopen() f_open()
fread() f_read()
fwrite() f_write()
fseek() f_lseek()
remove() f_unlink()
rename() f_rename()
mkdir() f_mkdir()

示例:

c 复制代码
#include "ff.h"

FIL file;
UINT bw;

f_mount(&FatFs, "", 1);
f_open(&file, "log.txt", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&file, "Hello FATFS\n", 13, &bw);
f_close(&file);

✅ 2️⃣ LittleFS(小型、掉电安全)

由 ARM mbed 团队开发,用于 Flash 的轻量级文件系统。

特点:

  • 块擦除次数均衡;
  • 掉电保护;
  • 支持目录;
  • 适合小型 MCU。

API 示例(与 POSIX 类似):

c 复制代码
lfs_file_open(&lfs, &file, "data.bin", LFS_O_WRONLY | LFS_O_CREAT);
lfs_file_write(&lfs, &file, buffer, size);
lfs_file_close(&lfs, &file);

✅ 3️⃣ SPIFFS(常用于 ESP32)

特点:

  • 专为 SPI NOR Flash;
  • 不支持目录(扁平结构);
  • 支持动态 wear leveling。
c 复制代码
SPIFFS_open(&fs, "config.txt", SPIFFS_CREAT | SPIFFS_RDWR, 0);

✅ 4️⃣ 嵌入式文件系统与标准 C API 的映射关系

为了让 printf(), fopen() 等能直接使用,

很多嵌入式系统会建立 文件系统适配层(Wrapper Layer)

c 复制代码
int f_open_wrapper(FILE *fp, const char *path, const char *mode) {
    // 转换 fopen 模式为 FATFS 模式
    // 调用 f_open() / f_close()
}

📘 在 FreeRTOS + FATFS 组合中,

很多厂商(如 STM32 HAL、NXP SDK)已实现该层。


✅ 5️⃣ 模拟文件系统(在 MCU RAM 中)

嵌入式调试阶段,有时 MCU 只有 RAM,没有实际文件系统。

可使用 RAM 模拟文件操作:

c 复制代码
char fake_file[256];
memcpy(fake_file, "CONFIG=ON\n", strlen("CONFIG=ON\n"));

或在仿真中用 PC 文件系统作为宿主,通过串口通信实现虚拟文件系统(VFS)


五、📚 小结

功能 标准C POSIX接口 FATFS/LittleFS
打开文件 fopen open f_open
读写文件 fread/fwrite read/write f_read/f_write
关闭文件 fclose close f_close
创建目录 mkdir mkdir f_mkdir
遍历目录 --- opendir/readdir f_opendir/f_readdir
删除文件 remove unlink f_unlink
重命名 rename rename f_rename

六、💡 嵌入式开发中的实践要点

问题 原因 建议
文件系统损坏 掉电、写中断 使用 LittleFS / 日志式文件系统
文件读写失败 路径错误 检查当前目录或挂载点
内存不足 缓冲区大 减小 FF_MAX_SS、启用 DMA 模式
频繁写入 Flash 寿命 引入缓存层或循环日志区
移植困难 API 不统一 建立统一的文件操作抽象层(FS Wrapper)

相关推荐
coppher2 分钟前
Ubuntu 22.04 amd64 离线安装 Docker 完整教程
linux·docker
xyz59916 分钟前
如何在 WSL 中删除指定版本的 Ubuntu 以及安装
linux·运维·ubuntu
亚空间仓鼠44 分钟前
OpenEuler系统常用服务(五)
linux·运维·服务器·网络
minji...2 小时前
Linux 线程同步与互斥(二) 线程同步,条件变量,pthread_cond_init/wait/signal/broadcast
linux·运维·开发语言·jvm·数据结构·c++
虚伪的空想家2 小时前
k8s集群configmap和secrets备份脚本
linux·容器·kubernetes
the sun342 小时前
从 QEMU 直接启动到 U-Boot 引导:嵌入式 Linux 启动流程的本质差异
linux·运维·服务器
草莓熊Lotso2 小时前
【Linux 线程进阶】进程 vs 线程资源划分 + 线程控制全详解
java·linux·运维·服务器·数据库·c++·mysql
ShineWinsu2 小时前
对于Linux:文件操作以及文件IO的解析
linux·c++·面试·笔试·io·shell·文件操作
-SGlow-2 小时前
Linux相关概念和易错知识点(52)(基于System V的信号量和消息队列)
linux·运维·服务器
江畔何人初2 小时前
TCP的三次握手与四次挥手
linux·服务器·网络·网络协议·tcp/ip