18、文件操作(上):fopen/fclose/fread/fwrite/fprintf/fscanf,文本与二进制文件
文件操作,说白了就是让程序跟硬盘上的数据打交道。我刚开始学C语言时,觉得文件操作可有可无——不就是存个数据嘛,变量不也能存?后来做嵌入式项目才明白,没有文件操作,你的程序就是个“失忆症患者”。断电重启,一切归零。
这一章,咱们把文件操作的基础打牢。我会从最常用的几个函数讲起,再聊聊文本文件和二进制文件的区别。嗯,这些都是我实际项目中反复踩过的坑,你仔细看。
18.1 文件操作的“三板斧”:fopen、fread、fwrite
文件操作有个固定套路:打开 → 读写 → 关闭。少了哪一步都不行。
18.1.1 fopen:打开文件
fopen 是文件操作的入口。它的原型很简单:
FILE *fopen(const char *filename, const char *mode);
第一个参数是文件名,第二个参数是打开模式。模式决定了你能对文件做什么。
| 模式 | 含义 | 文件不存在时 |
|---|---|---|
| "r" | 只读 | 返回NULL |
| "w" | 只写 | 创建新文件 |
| "a" | 追加 | 创建新文件 |
| "r+" | 读写 | 返回NULL |
| "w+" | 读写(清空) | 创建新文件 |
| "a+" | 读写(追加) | 创建新文件 |
18.1.2 fread 和 fwrite:二进制读写
这两个函数是二进制文件操作的“主力军”。它们的原型是:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数解释:
ptr:数据缓冲区的指针size:每个数据元素的大小(字节数)nmemb:要读写的数据元素个数stream:文件指针
举个例子,读写一个结构体数组:
typedef struct {
int id;
char name[32];
float score;
} Student;
Student students[100];
// 写入文件
fwrite(students, sizeof(Student), 100, fp);
// 从文件读取
fread(students, sizeof(Student), 100, fp);
fread 的返回值来判断是否读到了足够的数据。如果返回值小于 nmemb,说明文件提前结束了或者发生了错误。
18.2 文本文件的读写:fprintf 和 fscanf
文本文件,说白了就是人能直接看懂的文件。每个字节都对应一个可打印字符(或换行符)。
18.2.1 fprintf:格式化写入
fprintf 跟 printf 几乎一样,只是多了一个文件指针参数:
int fprintf(FILE *stream, const char *format, ...);
举个例子:
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
perror("fopen");
return -1;
}
fprintf(fp, "ID: %d, Name: %s, Score: %.2f\n", 1, "Alice", 95.5);
fclose(fp);
这样写出来的文件,用记事本打开就能看到内容。调试时特别方便。
18.2.2 fscanf:格式化读取
fscanf 是 fprintf 的逆操作:
int fscanf(FILE *stream, const char *format, ...);
读取刚才写入的数据:
FILE *fp = fopen("data.txt", "r");
int id;
char name[32];
float score;
fscanf(fp, "ID: %d, Name: %s, Score: %f", &id, name, &score);
printf("id=%d, name=%s, score=%.2f\n", id, name, score);
fclose(fp);
fscanf 读取一个格式不匹配的文件,结果程序直接崩溃。后来我养成了一个习惯:每次用 fscanf 前,先检查返回值。如果返回值不等于期望的匹配项数,说明格式有问题。
18.3 文本文件 vs 二进制文件
这是很多初学者容易混淆的地方。我简单说下区别:
| 对比项 | 文本文件 | 二进制文件 |
|---|---|---|
| 存储方式 | 每个字节存一个字符 | 按内存中的二进制格式存储 |
| 可读性 | 人可以直接阅读 | 需要程序解析 |
| 存储效率 | 较低(数字要转成字符串) | 较高(直接存二进制) |
| 跨平台性 | 较好(换行符可能不同) | 较差(字节序、结构体对齐) |
| 典型用途 | 配置文件、日志文件 | 图片、音频、数据库文件 |
举个例子,存储整数 12345:
- 文本文件: 占用5个字节,分别存储 '1'、'2'、'3'、'4'、'5' 的ASCII码
- 二进制文件: 占用4个字节(int类型),直接存储 0x00003039
你想想看,如果存的是浮点数或者结构体,文本文件的存储效率会更低。但文本文件的好处是——出问题了,你用记事本打开就能看到数据对不对。
18.4 fclose:别忘了关文件
这个函数看起来简单,但很多人会忘。它的原型:
int fclose(FILE *fp);
为什么一定要关文件?
- 释放系统资源(文件描述符是有限的)
- 刷新缓冲区(确保数据真正写到磁盘)
- 避免文件损坏(程序崩溃时未关闭的文件可能不完整)
我记得有一次,一个同事写了个日志程序,每次写日志都 fopen 但不 fclose。跑了几天后,程序突然打不开任何文件了——文件描述符耗尽了。嗯,这种问题排查起来特别头疼。
18.5 知识体系图
下面这张图帮你理清文件操作的核心逻辑:
18.6 综合示例:一个简单的配置读写器
最后,我写个小例子,把今天讲的内容串起来。这个程序会读写一个配置文件:
#include <stdio.h>
#include <string.h>
typedef struct {
int baudrate;
char port[16];
int timeout_ms;
} Config;
int save_config(const char *filename, Config *cfg) {
FILE *fp = fopen(filename, "w");
if (fp == NULL) {
perror("fopen");
return -1;
}
fprintf(fp, "baudrate=%d\n", cfg->baudrate);
fprintf(fp, "port=%s\n", cfg->port);
fprintf(fp, "timeout=%d\n", cfg->timeout_ms);
fclose(fp);
return 0;
}
int load_config(const char *filename, Config *cfg) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
perror("fopen");
return -1;
}
if (fscanf(fp, "baudrate=%d\n", &cfg->baudrate) != 1) {
fprintf(stderr, "读取波特率失败\n");
fclose(fp);
return -1;
}
if (fscanf(fp, "port=%s\n", cfg->port) != 1) {
fprintf(stderr, "读取端口失败\n");
fclose(fp);
return -1;
}
if (fscanf(fp, "timeout=%d\n", &cfg->timeout_ms) != 1) {
fprintf(stderr, "读取超时时间失败\n");
fclose(fp);
return -1;
}
fclose(fp);
return 0;
}
int main() {
Config cfg = {115200, "COM3", 1000};
save_config("config.txt", &cfg);
Config loaded = {0};
load_config("config.txt", &loaded);
printf("波特率: %d, 端口: %s, 超时: %dms\n",
loaded.baudrate, loaded.port, loaded.timeout_ms);
return 0;
}
这个例子虽然简单,但涵盖了文件操作的核心流程。你可以在自己的项目中直接套用这个模板。
公众号:蓝海资料掘金营,微信deep3321