搞定嵌入式文件读写!16 个核心函数 + 二进制 / 文本文件实战指南

48 阅读11分钟

【嵌入式 C 语言实战】文件操作完全指南:从基础函数到实战案例

大家好,我是学嵌入式的小杨同学。在嵌入式开发中,文件操作是实现数据持久化的核心手段 —— 程序运行时的临时数据(如传感器采集值、设备配置参数、日志记录),只有通过文件存储到硬盘或闪存中,才能实现永久保存。今天就结合资料,系统讲解 Linux 环境下 C 语言文件操作的核心知识点、常用函数和嵌入式实战案例,帮你彻底掌握文件读写技巧。

一、文件操作的核心基础

1. 为什么需要文件操作?

嵌入式程序运行时,数据默认存储在 RAM(运行内存)中,断电后会丢失。文件操作的核心作用,就是将数据从 RAM 写入存储设备(硬盘、SD 卡、闪存),实现 “临时数据→永久存储” 的转换,典型场景包括:

  • 保存设备配置参数(如串口波特率、IP 地址);
  • 记录传感器采集的历史数据(如温度、湿度日志);
  • 存储程序运行日志(便于调试和问题追溯)。

2. 文件的两种存储形式

文件存储分为两种形式,适用于不同场景:

  • 文本文件(ASCII 码形式):以字符为单位存储,可通过记事本直接查看(如.txt文件),可读性强,适合存储日志、配置项等需人工查看的数据;
  • 二进制文件:以字节为单位存储原始数据,记事本打开显示为乱码,需专用工具解析,优点是存储效率高、读写速度快,适合存储传感器原始数据、二进制固件等。

3. 文件操作的两种方式

C 语言文件操作分为两类,核心区别在于是否使用缓冲区:

  • 缓冲式文件操作(标准库函数):数据先写入缓冲区,缓冲区满后再写入文件(或读取时先填满缓冲区),优点是数据安全(减少 IO 次数),缺点是速度稍慢,本文重点讲解这类操作(如fopenfreadfwrite);
  • 非缓冲式文件操作(系统调用):直接操作文件描述符,无缓冲区,速度快但数据安全性低,适合对实时性要求极高的场景(如高频数据采集)。

4. 文件操作的通用步骤

无论哪种文件操作,都遵循 “打开→读写→关闭” 的流程,这是保证数据安全和程序稳定的关键:

  1. 打开文件:建立程序与文件的连接(通道),获取文件操作指针;
  2. 读写操作:通过文件指针执行读(从文件到程序)或写(从程序到文件)操作;
  3. 关闭文件:断开程序与文件的连接,释放资源,必须执行(否则可能导致数据丢失或文件损坏)。

二、核心文件操作函数详解(附实战代码)

文件操作的核心是标准库函数(需包含<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);
核心参数
  • streamfopen返回的文件指针。
返回值
  • 成功:返回 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(格式化读)

适用于文本文件,支持格式化输入输出(类似printfscanf),兼顾可读性和效率。

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. 光标操作:rewindfseekftell(定位读写位置)

文件读写时,光标(文件指针)决定了下一次读写的位置,通过以下函数可灵活控制光标:

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。

三、嵌入式开发关键注意事项

  1. 路径适配:嵌入式设备的存储路径可能与 PC 不同(如 SD 卡挂载在/mnt/sdcard/),需根据设备修改文件路径,避免路径错误;
  2. 权限问题:嵌入式 Linux 中,文件读写需对应权限(如chmod 777 data/),否则fopen会返回NULL
  3. 缓冲区刷新:缓冲式操作中,数据可能暂存于缓冲区,若需立即写入文件,可调用fflush(fp)强制刷新缓冲区;
  4. 内存安全:读取文件时,缓冲区大小需足够,避免数组溢出;动态分配的缓冲区使用后需释放;
  5. 二进制与文本模式区分:Windows 系统中,文本模式(r/w)会自动转换换行符(\n\r\n),二进制模式(rb/wb)不转换;Linux 系统无区别,但建议明确指定模式,提升代码可移植性;
  6. 异常处理:所有文件操作函数都需判断返回值(如fopen是否返回NULLfwrite是否写入成功),避免异常导致程序崩溃。

四、总结

文件操作是嵌入式 C 语言的核心技能,核心要点可总结为:

  1. 流程固定:遵循 “打开→读写→关闭”,fclose不可省略;
  2. 函数选型:文本文件用fprintf/fscanf/fgets,二进制文件用fread/fwrite(效率最高);
  3. 关键细节:注意文件路径、权限、缓冲区,做好异常处理;
  4. 嵌入式适配:根据设备存储路径和权限要求调整代码,优先使用二进制文件存储数据(节省内存和带宽)。

掌握这些文件操作技巧,就能轻松实现嵌入式设备的数据持久化,应对配置存储、日志记录、数据采集等常见场景。我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式实战技巧,一起进步!