📘 第五章:二进制文件操作与结构体存储(详细讲解)
一、🧩 什么是二进制文件(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=4,nmemb=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 |
逐个个字符写入,效率低 | 逐字符处理(如加密转换后写入) |
六、关键注意事项
-
文件模式必须匹配 :
写入二进制数据时,必须用
wb(二进制写)或ab(二进制追加)模式 ,否则 Windows 等系统会自动转换\n为\r\n,导致二进制数据(如图片、结构体)损坏。 -
返回依赖依赖文本格式 :
fwrite不识别字符串的\0终止符,若写入字符串(如char str[] = "hello"),会连\0一起同写入(而fputs会忽略\0)。如需写入字符串内容(不含\0),需用fwrite(str, 1, strlen(str), fp)。 -
缓冲区与落盘 :
fwrite写入的数据先先存入 C 标准库的用户态缓冲区,需通过fflush(fp)刷新到内核缓冲区,或fsync(fileno(fp))强制证写入磁盘(避免程序统崩溃丢失数据)。 -
返回值必须检查 :
即使返回值 <
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=8,nmemb=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 |
逐字符读取,效率低 | 逐字符解析(如语法分析) |
六、关键注意事项
-
文件模式必须匹配 :
读取二进制数据时,必须用
rb(二进制读)模式 ,否则 Windows 等系统会将\r\n转换为\n,导致二进制数据(如结构体、图片)解析错误。 -
缓冲区大小需足够 :
内存缓冲区
ptr必须至少能容纳size * nmemb字节,否则会导致内存溢出(如用char buf[5]读取size=3, nmemb=2的 6 字节数据)。 -
返回值必须结合
feof/ferror判断 :当返回值 <
nmemb时,需用feof(stream)确认是否因文件结束(正常情况),或ferror(stream)确认是否因错误(如磁盘故障)。 -
跨平台结构体读取注意对齐 :
不同编译器的结构体对齐方式可能不同(如成员间的填充字节),导致跨平台读取
fwrite写入的结构体时出现数据错位。解决:用#pragma pack统一对齐(如#pragma pack(1)取消填充)。 -
不处理字符串终止符 :
若读取的是字符串(如
char数组),fread会读取所有字节(包括\0前后的内容),不会自动添加\0终止符(需手动处理)。
总结
fread 是处理二进制数据读取的"主力函数",其核心优势是按字节精确读取连续数据块,不依赖文本格式,适用于结构体、数组、二进制文件等场景。使用时需注意:
- 用二进制模式打开文件(
rb); - 检查返回值并结合
feof/ferror判断结果; - 确保缓冲区大小足够,跨平台时处理结构体对齐问题。
它与 fwrite 成对使用,是实现二进制数据持久化(写入文件→读取恢复)的核心工具。
四、🔄 字节序(Endianness)问题
📘 概念:
字节序 是指多字节数据(如
int、float)在内存中存储的高低字节顺序。
| 类型 | 定义 | 内存存储顺序(以 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):高位字节存放在内存的低地址处,低位字节存放在高地址处。
函数工作原理
函数通过以下步骤判断字节序:
-
定义一个 32 位无符号整数:
cunsigned int x = 0x12345678;这个整数的 4 个字节从高位到低位依次是:
0x12(最高位)、0x34、0x56、0x78(最低位)。 -
将整数地址强制转换为单字节指针:
c*(unsigned char*)&x&x取整数x的内存首地址(低地址)。(unsigned char*)&x将地址强制转换为unsigned char*类型(单字节指针),此时解引用(*)会只读取该地址处的第一个字节(低地址的字节)。
-
判断第一个字节的值:
- 若第一个字节是
0x78(最低位字节):说明低位字节存放在低地址 → 小端序 ,函数返回0。 - 若第一个字节是
0x12(最高位字节):说明高位字节存放在低地址 → 大端序 ,函数返回1。
- 若第一个字节是
内存存储示意图
以 x = 0x12345678 为例,两种字节序的内存布局(假设内存地址从低到高为 addr、addr+1、addr+2、addr+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 命令的底层原理。
核心思路
- 遍历目录内容:打开目标目录,读取所有条目(文件、子目录、隐藏文件等)。
- 区分条目类型 :
- 若为文件 (或链接文件):直接用
unlink删除。 - 若为子目录:递归执行删除操作(先清空子目录,再删除子目录本身)。
- 若为文件 (或链接文件):直接用
- 删除空目录 :当目录内所有内容都被删除后,用
rmdir删除当前目录。
实现步骤与示例代码
以下是一个递归删除非空目录的函数实现,依赖 POSIX 标准库函数(opendir、readdir 等):
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;
}
关键函数解析
opendir(const char *path):打开目录,返回DIR*目录流指针(类似文件流的目录句柄)。readdir(DIR *dir):读取目录中的下一个条目(文件/子目录),返回struct dirent*(包含文件名等信息)。lstat(const char *path, struct stat *st):获取文件/目录的属性(如是否为目录),lstat与stat的区别是:不跟随符号链接(避免误删链接指向的目标)。unlink(const char *path):删除文件或符号链接(不能删除目录)。- 递归逻辑 :对每个子目录重复调用
rmdir_recursive,直到所有子目录被清空并删除,最后删除顶层目录。
注意事项
- 权限问题 :需要对目录及其所有父目录有执行权限 (进入目录),对目录内的文件有写权限(删除文件)。
- 符号链接处理 :示例中用
lstat而非stat,避免将符号链接误认为目录(若需删除符号链接本身,而非其指向的目标,这是正确的)。 - 路径长度 :示例中
full_path设为 1024 字节,实际使用时需根据系统最大路径长度(如PATH_MAX)调整。 - 风险提示:递归删除非常危险(误删可能导致数据丢失),实际使用前务必确认路径正确性。
总结
删除非空目录的核心是递归遍历并删除所有内容 ,直到目录为空,再用 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_name:struct dirent中最常用的成员,存储以 null 结尾的文件名/目录名(如test.txt、docs、.bashrc等)。
5. 关闭目录
c
closedir(dir); // 关闭目录流,释放系统资源(类似 fclose 关闭文件)
return 0;
closedir 必须在目录遍历结束后调用,否则会导致资源泄漏。
运行效果
编译运行后,会输出当前目录下的所有文件和子目录名称,包括:
- 普通文件(如
main.c、data.txt); - 子目录(如
docs、images); - 隐藏文件/目录(以
.开头,如.git、.bash_profile); - 特殊目录
.(当前目录)和..(父目录)。
例如,在包含 a.txt、b_dir、.hidden 的目录中运行,输出可能为:
.
..
a.txt
b_dir
.hidden
关键说明
- 遍历顺序 :
readdir返回条目的顺序由文件系统决定(通常不是字母序),若需排序,需手动将名称存入数组后排序。 - 跨平台性 :
dirent.h是 POSIX 标准(Linux/macOS/UNIX 支持),Windows 下需使用兼容层(如 MinGW)或替换为 Windows API(如FindFirstFile)。 - 权限限制 :若目录包含无权限访问的条目,
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) |