文件
变量里的东西,程序一结束就没了。想留到下次用,就要写进文件。下面的程序往 hello.txt 里写一行,关上,再打开把这一行读回来:
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("hello.txt", "w");5 fprintf(f, "你好,文件!\n");6 fclose(f);7 8 char line[100];9 f = fopen("hello.txt", "r");10 fgets(line, sizeof line, f);11 fclose(f);12 printf("读回来:%s", line);13 return 0;14}
读回来:你好,文件!
fopen 打开一个文件,返回一个 FILE *,也就是一个指针,指向 C 记着的这个文件的情况。别的文件函数都要用到这个指针。fprintf 和 printf 一样用,只是多了第一个参数:写到哪个文件里。
这一页上,每段例子都在浏览器里自己的一个小文件夹里跑。它写的文件只在程序运行期间存在,程序一结束就没了。所以这里每段例子都是先写文件、再读文件,放在同一段程序里。
打开和关闭
fopen 的第二个参数是模式,说明你要拿这个文件做什么:
| 模式 | 做什么 | 文件不存在时 | 文件已经有了时 |
|---|---|---|---|
"r" | 读 | fopen 返回 NULL | 从头开始读 |
"w" | 写 | 新建一个 | 先清空 |
"a" | 追加(写在后面) | 新建一个 | 接着原来的内容往后写 |
用完要调用 fclose。fprintf 写的东西先在内存里攒着,攒多了才一起写进文件;fclose 保证剩下的都写进去,再把文件放开。打开的文件都要关上。
这段程序显示什么?
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("day.txt", "w");5 fprintf(f, "星期一\n");6 fclose(f);7 f = fopen("day.txt", "w");8 fprintf(f, "星期二\n");9 fclose(f);10 11 char line[100];12 f = fopen("day.txt", "r");13 while (fgets(line, sizeof line, f) != NULL) {14 printf("%s", line);15 }16 fclose(f);17 return 0;18}
"w" 写之前会先清空文件。第二次 fopen 把 星期一 扔掉了,文件里只剩 星期二。想往文件后面加内容,要用 "a" 打开。
fopen 失败的时候
fopen 不一定成功。用 "r" 打开一个不存在的文件,或者名字写错了,它就返回 NULL。用指针之前一定要先检查:
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("plans.txt", "r");5 if (f == NULL) {6 printf("打不开 plans.txt\n");7 return 1;8 }9 printf("打开了\n");10 fclose(f);11 return 0;12}
打不开 plans.txt
return 1; 让程序提前结束。返回 0 以外的数,是告诉系统「出问题了」。
不检查的话,这个 NULL 就被交给了下一个函数。真的 C 程序多半会在那里崩溃;我们的引擎会停在那一行,告诉你原因:
1#include <stdio.h>2 3int main(void) {4 char line[100];5 FILE *f = fopen("plans.txt", "r");6 fgets(line, sizeof line, f);7 printf("%s", line);8 return 0;9}
fgets on a NULL FILE *发生了什么: 停在第 6 行:fgets 拿到的文件指针是 NULL:fopen 没有打开文件(文件不存在,或者名字、模式写错了)。用之前要先检查 if (fp == NULL)。
一行一行地读
fgets(line, sizeof line, f) 读一行,放进 char 数组 line,最多读 sizeof line - 1 个字符,再补上 '\0'。读不到东西了,它返回 NULL。所以常见的循环是「一直读,直到 fgets 返回 NULL」:
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("shopping.txt", "w");5 fprintf(f, "牛奶\n");6 fprintf(f, "鸡蛋\n");7 fprintf(f, "苹果 %d 个\n", 6);8 fclose(f);9 10 char line[100];11 int count = 0;12 f = fopen("shopping.txt", "r");13 while (fgets(line, sizeof line, f) != NULL) {14 count++;15 printf("%d:%s", count, line);16 }17 fclose(f);18 printf("一共 %d 行\n", count);19 return 0;20}
1:牛奶 2:鸡蛋 3:苹果 6 个 一共 3 行
注意 printf("%d:%s", count, line) 里没有 \n。fgets 会把行末的 '\n' 也读进来,line 本来就以它结尾。点单步,每次 fgets 之后看看 line 的格子:'\0' 前面就是那个 '\n'。
读数
fgets 读到的是文字。要读数,用 fscanf 更方便:它就是对着文件用的 scanf(第 3 课)。它返回读到了几个值,== 1 就是「读到了一个数」。到了文件末尾读不出来了,循环就停下。
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("steps.txt", "w");5 int week[5] = {4200, 6100, 5300, 8000, 7200};6 for (int i = 0; i < 5; i++) {7 fprintf(f, "%d\n", week[i]);8 }9 fclose(f);10 11 int steps;12 int total = 0;13 int days = 0;14 f = fopen("steps.txt", "r");15 while (fscanf(f, "%d", &steps) == 1) {16 total += steps;17 days++;18 }19 fclose(f);20 printf("%d 天,共 %d 步,平均每天 %.1f 步\n", days, total, (double)total / days);21 return 0;22}
5 天,共 30800 步,平均每天 6160.0 步
fscanf 读数之前会跳过空格和换行,所以数写在一行还是好几行都一样。
每行一个名字一个数
fscanf 一次可以读好几个值。"%s %d" 里,%s 读一个词放进 char 数组(读到空格为止),%d 再读一个数。两个都读到了,它返回 2:
1#include <stdio.h>2#include <string.h>3 4int main(void) {5 FILE *f = fopen("scores.txt", "w");6 fprintf(f, "小林 87\n");7 fprintf(f, "小王 92\n");8 fprintf(f, "小陈 78\n");9 fclose(f);10 11 char name[20];12 int score;13 char best_name[20] = "";14 int best = -1;15 f = fopen("scores.txt", "r");16 while (fscanf(f, "%19s %d", name, &score) == 2) {17 if (score > best) {18 best = score;19 strcpy(best_name, name);20 }21 }22 fclose(f);23 printf("最高分:%s,%d 分\n", best_name, best);24 return 0;25}
最高分:小王,92 分
这里有两处要留意。name 前面没有 &:数组名本身就是地址(第 17 课)。%19s 最多读 19 个字节,名字再长也不会写出 name[20] 的边界。一个汉字在 UTF-8 里占 3 个字节,小林 占 6 个。
往文件后面加
"a" 模式保留文件里原来的内容,接着往后写。比如日志,应该越记越多,而不是每次从头开始:
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("log.txt", "w");5 fprintf(f, "09:00 开门\n");6 fclose(f);7 8 f = fopen("log.txt", "a");9 fprintf(f, "12:30 午休\n");10 fprintf(f, "18:00 关门\n");11 fclose(f);12 13 char line[100];14 f = fopen("log.txt", "r");15 while (fgets(line, sizeof line, f) != NULL) {16 printf("%s", line);17 }18 fclose(f);19 return 0;20}
09:00 开门 12:30 午休 18:00 关门
第一次 fopen 用 "w",日志从空的开始;第二次用 "a",新的两行就接在 09:00 开门 后面。
小技巧: 用 "w" 和 "a" 打开只能写,用 "r" 打开只能读。刚写完的文件想读,就先关上,再用 "r" 打开,这里每段例子都是这么做的。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
给每行编号
✓程序往 todo.txt 里写了三行。把它们读回来,每行前面加上编号显示,比如 1. 买牛奶。
1. 买牛奶 2. 给妈妈打电话 3. 交房租
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("todo.txt", "w");5 fprintf(f, "买牛奶\n");6 fprintf(f, "给妈妈打电话\n");7 fprintf(f, "交房租\n");8 fclose(f);9 10 return 0;11}
价格加起来
✓程序往 prices.txt 里写了几个价格。用 fscanf 把它们读回来,显示有几件、一共多少钱,比如 4 件,一共 23.75 元。
4 件,一共 23.75 元
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("prices.txt", "w");5 fprintf(f, "3.50\n8.25\n2.00\n10.00\n");6 fclose(f);7 8 return 0;9}
丢了一行
✓程序想记两条日记,可文件里只剩下第二条。改好它,让两行都显示出来。
下了一天雨 终于晴了
1#include <stdio.h>2 3int main(void) {4 FILE *f = fopen("diary.txt", "w");5 fprintf(f, "下了一天雨\n");6 fclose(f);7 f = fopen("diary.txt", "w");8 fprintf(f, "终于晴了\n");9 fclose(f);10 11 char line[100];12 f = fopen("diary.txt", "r");13 while (fgets(line, sizeof line, f) != NULL) {14 printf("%s", line);15 }16 fclose(f);17 return 0;18}
这一课学会了
fopen(名字, 模式)打开文件,返回FILE *;"r"读,"w"写(先清空),"a"往后面加。- 打不开时
fopen返回NULL:用之前先if (f == NULL)检查。 fprintf(f, ...)像printf一样写;fclose(f)保证都写进去,再把文件放开。fgets(line, sizeof line, f)读一行,行末的'\n'也在里面;到文件末尾返回NULL。fscanf(f, "%d", &x)像scanf一样读,返回读到几个值:== 1时接着读(两个值就是== 2)。