30. 文件IO (1)

我们从 最底层原理 → C语言标准接口 → 嵌入式理解 逐层展开,让你彻底理解 "文件" 在系统和程序中的含义。


🧭 一、文件 I/O 基础概念(全面讲解)


1️⃣ 什么是文件(File)

文件(File) 是操作系统用来 组织和管理数据的基本单位

无论是:

  • 一张图片(.jpg)
  • 一个程序(.elf)
  • 还是一个串口设备(/dev/ttyUSB0)

在操作系统看来,它们都是"文件"。

👉 换句话说:在 Linux / Unix / 嵌入式系统中,"一切皆文件"。


📦 文件的本质

在底层,文件只是一个字节序列(Byte Stream)

复制代码
+-------------------------------+
| 0x41 | 0x42 | 0x43 | 0x44 ... |
+-------------------------------+

程序只是通过文件接口(read/write)把数据写入这个字节序列,或者读出来。


2️⃣ 文件在操作系统中的定义

在操作系统(OS)层面,文件包含两类信息:

分类 内容
文件数据区 实际内容,例如文本、二进制数据
文件控制块(FCB) 存储文件的元信息,如文件名、权限、大小、时间戳等

在 C 程序中,当你打开一个文件时,操作系统并不是直接给你文件的内容,而是返回一个文件描述符(File Descriptor) 或者一个 FILE结构体指针


3️⃣ 文件描述符(File Descriptor)

文件描述符是一个 整数(int),是内核用来标识一个已打开文件的编号。

每个进程在内核中都有一个文件描述符表

文件描述符 含义 默认流
0 标准输入 stdin
1 标准输出 stdout
2 标准错误 stderr

示意图:

复制代码
+-----------------------+
| FD=0 -> 键盘输入流(stdin)
| FD=1 -> 屏幕输出流(stdout)
| FD=2 -> 屏幕错误流(stderr)
| FD=3 -> test.txt
| FD=4 -> /dev/ttyUSB0
+-----------------------+

📌 在底层系统调用中,我们使用这些文件描述符来读写文件:

c 复制代码
read(fd, buf, size);
write(fd, buf, size);
close(fd);

4️⃣ 文件流(Stream)与缓冲(Buffer)概念

C语言的标准库(stdio.h)在文件描述符之上封装了一层更高层的抽象:

文件流(FILE Stream)

🔹 Stream(流)

流是文件输入输出的抽象概念。

C 程序中所有的 I/O 操作都通过流来完成,比如:

  • 从键盘读取(输入流)
  • 向显示器输出(输出流)
  • 从磁盘读取文件(输入流)
  • 向磁盘写入文件(输出流)

🔹 Buffer(缓冲区)

I/O 操作通常速度慢(比如磁盘/串口),

因此 C 标准库会在用户空间开一个"缓冲区",

只有当缓冲区满了或手动刷新(fflush)时,才真正写入设备。

类型:

类型 行为 示例
全缓冲(Full Buffered) 缓冲区满时才写入 文件写入(磁盘)
行缓冲(Line Buffered) 每行或遇到换行符时写入 stdout(终端)
无缓冲(Unbuffered) 立即输出 stderr(错误输出)

例如:

c 复制代码
printf("Hello");   // 可能还在缓冲区
fflush(stdout);    // 强制刷新到屏幕

5️⃣ 标准输入/输出/错误流:stdin, stdout, stderr

stdio.h 中定义了三个全局流:

c 复制代码
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

默认映射:

文件描述符 默认设备
stdin 0 键盘
stdout 1 屏幕
stderr 2 屏幕(不带缓冲)

示例:

c 复制代码
fprintf(stdout, "普通输出\n");
fprintf(stderr, "错误输出!\n");

⚙️ 嵌入式中:

  • 这些标准流常常被重定向到串口、LCD 或日志系统
  • 比如 printf() 实际是通过 UART 发送。

6️⃣ C语言中的文件操作接口(stdio.h)

C语言标准库为文件操作提供了完整接口:

操作 函数 说明
打开文件 fopen() 以指定模式打开文件
关闭文件 fclose() 关闭文件并释放资源
读写操作 fgetc(), fgets(), fread() / fputc(), fputs(), fwrite() 读取/写入文件内容
文件位置 fseek(), ftell(), rewind() 控制文件指针
检查状态 feof(), ferror() 检查是否到文件末尾或出错

7️⃣ FILE 结构体与 fopen() / fclose()

打开文件:

c 复制代码
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("文件打开失败");
    return -1;
}

关闭文件:

c 复制代码
fclose(fp);

💡 FILE 是一个结构体,内部保存了:

  • 文件描述符
  • 缓冲区指针
  • 当前读写位置
  • 文件状态标志

8️⃣ 文件模式(r, w, a, r+, w+, a+)

模式 含义
"r" 只读打开文件,文件必须存在
"w" 只写打开文件,不存在则创建,存在则清空
"a" 追加写入文件,不存在则创建
"r+" 可读可写,文件必须存在
"w+" 可读可写,不存在则创建,存在则清空
"a+" 可读可写,写入追加到末尾,不存在则创建

示例:

c 复制代码
FILE *fp1 = fopen("log.txt", "a+"); // 读写模式,追加写入
FILE *fp2 = fopen("data.bin", "wb"); // 写二进制文件

9️⃣ 二进制模式(rb, wb, ab)

文本文件(text mode)和二进制文件(binary mode)的区别:

  • 文本模式\n 可能在不同平台被转换(如 Windows 为 \r\n
  • 二进制模式:原样读写字节流,不做转换
c 复制代码
fopen("data.bin", "wb"); // 写入二进制文件
fopen("data.bin", "rb"); // 读取二进制文件

⚙️ 嵌入式开发中常用 二进制模式,例如:

  • 写入 Flash 镜像
  • 记录传感器原始数据
  • 处理图片、音频、固件文件等原始字节流

✅ 小结

概念 关键点
文件 数据的逻辑抽象,一切皆文件
文件描述符 操作系统级别的文件编号
文件流 C标准库对文件描述符的高级封装
缓冲区 提高I/O性能,控制刷新
标准流 stdin / stdout / stderr 三个默认通道
文件模式 决定读写方式(文本/二进制、覆盖/追加等)

非常好 👍!

你现在学到的这一部分是 C 语言文件 I/O 的核心章节之一。

这章讲的是 文件的基本读写操作 ,也就是------如何把数据读进内存写进文件

我们一步一步讲清楚每一类函数的机制、应用场景和示例,

学完这一章,你能轻松地自己实现日志系统、配置文件、数据保存与读取。


📘 第二章:文件的基本读写操作


一、C 文件 I/O 模型回顾

在 C 语言中,文件操作都是通过一个结构体 FILE 来完成的:

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

fp 是一个文件指针(文件流) ,它内部记录了文件缓冲区、文件位置等信息。

所有的读写函数 (fgetc, fputc, fgets, fputs, fread, fwrite, fprintf, fscanf)

都通过这个 FILE * 来操作文件。


二、📂 文件的基本读写操作

文件操作可以分为四种层次:

字符级 → 行级 → 块级(二进制) → 格式化文本。


✅ 1. 逐字符操作

字符操作最简单,适合文本文件的逐字读写

🔹 函数:
c 复制代码
int fgetc(FILE *fp);
int fputc(int ch, FILE *fp);
📘 示例:
c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "w");
    if (!fp) return -1;

    char *str = "Hello C!";
    for (int i = 0; str[i] != '\0'; i++)
        fputc(str[i], fp);  // 写一个字符

    fclose(fp);

    // 读回文件
    fp = fopen("test.txt", "r");
    int ch;
    while ((ch = fgetc(fp)) != EOF)
        putchar(ch);         // 输出到屏幕

    fclose(fp);
    return 0;
}

📤 输出:

复制代码
Hello C!
💡 知识点:
  • 每次读写一个字符;
  • fgetc() 返回 int,以便能识别 EOF
  • 适合小文件或字符流解析。

✅ 2. 按行操作

按行操作是最常用的文本读取方式,比如读取配置文件、日志文件等。

🔹 函数:
c 复制代码
char *fgets(char *buf, int size, FILE *fp);
int fputs(const char *str, FILE *fp);
📘 示例:
c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("config.txt", "w");
    fputs("name=Sensor\n", fp);
    fputs("threshold=25.5\n", fp);
    fclose(fp);

    fp = fopen("config.txt", "r");
    char line[64];

    while (fgets(line, sizeof(line), fp)) {
        printf("读取到一行:%s", line);
    }

    fclose(fp);
    return 0;
}

📤 输出:

复制代码
读取到一行:name=Sensor
读取到一行:threshold=25.5
💡 知识点:
  • fgets() 会读取换行符 \n
  • 如果行太长,会分段读取;
  • fputs() 不会自动添加换行。

✅ 3. 按块操作(二进制文件)

适合读取结构体、数组、图片、音频等二进制数据

文件中存储的内容不是字符,而是原始字节数据

🔹 函数:
c 复制代码
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
📘 示例:
c 复制代码
#include <stdio.h>

typedef struct {
    int id;
    float temp;
} Sensor;

int main() {
    Sensor s1 = {1001, 28.5};
    FILE *fp = fopen("sensor.bin", "wb"); // 二进制写
    fwrite(&s1, sizeof(Sensor), 1, fp);
    fclose(fp);

    Sensor s2;
    fp = fopen("sensor.bin", "rb");       // 二进制读
    fread(&s2, sizeof(Sensor), 1, fp);
    fclose(fp);

    printf("ID=%d, 温度=%.1f\n", s2.id, s2.temp);
    return 0;
}

📤 输出:

复制代码
ID=1001, 温度=28.5
💡 知识点:
  • fwrite()/fread() 是原始字节操作;
  • 不做格式化,不会处理文本换行;
  • 读写结构体或数组时非常高效;
  • 嵌入式中常用于保存 Flash 数据或 EEPROM 数据。

✅ 4. 文件格式化读写

这是最灵活、最常见 的文件操作方式。

它让你可以直接像打印一样读写文本文件。

🔹 函数:
c 复制代码
int fprintf(FILE *fp, const char *fmt, ...);
int fscanf(FILE *fp, const char *fmt, ...);
📘 示例:
c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "w");
    int id = 101;
    float voltage = 3.3;
    const char *name = "SensorA";

    fprintf(fp, "ID=%d, 电压=%.2f, 名称=%s\n", id, voltage, name);
    fclose(fp);

    fp = fopen("data.txt", "r");
    int rid;
    float rv;
    char rname[16];
    fscanf(fp, "ID=%d, 电压=%f, 名称=%s", &rid, &rv, rname);
    fclose(fp);

    printf("读取: ID=%d, 电压=%.2f, 名称=%s\n", rid, rv, rname);
    return 0;
}

📤 输出:

复制代码
读取: ID=101, 电压=3.30, 名称=SensorA

✅ 5. printf / fprintf / scanf / fscanf 区别总结

函数 用途 目标
printf() 输出到标准输出 屏幕
scanf() 从标准输入读取 键盘
fprintf() 输出到指定文件 文件
fscanf() 从文件按格式读取 文件

🧠 小结:

fprintf(fp, ...) = 文件版的 printf()
fscanf(fp, ...) = 文件版的 scanf()


三、文件读写函数选择建议

场景 推荐函数 原因
读写文本日志 fgets(), fputs() 行级处理方便
保存/读取配置文件 fprintf(), fscanf() 格式清晰
存储结构体或二进制数据 fwrite(), fread() 效率高
简单字符流 fgetc(), fputc() 控制细粒度

四、⚙️ 嵌入式开发中的应用建议

应用 推荐方式 示例
Flash 参数存储 fwrite() / fread() 保存结构体
传感器日志 fprintf() 记录时间与数据
配置加载 fgets() / sscanf() 解析配置文件
通信模拟(串口日志) fputc() / fgetc() 模拟字节流

相关推荐
gfdgd xi4 小时前
GXDE For deepin 25:deepin25 能用上 GXDE 了!
linux·运维·python·ubuntu·架构·bug·deepin
草帽lufei5 小时前
轻松上手WSL安装与使用
linux·前端·操作系统
XH-hui6 小时前
【打靶日记】VulNyx 之 Lower6
linux·网络安全·vulnyx
陌路206 小时前
操作系统(9)虚拟内存-内存映射
linux
Starry_hello world8 小时前
进程的替换
linux·笔记·有问必答
Jasonakeke8 小时前
一位脑瘫患者如何接单4位数
c语言
拥友LikT9 小时前
惠普DL380服务器安装系统以后无法读取到系统盘启动解决方案(其他品牌服务器类似解决思路)
linux·服务器系统安装
程序猿编码9 小时前
Linux 文件变动监控工具:原理、设计与实用指南(C/C++代码实现)
linux·c语言·c++·深度学习·inotify
网硕互联的小客服9 小时前
SSD和HDD存储应该如何选择?
linux·运维·服务器·网络·安全