Kodewick
第 27 课,共 30 课目录

文件

第 27 课,共 30 课 · 第 6 部分 · 再往前 · 约 15 分钟

变量里的东西,程序一结束就没了。想留到下次用,就要写进文件。下面的程序往 hello.txt 里写一行,关上,再打开把这一行读回来:

main.c
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}

fopen 失败的时候

fopen 不一定成功。用 "r" 打开一个不存在的文件,或者名字写错了,它就返回 NULL。用指针之前一定要先检查:

main.c
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 程序多半会在那里崩溃;我们的引擎会停在那一行,告诉你原因:

main.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」:

main.c
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 就是「读到了一个数」。到了文件末尾读不出来了,循环就停下。

main.c
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:

main.c
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" 模式保留文件里原来的内容,接着往后写。比如日志,应该越记越多,而不是每次从头开始:

main.c
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" 打开,这里每段例子都是这么做的。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

给每行编号

程序往 todo.txt 里写了三行。把它们读回来,每行前面加上编号显示,比如 1. 买牛奶。

要显示
1. 买牛奶
2. 给妈妈打电话
3. 交房租
exercise1.c
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

价格加起来

程序往 prices.txt 里写了几个价格。用 fscanf 把它们读回来,显示有几件、一共多少钱,比如 4 件,一共 23.75 元。

要显示
4 件,一共 23.75 元
exercise2.c
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

丢了一行

程序想记两条日记,可文件里只剩下第二条。改好它,让两行都显示出来。

要显示
下了一天雨
终于晴了
exercise3.c
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 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)。