上节课咱们把文件打开了,也学会了怎么关闭。但光打开不干活,那不是白开了吗?这节课咱们就讲怎么往文件里写东西,怎么从文件里读东西。
C语言提供了四种读写方式,从简单到复杂,从“一个字一个字来”到“一整块一整块搬”。咱们挨个说,每种都配上例子,最后你就能根据实际需要挑着用了。
方式一:一个字符一个字符地读写——fgetc和fputc
这是最基础的读写方式。你一次处理一个字符,简单粗暴,但适合处理文本文件。
fputc——往文件里写一个字符
int fputc(int ch, FILE *fp);
两个参数:ch是要写的字符,fp是文件指针。成功返回写的那个字符,失败返回EOF。
用法:
FILE *fp = fopen("test.txt", "w"); if (fp == NULL) { perror("打开失败"); return 1; } fputc('H', fp); fputc('e', fp); fputc('l', fp); fputc('l', fp); fputc('o', fp); fclose(fp);
运行完,文件里就是“Hello”五个字符。
fgetc——从文件里读一个字符
int fgetc(FILE *fp);
从文件里读一个字符,返回读到的字符(转成int了)。如果读到文件末尾,返回EOF。EOF是stdio.h里定义的一个常量,通常是-1。
用法:
FILE *fp = fopen("test.txt", "r"); if (fp == NULL) { perror("打开失败"); return 1; } int ch; while ((ch = fgetc(fp)) != EOF) { putchar(ch); // 把读到的字符打印到屏幕上 } fclose(fp);
这个循环是文件读取的经典套路——读一个字符,判断是不是EOF,不是就处理,是就退出。
注意:fgetc返回的是int,不是char。 因为EOF是-1,char可能是无符号的(0到255),存不下-1。所以必须用int来接。
完整例子:文件复制
把源文件的内容一个字符一个字符地复制到目标文件:
#include <stdio.h> int main() { FILE *src = fopen("source.txt", "r"); FILE *dest = fopen("dest.txt", "w"); int ch; if (src == NULL || dest == NULL) { perror("打开文件失败"); return 1; } while ((ch = fgetc(src)) != EOF) { fputc(ch, dest); } fclose(src); fclose(dest); printf("复制完成\n"); return 0; }
这个例子很简单,但你想想——这不就是copy命令的原理吗?读一个字符,写一个字符,循环往复,直到读完。
fgetc/fputc适合什么场景?
-
处理文本文件,逐字符分析
-
简单的文件复制
-
字符级别的过滤(比如把文件里所有小写字母转成大写)
方式二:一行一行地读写——fgets和fputs
一个字符一个字符处理太慢了,尤其是文本文件,你经常是按行处理的。fgets和fputs就是干这个的。
fputs——往文件里写一个字符串
int fputs(const char *str, FILE *fp);
把str写到文件里,不会自动加换行符。成功返回非负数,失败返回EOF。
FILE *fp = fopen("test.txt", "w"); fputs("第一行\n", fp); // 要换行得自己加\n fputs("第二行\n", fp); fputs("第三行", fp); // 最后一行不加\n也行 fclose(fp);
fgets——从文件里读一行
char *fgets(char *str, int n, FILE *fp);
三个参数:str是接收字符串的缓冲区,n是最多读多少个字符(包括最后的\0),fp是文件指针。成功返回str,失败或读到文件末尾返回NULL。
char buffer[100]; FILE *fp = fopen("test.txt", "r"); while (fgets(buffer, sizeof(buffer), fp) != NULL) { printf("%s", buffer); // 打印读到的这一行 } fclose(fp);
注意:fgets会把换行符也读进来。 比如文件里有一行是“Hello\n”,fgets读到的就是“Hello\n”,不是“Hello”。如果你不想要那个换行符,得自己手动去掉:
buffer[strcspn(buffer, "\n")] = '\0'; // 把换行符替换成\0
strcspn函数返回字符串中第一个匹配字符的位置,这里就是找\n的位置。如果没找到,返回字符串长度,那这个操作就相当于在末尾加了个\0,没影响。
fgets最多读n-1个字符,因为要留一个位置给\0。如果一行特别长,超过n-1个字符,fgets只读前n-1个,剩下的下次再读。
完整例子:按行读取文件并打印行号
#include <stdio.h> #include <string.h> int main() { FILE *fp = fopen("test.txt", "r"); char buffer[256]; int line = 1; if (fp == NULL) { perror("打开失败"); return 1; } while (fgets(buffer, sizeof(buffer), fp) != NULL) { // 去掉末尾的换行符 buffer[strcspn(buffer, "\n")] = '\0'; printf("%d: %s\n", line, buffer); line++; } fclose(fp); return 0; }
fgets/fputs适合什么场景?
-
处理配置文件、日志文件,按行读取
-
从文件读取字符串数据
-
写简单的文本报告
方式三:格式化读写——fprintf和fscanf
你平时用printf往屏幕上打印格式化的数据,用scanf从键盘读格式化的数据。文件也有对应的版本:fprintf和fscanf。用法跟printf和scanf一模一样,就是多了一个文件指针参数。
fprintf——往文件里写格式化数据
int fprintf(FILE *fp, const char *format, ...);
FILE *fp = fopen("students.txt", "w"); fprintf(fp, "姓名:%s,学号:%d,成绩:%.1f\n", "张三", 101, 85.5); fprintf(fp, "姓名:%s,学号:%d,成绩:%.1f\n", "李四", 102, 92.0); fclose(fp);
文件里就会有两行格式化的学生信息。
fscanf——从文件里读格式化数据
int fscanf(FILE *fp, const char *format, ...);
FILE *fp = fopen("students.txt", "r"); char name[20]; int id; float score; while (fscanf(fp, "姓名:%s,学号:%d,成绩:%f\n", name, &id, &score) == 3) { printf("%s %d %.1f\n", name, id, score); } fclose(fp);
fscanf返回成功匹配并赋值的参数个数。上面这个例子中,如果成功读到三个数据,返回3。当返回的不是3(比如读完了,返回EOF),循环退出。
注意:fscanf的格式字符串必须跟文件里的内容格式严格匹配。 文件里是“姓名:张三,学号:101,成绩:85.5”,你的格式字符串就得是"姓名:%s,学号:%d,成绩:%f"。差一个字符都读不出来。所以fscanf比较“脆”,文件格式一变,读取代码就得跟着改。
完整例子:把学生信息存到文件再读出来
#include <stdio.h> int main() { // 写入 FILE *fp = fopen("students.txt", "w"); if (fp == NULL) { perror("打开失败"); return 1; } fprintf(fp, "%s %d %.1f\n", "张三", 101, 85.5); fprintf(fp, "%s %d %.1f\n", "李四", 102, 92.0); fprintf(fp, "%s %d %.1f\n", "王五", 103, 78.5); fclose(fp); // 读取 fp = fopen("students.txt", "r"); if (fp == NULL) { perror("打开失败"); return 1; } char name[20]; int id; float score; printf("学生信息:\n"); while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) { printf("%s %d %.1f\n", name, id, score); } fclose(fp); return 0; }
fprintf/fscanf适合什么场景?
-
需要按特定格式读写数据
-
把结构化的数据存成文本文件
-
调试时往文件里打印日志
方式四:二进制块读写——fread和fwrite
前面三种都是针对文本文件的。如果你要存的是结构体、数组这种二进制数据,用文本方式存又慢又占空间,而且读回来还得解析。这时候就该fread和fwrite上场了。
fwrite——把一块内存的数据原封不动写到文件里
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
四个参数:
-
ptr:要写的数据在内存里的地址 -
size:每个元素占多少字节 -
count:要写多少个元素 -
fp:文件指针
返回值:成功写入的元素个数。正常情况等于count。
int arr[5] = {1, 2, 3, 4, 5}; FILE *fp = fopen("data.bin", "wb"); // 注意模式是"wb",二进制写 fwrite(arr, sizeof(int), 5, fp); fclose(fp);
这行代码把整个数组的5个int原封不动地写到了文件里,总共写了5 * sizeof(int)个字节。
fread——从文件里把一块数据原封不动读回内存
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);
参数跟fwrite对应,返回值是实际读到的元素个数。如果读完了文件,返回值可能小于count。
int arr[5]; FILE *fp = fopen("data.bin", "rb"); // 二进制读 int n = fread(arr, sizeof(int), 5, fp); printf("读了%d个元素\n", n); fclose(fp);
注意:用二进制模式读写的时候,fopen的模式要加b——"wb"、"rb"、"ab"。在Windows上这个区别很重要,因为Windows会把\n转成\r\n。在Linux上b可加可不加,效果一样。但为了跨平台,建议加上。
完整例子:结构体的二进制存取
#include <stdio.h> typedef struct { int id; char name[20]; float score; } Student; int main() { Student s1 = {101, "张三", 85.5}; Student s2 = {102, "李四", 92.0}; // 写入 FILE *fp = fopen("students.dat", "wb"); if (fp == NULL) { perror("打开失败"); return 1; } fwrite(&s1, sizeof(Student), 1, fp); fwrite(&s2, sizeof(Student), 1, fp); fclose(fp); // 读取 fp = fopen("students.dat", "rb"); if (fp == NULL) { perror("打开失败"); return 1; } Student s; while (fread(&s, sizeof(Student), 1, fp) == 1) { printf("%d %s %.1f\n", s.id, s.name, s.score); } fclose(fp); return 0; }
你打开students.dat看看,里面是一堆乱码——因为它是二进制文件,不是给人看的,是给程序读的。但它存取效率高,而且结构体里有什么它就存什么,读回来也是原样。
fread/fwrite适合什么场景?
-
存结构体、数组等二进制数据
-
需要快速读写大量数据
-
数据不需要给人直接看
-
游戏存档、数据库文件、图片音频
四种方式对比——什么时候用哪个?
| 方式 | 函数 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 字符读写 | fgetc/fputc | 逐字符处理文本 | 简单、精细控制 | 慢,代码啰嗦 |
| 字符串读写 | fgets/fputs | 按行处理文本 | 方便,一次一行 | 有换行符问题 |
| 格式化读写 | fprintf/fscanf | 结构化文本数据 | 格式灵活 | 文件格式变了读取就失败 |
| 二进制块读写 | fread/fwrite | 结构体、数组、二进制数据 | 快,省空间,精度不丢 | 人看不懂,跨平台可能有字节序问题 |
给你一个简单的选择标准:
-
数据是给人看的(配置文件、日志、报告)→ 用文本方式(fgets/fputs或fprintf/fscanf)
-
数据是给程序用的(存档、缓存、数据库)→ 用二进制方式(fread/fwrite)
-
你要精细处理每个字符 → 用fgetc/fputc
几个常见的坑
坑1:fgetc返回类型写成char
char ch; while ((ch = fgetc(fp)) != EOF) { ... } // 可能死循环
因为char可能是无符号的,永远不等于-1。必须用int。
坑2:fgets忘了处理换行符
fgets(buffer, 100, fp); printf("%s\n", buffer); // 会多一个空行,因为buffer里已经有\n了
坑3:fscanf格式字符串跟文件内容不匹配
// 文件里是:101 张三 85.5 fscanf(fp, "%d %s %f", &id, name, &score); // 正确 fscanf(fp, "%d,%s,%f", &id, name, &score); // 错误!文件里没逗号
坑4:用文本模式打开二进制文件
FILE *fp = fopen("data.bin", "r"); // 错误!应该用"rb" FILE *fp = fopen("data.bin", "rb"); // 正确
坑5:fread的返回值不检查
fread(&s, sizeof(Student), 1, fp); printf("%s\n", s.name); // 如果读失败,s里是垃圾数据
要检查返回值,确定真的读到了数据再用。
坑6:fwrite写字符串时多写了\0
char str[] = "Hello"; fwrite(str, sizeof(char), strlen(str), fp); // 正确,只写5个字符 fwrite(str, sizeof(char), sizeof(str), fp); // 会多写一个\0
总结一下:
这节课讲了四种读写方式,核心就这些:
-
fgetc/fputc:一个字符一个字符来,适合精细处理文本。
-
fgets/fputs:一行一行来,适合处理日志、配置。
-
fprintf/fscanf:格式化读写,适合结构化文本。
-
fread/fwrite:二进制块读写,适合结构体、数组,快且省空间。
不管用哪种方式,核心流程都是一样的:打开文件→读写→关闭文件。你根据数据的类型和用途选合适的方式就行。
回去自己动手,每种方式都写个例子跑一遍。把学生信息分别用文本方式和二进制方式存一遍,然后用记事本打开看看区别。亲手试过了,你才知道什么时候该用哪种。下节课咱们讲文件定位——怎么在文件里跳来跳去,想读哪儿读哪儿。