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)

相关推荐
无敌最俊朗@7 小时前
C++音视频就业路线
linux·windows
Fr2ed0m7 小时前
Linux 文本处理完整指南:grep、awk、sed、jq 命令详解与实战
linux·运维·服务器
大聪明-PLUS7 小时前
使用 GitLab CI/CD 为 Linux 创建 RPM 包(一)
linux·嵌入式·arm·smarc
边疆.7 小时前
【Linux】自动化构建工具make和Makefile和第一个系统程序—进度条
linux·运维·服务器·makefile·make
2021黑白灰8 小时前
windows11 vscode ssh远程linux服务器/虚拟机 免密登录
linux·服务器·ssh
z202305088 小时前
linux之PCIE 设备枚举流程分析
linux·运维·服务器
simple_whu8 小时前
编译tiff:arm64-linux-static报错 Could NOT find CMath (missing: CMath_pow)
linux·运维·c++
SundayBear8 小时前
Linux驱动开发指南
linux·驱动开发·嵌入式
大聪明-PLUS8 小时前
使用 GitLab CI/CD 为 Linux 构建 RPM 包(二)
linux·嵌入式·arm·smarc