【嵌入式 C 语言实战】文件操作完全指南:从基础函数到实战案例
大家好,我是学嵌入式的小杨同学。在嵌入式开发中,文件操作是实现数据持久化的核心手段 —— 程序运行时的临时数据(如传感器采集值、设备配置参数、日志记录),只有通过文件存储到硬盘或闪存中,才能实现永久保存。今天就结合资料,系统讲解 Linux 环境下 C 语言文件操作的核心知识点、常用函数和嵌入式实战案例,帮你彻底掌握文件读写技巧。
一、文件操作的核心基础
1. 为什么需要文件操作?
嵌入式程序运行时,数据默认存储在 RAM(运行内存)中,断电后会丢失。文件操作的核心作用,就是将数据从 RAM 写入存储设备(硬盘、SD 卡、闪存),实现 “临时数据→永久存储” 的转换,典型场景包括:
- 保存设备配置参数(如串口波特率、IP 地址);
- 记录传感器采集的历史数据(如温度、湿度日志);
- 存储程序运行日志(便于调试和问题追溯)。
2. 文件的两种存储形式
文件存储分为两种形式,适用于不同场景:
- 文本文件(ASCII 码形式):以字符为单位存储,可通过记事本直接查看(如
.txt文件),可读性强,适合存储日志、配置项等需人工查看的数据; - 二进制文件:以字节为单位存储原始数据,记事本打开显示为乱码,需专用工具解析,优点是存储效率高、读写速度快,适合存储传感器原始数据、二进制固件等。
3. 文件操作的两种方式
C 语言文件操作分为两类,核心区别在于是否使用缓冲区:
- 缓冲式文件操作(标准库函数):数据先写入缓冲区,缓冲区满后再写入文件(或读取时先填满缓冲区),优点是数据安全(减少 IO 次数),缺点是速度稍慢,本文重点讲解这类操作(如
fopen、fread、fwrite); - 非缓冲式文件操作(系统调用):直接操作文件描述符,无缓冲区,速度快但数据安全性低,适合对实时性要求极高的场景(如高频数据采集)。
4. 文件操作的通用步骤
无论哪种文件操作,都遵循 “打开→读写→关闭” 的流程,这是保证数据安全和程序稳定的关键:
- 打开文件:建立程序与文件的连接(通道),获取文件操作指针;
- 读写操作:通过文件指针执行读(从文件到程序)或写(从程序到文件)操作;
- 关闭文件:断开程序与文件的连接,释放资源,必须执行(否则可能导致数据丢失或文件损坏)。
二、核心文件操作函数详解(附实战代码)
文件操作的核心是标准库函数(需包含<stdio.h>头文件),下面按 “打开→读写→光标操作→关闭” 的流程,讲解最常用的 16 个函数,每个函数都附参数说明和实战示例。
1. 打开文件:fopen(核心入口函数)
函数原型
c
运行
FILE *fopen(const char *pathname, const char *mode);
核心参数
pathname:文件路径 + 文件名(如"data/log.txt"、"/mnt/sdcard/config.bin");mode:打开方式(决定文件操作权限),常用方式如下:
| 打开方式 | 权限说明 | 若文件不存在 | 适用场景 |
|---|---|---|---|
r | 只读 | 报错(返回 NULL) | 读取已存在的文本 / 二进制文件 |
w | 只写 | 创建新文件,覆盖原有文件 | 新建或重写文件 |
a | 追加写 | 创建新文件 | 向文件末尾添加数据(如日志) |
r+ | 读写 | 报错 | 读取并修改已存在的文件 |
w+ | 读写 | 创建新文件,覆盖原有文件 | 新建文件并读写 |
a+ | 读写 | 创建新文件 | 追加数据同时支持读取 |
返回值
- 成功:返回
FILE*类型的文件指针(后续所有操作都依赖该指针); - 失败:返回
NULL(需通过perror函数查看失败原因)。
实战示例
c
运行
#include <stdio.h>
int main() {
// 以“追加写”方式打开文本文件,不存在则创建
FILE *fp = fopen("data/log.txt", "a");
if (fp == NULL) {
perror("fopen error"); // 打印失败原因(如目录不存在、权限不足)
return -1;
}
// 后续操作...
fclose(fp); // 必须关闭文件
return 0;
}
2. 关闭文件:fclose(资源释放关键)
函数原型
c
运行
int fclose(FILE *stream);
核心参数
stream:fopen返回的文件指针。
返回值
- 成功:返回 0;
- 失败:返回 - 1(如文件已被关闭)。
注意事项
- 文件操作完成后必须调用
fclose,否则缓冲区数据可能无法写入文件,导致数据丢失; - 关闭后的文件指针不可再使用,建议置为
NULL(如fp = NULL),避免野指针。
3. 字符级读写:fputc(写字符)、fgetc(读字符)
适用于文本文件的逐字符读写,操作粒度小,灵活度高。
fputc(写字符)
- 函数原型:
int fputc(int c, FILE *stream); - 功能:将单个字符
c写入文件; - 返回值:成功返回写入的字符,失败返回 - 1。
fgetc(读字符)
- 函数原型:
int fgetc(FILE *stream); - 功能:从文件中读取单个字符;
- 返回值:成功返回读取的字符,到达文件末尾返回
EOF(宏定义,值为 - 1)。
实战示例:逐字符读写文本文件
c
运行
// 写字符到文件
void write_char() {
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) { perror("fopen"); return; }
char str[] = "Hello, Embedded!";
for (int i = 0; str[i] != '\0'; i++) {
fputc(str[i], fp); // 逐字符写入
}
fclose(fp);
printf("字符写入完成\n");
}
// 从文件读字符
void read_char() {
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) { perror("fopen"); return; }
char c;
printf("读取结果:");
while ((c = fgetc(fp)) != EOF) { // 读到EOF结束
printf("%c", c);
}
printf("\n");
fclose(fp);
}
4. 字符串级读写:fputs(写字符串)、fgets(读字符串)
适用于文本文件的字符串读写,比字符级操作更高效。
fputs(写字符串)
- 函数原型:
int fputs(const char *s, FILE *stream); - 功能:将字符串
s(不含'\0')写入文件; - 返回值:成功返回非负整数,失败返回 - 1。
fgets(读字符串)
- 函数原型:
char *fgets(char *s, int size, FILE *stream); - 功能:从文件读取最多
size-1个字符到字符串s,自动添加'\0'; - 参数说明:
size为字符串缓冲区大小(避免溢出); - 返回值:成功返回
s的首地址,到达文件末尾返回NULL。
实战示例:字符串读写
c
运行
// 写字符串到文件
void write_str() {
FILE *fp = fopen("log.txt", "a");
if (fp == NULL) { perror("fopen"); return; }
char log[] = "2026-01-16: 设备启动成功\n";
fputs(log, fp); // 追加写入日志
fclose(fp);
}
// 从文件读字符串
void read_str() {
FILE *fp = fopen("log.txt", "r");
if (fp == NULL) { perror("fopen"); return; }
char buf[1024];
while (fgets(buf, sizeof(buf), fp) != NULL) { // 逐行读取
printf("%s", buf);
}
fclose(fp);
}
5. 块数据读写:fwrite(写块)、fread(读块)
适用于二进制文件(也可用于文本文件),按 “数据块” 读写,效率最高,是嵌入式开发的首选。
fwrite(写块数据)
-
函数原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); -
功能:将
ptr指向的块数据写入文件; -
参数说明:
ptr:待写入数据的首地址(任意类型,故为void*);size:单个数据块的大小(如sizeof(int)、sizeof(struct SensorData));nmemb:数据块的个数;
-
返回值:成功写入的数据块个数,失败返回 0。
fread(读块数据)
- 函数原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); - 功能:从文件读取
nmemb个大小为size的数据块到ptr; - 返回值:成功读取的数据块个数,到达文件末尾返回 0。
实战示例:二进制文件读写(存储传感器数据)
c
运行
// 定义传感器数据结构体
typedef struct {
int temperature; // 温度
int humidity; // 湿度
long timestamp; // 时间戳
} SensorData;
// 写传感器数据到二进制文件
void write_binary() {
SensorData data = {25, 60, 1736992800}; // 温度25℃,湿度60%,时间戳
FILE *fp = fopen("sensor.bin", "wb"); // "wb":二进制写
if (fp == NULL) { perror("fopen"); return; }
// 写入1个大小为sizeof(SensorData)的数据块
fwrite(&data, sizeof(SensorData), 1, fp);
fclose(fp);
printf("二进制数据写入完成\n");
}
// 从二进制文件读传感器数据
void read_binary() {
SensorData data;
FILE *fp = fopen("sensor.bin", "rb"); // "rb":二进制读
if (fp == NULL) { perror("fopen"); return; }
// 读取1个数据块
size_t ret = fread(&data, sizeof(SensorData), 1, fp);
if (ret == 1) {
printf("温度:%d℃,湿度:%d%,时间戳:%ld\n",
data.temperature, data.humidity, data.timestamp);
} else {
printf("读取失败或文件结束\n");
}
fclose(fp);
}
6. 格式化读写:fprintf(格式化写)、fscanf(格式化读)
适用于文本文件,支持格式化输入输出(类似printf、scanf),兼顾可读性和效率。
fprintf(格式化写)
- 函数原型:
int fprintf(FILE *stream, const char *format, ...); - 功能:按格式化字符串
format写入数据到文件; - 示例:
fprintf(fp, "ID:%d, 温度:%d℃\n", 1, 25);
fscanf(格式化读)
- 函数原型:
int fscanf(FILE *stream, const char *format, ...); - 功能:按格式化字符串
format从文件读取数据; - 示例:
fscanf(fp, "ID:%d, 温度:%d℃\n", &id, &temp);
实战示例:格式化读写配置文件
c
运行
// 写配置文件(格式化)
void write_config() {
FILE *fp = fopen("config.txt", "w");
if (fp == NULL) { perror("fopen"); return; }
int baudrate = 115200; // 串口波特率
int timeout = 5; // 超时时间(秒)
fprintf(fp, "baudrate:%d\n", baudrate);
fprintf(fp, "timeout:%d\n", timeout);
fclose(fp);
}
// 读配置文件(格式化)
void read_config() {
FILE *fp = fopen("config.txt", "r");
if (fp == NULL) { perror("fopen"); return; }
int baudrate, timeout;
// 按格式读取
fscanf(fp, "baudrate:%d\n", &baudrate);
fscanf(fp, "timeout:%d\n", &timeout);
printf("波特率:%d,超时时间:%d秒\n", baudrate, timeout);
fclose(fp);
}
7. 光标操作:rewind、fseek、ftell(定位读写位置)
文件读写时,光标(文件指针)决定了下一次读写的位置,通过以下函数可灵活控制光标:
rewind(光标重置到文件开头)
- 函数原型:
void rewind(FILE *stream); - 功能:将光标移动到文件起始位置,无返回值;
- 示例:读取文件后,调用
rewind(fp)可重新读取。
fseek(光标定位到指定位置)
-
函数原型:
int fseek(FILE *stream, long offset, int whence); -
核心参数:
offset:偏移量(正数向右移动,负数向左移动);whence:起始位置(SEEK_SET= 文件开头,SEEK_CUR= 当前位置,SEEK_END= 文件末尾);
-
返回值:成功返回 0,失败返回 - 1;
-
示例:
fseek(fp, 10, SEEK_SET);(从文件开头向右移动 10 字节)。
ftell(获取光标位置)
- 函数原型:
long ftell(FILE *stream); - 功能:返回光标距离文件开头的字节数,可用于计算文件大小;
- 示例:计算文件大小(光标移到末尾,获取偏移量)。
实战示例:计算文件大小
c
运行
long get_file_size(const char *filename) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) { perror("fopen"); return -1; }
fseek(fp, 0, SEEK_END); // 光标移到文件末尾
long size = ftell(fp); // 获取偏移量(即文件大小)
rewind(fp); // 光标重置到开头(不影响后续操作)
fclose(fp);
return size;
}
// 调用示例
int main() {
long size = get_file_size("sensor.bin");
printf("文件大小:%ld字节\n", size);
return 0;
}
8. 其他常用函数
feof:判断光标是否到达文件末尾(int feof(FILE *stream)),返回非 0 表示到达末尾;perror:打印函数执行失败原因(void perror(const char *s)),如perror("fread");remove:删除文件(int remove(const char *pathname)),成功返回 0,失败返回 - 1。
三、嵌入式开发关键注意事项
- 路径适配:嵌入式设备的存储路径可能与 PC 不同(如 SD 卡挂载在
/mnt/sdcard/),需根据设备修改文件路径,避免路径错误; - 权限问题:嵌入式 Linux 中,文件读写需对应权限(如
chmod 777 data/),否则fopen会返回NULL; - 缓冲区刷新:缓冲式操作中,数据可能暂存于缓冲区,若需立即写入文件,可调用
fflush(fp)强制刷新缓冲区; - 内存安全:读取文件时,缓冲区大小需足够,避免数组溢出;动态分配的缓冲区使用后需释放;
- 二进制与文本模式区分:Windows 系统中,文本模式(
r/w)会自动转换换行符(\n↔\r\n),二进制模式(rb/wb)不转换;Linux 系统无区别,但建议明确指定模式,提升代码可移植性; - 异常处理:所有文件操作函数都需判断返回值(如
fopen是否返回NULL、fwrite是否写入成功),避免异常导致程序崩溃。
四、总结
文件操作是嵌入式 C 语言的核心技能,核心要点可总结为:
- 流程固定:遵循 “打开→读写→关闭”,
fclose不可省略; - 函数选型:文本文件用
fprintf/fscanf/fgets,二进制文件用fread/fwrite(效率最高); - 关键细节:注意文件路径、权限、缓冲区,做好异常处理;
- 嵌入式适配:根据设备存储路径和权限要求调整代码,优先使用二进制文件存储数据(节省内存和带宽)。
掌握这些文件操作技巧,就能轻松实现嵌入式设备的数据持久化,应对配置存储、日志记录、数据采集等常见场景。我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式实战技巧,一起进步!