我们从 最底层原理 → 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() |
模拟字节流 |