CC语言通关
第11课 · 文件
教材对照
第10章 · 书页 331–356

第11课 · 文件

FILE*、fopen/fclose、顺序读写、随机读写、出错检测

9 个小节 37 段代码 建议 2h
教材依据第10章《对文件的输入输出》书页 p.331–356
本课覆盖:教材第10章正文 + 书末习题(p.355–356)。标「教材 p.X」= 教材段落总结;「例题」= 教材例题;「习题」= 书末题目;「本课补充」= 非教材原文。
在全书对照表中查看 →

教材《C程序设计》第五版(谭浩强)第10章,书页 331–356,共 26 页|建议 2h

课前一句话

📖 本课补充 · 非教材原文

前 9 课的数据都住在内存里,程序一关就蒸发。这一课把数据搬进硬盘上的文件:打开、读写、关闭,复制、统计、按格式存取,还能直接跳到第 n 条记录。学完你能写出"关掉再打开、数据还在"的程序——期末文件编程题的骨架永远是:FILE * → fopen(判空)→ 读写 → fclose。

一、先补地基:本章要解决的问题

📖 本课补充 · 非教材原文

你写成绩管理程序:struct Student stu[50]; 从键盘录入、算平均分、排名,屏幕打得整整齐齐。关掉窗口,第二天再打开——一片空白。因为数组住在内存里:内存是"桌面",一断电就清空;硬盘才是"仓库",放进去明年还在。本章就解决一件事:把内存数据存到硬盘,下次再读回来。

把文件想成建在硬盘上的仓库,整章用这一个比喻:

仓库C 语言
仓库(建在外存,断电不丢)文件 file
仓库地址和编号文件名(路径 + 主干 + 后缀)
办租用手续、拿到提货单fopen,返回 FILE *
提货单 / 遥控器文件指针 FILE *fp
退租关门fclose
门口的小货车(凑满一车才进出)文件缓冲区
取货员脚下站的位置文件读写位置标记
一次搬一箱 / 一排 / 一整个托盘fgetc/fputc、fgets/fputs、fread/fwrite
直接走到第 n 号货架fseek
报警灯ferror

路线:10.1 认识仓库 → 10.2 开门关门 → 10.3 顺序搬货 → 10.4 走到指定货架 → 10.5 看报警灯。

二、知识点讲解

10.1 C 文件的有关基本知识

📖 教材 p.331–333 · 正文总结

10.1.1 什么是文件

📖 教材 p.331 · 正文总结

一句话:文件(file)一般指存储在外部介质上的数据的集合。

拆名字:file,英文"文件、档案"。操作系统以"文件"为单位管理外存数据。

种类内容例子
程序文件程序代码源程序 .c、目标文件 .obj、可执行文件 .exe
数据文件供程序运行时读写的数据学生成绩、货物交易记录

本章讨论数据文件。屏幕留不住结果,所以要输出到磁盘、需要时再读回。操作系统把设备也当作文件:键盘是输入文件,显示器、打印机是输出文件。

输入输出是数据传送的过程,数据像流水一样从一处流向另一处,所以常称为流(stream),即数据流。 输入时数据从文件流向内存,输出时从内存流向文件。

C 把文件看作一个字符(或字节)的序列,叫流式文件:① 由一连串字节组成,不考虑行的界限;② 两行之间不会自动加分隔符;③ 存取以字节为单位;④ 开始和结束只受程序控制,不受回车换行这类物理符号控制。

考点程序文件 vs 数据文件;"流"的含义;C 的文件是流式文件、按字节处理。

易错以为文件是"一行一行"存的。不是——记事本里的换行只是文件里存了一个 '\n'(Windows 文本文件里是 '\r' '\n' 两个字节),行要你自己用 fgets 或判断 '\n' 切出来。

10.1.2 文件名

📖 教材 p.332 · 正文总结

一句话:文件标识 = 文件路径 + 文件名主干 + 文件后缀。如 D:\CC\temp\file1.dat:D:\CC\temp\ 是路径,file1 是主干,dat 是后缀。

  • 主干命名遵循标识符规则(字母、数字、下划线,不能以数字开头);
  • 后缀表示性质:doc(Word)、txt(文本)、dat(数据)、c(C 源程序)、cpp(C++ 源程序)、obj(目标)、exe(可执行)等。

考点文件标识的三部分。

易错C 字符串里写 Windows 路径,反斜杠要写两个:"D:\\CC\\string.dat"。因为 \ 是转义符开头,\C、\s 会被当成转义序列。这是本章编程题最常见的丢分点。

10.1.3 文件的分类

📖 教材 p.332 · 正文总结

一句话:按数据组织形式,数据文件分 ASCII 文件(文本文件) 和 二进制文件(映像文件)。前者每字节存一个字符的 ASCII 码;后者把内存数据不加转换输出,是内存数据的映像。

看整数 10000:ASCII 形式存字符 '1' '0' '0' '0' '0',共 5 字节;二进制形式把内存里 4 字节的 int 原样搬过去,共 4 字节(Visual C++ 下 int 为 4 字节)。

对比项文本文件二进制文件
存什么字符的 ASCII 码,一字节一字符内存数据原样拷贝
空间较多(10000 占 5 字节)较少(10000 占 4 字节)
速度要来回转换,较慢不转换,较快
人能否直接看懂能,记事本可读一般不能,像乱码
典型用途文章、配置、给别的软件读的数据中间数据、大批事务数据、结构体记录

考点文本 vs 二进制的区别;10000 两种存法各占几字节;各自优缺点。

易错"二进制文件"不是"只能存数字",而是内存里什么样、文件里就什么样,不做转换。结构体、数组都能整块写成二进制文件。

10.1.4 文件缓冲区

📖 教材 p.333 · 正文总结

一句话:系统在内存里给每个正在使用的文件开辟一块文件缓冲区,数据先攒着,攒够一批再和磁盘交换。

磁盘慢、内存快,每写一字节就真写一次盘太亏。ANSI C 采用"缓冲文件系统":输出时数据先送到缓冲区,装满后才一起送到磁盘;输入时一次读一批把缓冲区充满,程序再从缓冲区逐个取。每个文件在内存中只有一个缓冲区,读写共用。

比喻:缓冲区是仓库门口的小货车——搬一个小箱子就往仓库跑太亏,先堆到车上,装满再开进去。

考点缓冲文件系统的作用;缓冲区满才写盘;fclose 会把缓冲区数据写出去。

易错不关文件就结束程序,缓冲区里没装满的数据可能丢失。有的系统会自动冲刷,但别依赖——养成"用完就 fclose"的习惯。

10.1.5 文件类型指针

📖 教材 p.333 · 正文总结

一句话:FILE 是 stdio.h 里声明好的结构体类型,存放一个文件的信息;FILE * 是指向它的指针,习惯叫文件指针。

每个被使用的文件都在内存中开辟一个文件信息区,存放文件名、状态、当前读写位置等,这些信息保存在一个 FILE 类型(typedef struct {...} FILE;)的结构体变量里。不同编译系统的 FILE 内容大同小异,成员不必深究。程序里一般不直接定义 FILE 变量,而是定义指向它的指针:

c
FILE *fp;          // fp 是指向 FILE 类型数据的指针变量

fopen 返回文件信息区的起始地址赋给 fp,fp 就与文件关联;有 n 个文件就设 n 个指针。

注意:指向文件的指针变量并不是指向外部介质上的数据文件的开头,而是指向内存中的文件信息区的开头。

系统自动打开三个标准流文件并定义三个指针:stdin(标准输入,键盘)、stdout(标准输出,显示器)、stderr(标准出错,显示器)。所以 printf、scanf 其实都在操作已经打开好的文件。

考点FILE 必须大写;FILE *fp; 的定义;文件指针指向内存中的文件信息区而非磁盘文件;三个标准流指针。

易错FILE 是类型名不是变量;FILE fp;(少 *)定义的是结构体变量,一般不用。别把"文件指针(FILE *)"和"文件位置标记(读到第几字节)"混为一谈。

10.2 打开与关闭文件

📖 教材 p.334–337 · 正文总结

打开:为文件建立文件信息区和文件缓冲区,并让一个指针变量指向它;关闭:撤销信息区和缓冲区,使指针不再指向该文件。

10.2.1 用 fopen 函数打开数据文件

📖 教材 p.335–336 · 正文总结

一句话:fopen(文件名, 使用文件方式) 按指定方式打开文件,成功返回指向该文件信息区的指针,失败返回 NULL。

拆名字:f = file(文件)+ open(打开)。返回的指针通常赋给 FILE *fp;,打开时通知系统三件事:文件名、使用方式、哪个指针指向它。

使用文件方式(表 10.1):

方式含义文件不存在时
r 只读输入数据,打开已存在的文本文件出错
w 只写输出数据,打开一个文本文件建立新文件
a 追加向文本文件尾添加数据出错
rb 只读输入数据,打开二进制文件出错
wb 只写输出数据,打开二进制文件建立新文件
ab 追加向二进制文件尾添加数据出错
r+ 读写读写,打开文本文件出错
w+ 读写读写,建立新的文本文件建立新文件
a+ 读写读写,打开文本文件出错
rb+ 读写读写,打开二进制文件出错
wb+ 读写读写,建立新的二进制文件建立新文件
ab+ 读写读写,打开二进制文件出错

教材说明里最要命的几条:

  1. r:只能读;文件必须已存在且有数据,否则出错。
  2. w:只能写;不存在就新建,已存在则先删掉再重建——原内容被清空!
  3. a:向末尾追加,不删原数据,位置标记移到末尾;教材指出文件应已存在,否则出错。
  4. 带 +:可读可写。r+ 要求文件存在;w+ 新建(或清空);a+ 位置在末尾,可添加也可读。
  5. 失败:返回空指针 NULL(stdio.h 中定义为 0),原因可能是文件不存在、磁盘故障、磁盘已满等。
  6. b 只影响换行符处理:文本方式输出时把 '\n' 转成 '\r' '\n',读入时又转回 '\n';二进制方式不转换。
  7. wb 不代表自动把 ASCII 转成二进制:fprintf/fscanf 按 ASCII,fread/fwrite 按二进制,b 只是通知系统"不必转换回车符"。
  8. stdin、stdout、stderr 程序启动时自动打开;有的系统方式名不同(如 rw),注意所用系统的规定。

必须背的判空骨架:

c
if ((fp = fopen("file1", "r")) == NULL)
{
    printf("cannot open this file\n");
    exit(0);
}

有错就提示,后用 exit 关闭所有文件、终止程序;exit 要用 stdlib.h。

考点两个参数与返回值;12 种方式;w 清空、a 追加、b 只管换行;失败返回 NULL;判空模板。

易错① 不判 NULL,文件不存在时后面的 fgetc 直接崩溃;② 想追加却用 "w",清空了原数据;③ "r+" 和 "w+" 搞混;④ 路径里的 \ 忘了写两个。

10.2.2 用 fclose 函数关闭数据文件

📖 教材 p.337 · 正文总结

一句话:fclose(文件指针) 关闭文件,成功返回 0,失败返回 EOF(-1)。

拆名字:f = file + close(关闭)。关闭就是撤销文件信息区和缓冲区,使指针与文件"脱钩",除非重新打开。

为什么必须关? 写数据时先送到缓冲区,缓冲区充满才正式写盘;数据没充满时程序结束,缓冲区里的数据可能丢失。fclose 会先把缓冲区数据输出到磁盘,再撤销文件信息区。

考点返回值(成功 0,失败 EOF);关闭的两个作用(冲刷缓冲区 + 撤销信息区)。

易错只 fopen 不 fclose;以为关闭只是礼貌动作。

10.3 顺序读写数据文件

📖 教材 p.338–349 · 正文总结

顺序写先写的在前、后写的在后;顺序读先读前面的、后读后面的——读写顺序与数据在文件中的物理顺序一致。

10.3.1 怎样向文件读写字符

📖 教材 p.338–340 · 正文总结
函数调用功能返回值
fgetcfgetc(fp)从 fp 指向的文件读入一个字符成功带回所读字符;失败返回 EOF(-1)
fputcfputc(ch, fp)把字符 ch 写到 fp 指向的文件成功返回输出的字符;失败返回 EOF

拆名字:fgetc = file + get(获取)+ c(character 字符);fputc = file + put(放)+ c。比喻:一次搬一个箱子。

文件复制与统计的骨架:while ((ch = fgetc(in)) != EOF) { fputc(ch, out); } 就是复制;把循环体换成 count++; 就是统计字符数——例 10.2 和变式 1 用同一个骨架。stdio.h 还把它们定义成宏 putc/getc,作用相同。

考点fgetc 返回值是 int(要能装下 -1);EOF 在 stdio.h 中定义为 -1;fputc 第一参数是字符、第二是文件指针。

易错用 char ch 接 fgetc 返回值。若 char 无符号,永远不等于 -1,循环停不下来。正确写法 int ch;。

10.3.2 怎样向文件读写一个字符串

📖 教材 p.341–344 · 正文总结

fgets(str, n, fp) 从文件读入长度 n-1 的字符串,最后加 '\0',共 n 个字符存到 str。

函数调用功能返回值
fgetsfgets(str, n, fp)从文件读入长度 n-1 的字符串,存到 str成功返回地址 str;失败返回 NULL
fputsfputs(str, fp)把 str 所指字符串写到文件成功返回 0;否则返回非 0

拆名字:fgets = file + get + s(string 字符串);fputs = file + put + s。比喻:一次搬一排箱子。

说明:① fgets 只读 n-1 个字符,再加 '\0';读完 n-1 个之前遇到 '\n' 或 EOF 就提前结束,但遇到的 '\n' 也作为一个字符读入;成功返回 str 首地址,一开始就遇文件尾或出错返回 NULL。② fputs 第一参数可以是字符串常量、字符数组名或字符指针;末尾 '\0' 不输出;成功返回 0,失败返回 EOF。③ 它们类似 gets/puts,只是后者以终端为对象。

考点fgets 最多读 n-1 个;遇换行提前结束并把换行也读进来;fputs 不自动换行、不输出 '\0'。

易错① fgets 读完后 printf("%s", str) 每行之间多空行——因为 str 里已有 '\n';② fputs 连写多个字符串会连成一片——要自己 fputs("\n", fp);③ n 太小,一行被截成两段。

10.3.3 用格式化的方式读写文本文件

📖 教材 p.345 · 正文总结

一句话:fprintf/fscanf 就是带第一个"文件指针"参数的 printf/scanf。f = file,print = 打印,f = formatted 格式化;scan = 扫描。作用相仿,唯一不同是读写对象不是终端而是文件。

c
fprintf(文件指针, 格式字符串, 输出表列);
fscanf(文件指针, 格式字符串, 输入表列);

如 fprintf(fp, "%d,%.2f", i, f);:i=3、f=4.5 时文件上是字符 3,4.50;fscanf(fp, "%d,%f", &i, &f); 又能读回。

方便,但输入时要把 ASCII 转成二进制、输出时再转回字符,花时间。频繁交换数据时最好用 fread/fwrite。

考点只比 printf/scanf 多第一个文件指针参数;按 ASCII 方式读写。

易错fscanf 漏写 &;格式串的分隔符要和文件里的实际字符对上。

10.3.4 用二进制方式向文件读写一组数据

📖 教材 p.345–349 · 正文总结

一句话:fread/fwrite 按数据块读写,以二进制形式、原样搬运。f = file,read/write = 读/写。

c
fread(buffer, size, count, fp);
fwrite(buffer, size, count, fp);
参数含义
buffer一个地址。fread 是存放读入数据的存储区地址;fwrite 是要输出数据的存储区起始地址
size要读写的字节数(每个数据项长度)
count要读写多少个数据项(每项长 size)
fpFILE 类型指针

例:fread(f, 4, 10, fp); 中 f 是 float 型数组名,从文件读入 10 个 4 字节数据存到 f。对结构体数组:

c
for (i = 0; i < 40; i++)
    fread(&stud[i], sizeof(struct Student_type), 1, fp);   // 读一个学生
for (i = 0; i < 40; i++)
    fwrite(&stud[i], sizeof(struct Student_type), 1, fp);  // 写一个学生

类型为 int,成功返回形参 count 的值,即读写的数据项个数。比喻:整托盘搬运,连托盘一起原样搬,不拆包不转换。

考点四参数含义和顺序;sizeof(类型) 求字节数;返回值 = 成功读写的数据项个数;结构体要取地址 &stud[i]。

易错① sizeof(stud) 是整个数组大小,不是一条记录;② size 和 count 写反(结果常相同但语义不同);③ 用 "r"/"w" 打开却用 fread/fwrite,Windows 上换行转换会弄坏数据;④ 不判断返回值。

教材特别提醒的四组概念(每组都有文本、二进制两种):

角度文本方式二进制方式
数据存储方式以字符(ASCII 码)存储,如整数 12 占 2 字节按内存状态原样复制,如整数 12 占 4 字节
文件分类文本文件(ASCII 文件),全部为 ASCII 字符二进制文件(映像文件)
打开方式不带 b,读写时对换行符进行转换带 b,不进行转换
读写函数fgetc、fgets、fputc、fputs、fscanf、fprintf 等getw、putw、fread、fwrite 等

C 不禁止两者交叉,但往往导致结果出错,不提倡。文本文件用文本方式打开、文本函数读写;二进制文件亦然。

10.4 随机读写数据文件

📖 教材 p.350–354 · 正文总结

顺序读写效率有时很低:文件有 1000 个数据,只查第 1000 个,也得先读前 999 个。随机访问可以直接访问任何位置的数据。

10.4.1 文件位置标记及其定位

📖 教材 p.350–352 · 正文总结

一句话:文件位置标记指示"接下来要读写的下一个字符的位置"。

  • 顺序读时,标记从文件开头逐字节后移;顺序写时,每写完一个数据也后移;
  • 人为把标记移到任意位置再读写,就是随机读写——读写顺序与数据在文件中的物理顺序一般不一致。

教材特意区分:有的书叫它"文件位置指针",但它和 C 的指针不是一回事——文件在外存上,没有内存地址。本书统一叫"文件位置标记",别和 FILE * 混。

(1)rewind——回到文件开头

c
rewind(fp);

使位置标记重新返回文件开头,没有返回值,同时把 feof 的值恢复为 0(假)。

(2)fseek——改变位置标记

c
fseek(文件类型指针, 位移量, 起始点);

起始点用 0、1、2 代替(表 10.4):

起始点名字数字
文件开始位置SEEK_SET0
文件当前位置SEEK_CUR1
文件末尾位置SEEK_END2

"位移量"是以起始点为基点向前移动的字节数,应是 long 型(数字末尾加 L)。fseek 一般用于二进制文件:fseek(fp, 100L, 0) 移到离开头 100 字节处;fseek(fp, -10L, 2) 从末尾后退 10 字节。

(3)ftell——报告当前位置

得到文件位置标记的当前位置,用相对文件开头的位移量表示。调用出错(如 fp 指向的文件不存在)返回 -1L:

c
i = ftell(fp);                    // 变量 i 存放文件当前位置
if (i == -1L) printf("error\n");  // 出错则输出 error

考点rewind 无返回值、回开头并清 feof;fseek 三参数;SEEK_SET/SEEK_CUR/SEEK_END = 0/1/2;位移量是 long;ftell 返回相对开头的字节数,出错 -1L。

易错① 位移量忘写 L;② fseek(fp, 10, 0) 是"跳过 10 个字节",读到的是第 11 个字符;③ 在文本文件上用 fseek 算字节位置——换行转换会让位置对不上,fseek 一般只用于二进制文件;④ 以为 rewind 会删内容。

10.4.2 随机读写

📖 教材 p.353–354 · 正文总结

有了 rewind 和 fseek 就能随机读写。教材例 10.6:文件里有 10 个学生数据,要求读第 1、3、5、7、9 个并显示。做法:以 rb 打开 → 读第 1 个 → 用 fseek 移到第 3、5、7、9 个记录开头逐个读 → 关闭。关键一行:

c
fseek(fp, i * sizeof(struct Student_type), 0);   // i 取 0,2,4,6,8

i 是下标,乘一条记录的字节数,正好跳到第 i+1 条记录开头。

考点随机读写的关键是 fseek 的偏移量算对:第 i 条记录(从 0 数)的偏移 = i * 每条记录的字节数。

易错偏移量忘了乘 sizeof;或 sizeof 用的类型和文件里实际写的类型不一致,跳过去就错位了。

10.5 文件读写的出错检测

📖 教材 p.355 · 正文总结

1. ferror:调用输入输出函数出错时,除了返回值有反映,还可以用 ferror(fp) 检查——返回 0(假)未出错,返回非零出错。对同一个文件,每调用一次输入输出函数都会产生一个新的 ferror 值,所以应当调用后立即检查,否则信息会丢失。执行 fopen 时 ferror 初值自动置 0。

2. clearerr:使文件出错标志和文件结束标志都置为 0。出错后应立即 clearerr(fp),使 ferror(fp) 变 0,以便下次检测。出错标志会一直保留,直到对同一文件调用 clearerr、rewind 或任何其他一个输入输出函数。

比喻:ferror 是报警灯,clearerr 是复位按钮。

考点ferror 返回 0 未出错、非 0 出错;clearerr 清除出错标志和结束标志两个;rewind 也能清结束标志。

易错把 feof 当出错检测。feof 只回答"文件尾标志读过没有",不回答"有没有出错"。

书末习题(书页 355–356)

📖 教材 p.355–356 · 习题

本章书末共 11 题:第 1、2 题概念题,第 3~11 题编程题。完整原文、思路和可编译代码见第四节,一题不漏。

三、现场写代码

📖 教材例题 · 本课范围 p.331–355

例 10.1 键盘输入字符,逐个存盘,直到输入 '#'

📖 教材 p.338 · 例题 10.1

思路:getchar 逐个读键盘字符,fputc 逐个写到文件。

c
#include <stdio.h>
#include <stdlib.h>
int main()
{
    FILE *fp;
    char ch, filename[10];
    printf("请输入所用的文件名:");
    scanf("%s", filename);
    getchar();
    if ((fp = fopen(filename, "w")) == NULL)
    {
        printf("cannot open file\n");
        exit(0);
    }
    printf("请输入一个准备存储到磁盘的字符串(以#结束):");
    ch = getchar();
    while (ch != '#')
    {
        fputc(ch, fp);
        putchar(ch);
        ch = getchar();
    }
    fclose(fp);
    putchar(10);
    return 0;
}

逐行解释:

  • 第 1~2 行:stdio.h 提供 FILE、fopen、fputc、fclose、getchar;stdlib.h 提供 exit。
  • 第 5 行:文件指针,接 fopen 的返回值;第 6 行:ch 存字符,filename[10] 存文件名。
  • 第 8 行 scanf("%s", filename);:数组名本身就是地址,不加 &。
  • 第 9 行 getchar();:把 scanf 留在缓冲区的回车吃掉,否则第 16 行会先读到回车。
  • 第 10 行:fopen 以 "w" 打开并直接判空——赋值表达式的值就是 fp。
  • 第 12~13 行:打不开就提示并用 exit(0) 终止。
  • 第 16~21 行:读第一个字符;不是 # 就循环:写文件、显示到屏幕、读下一个。
  • 第 23 行:关闭文件,缓冲区数据真正落盘;第 24 行:输出换行(10 是 '\n' 的 ASCII 码)。

运行结果:

c
请输入所用的文件名:file1.dat
请输入一个准备存储到磁盘的字符串(以#结束):computer and c#
computer and c

补充:教材这里用 char ch 且只判断 '#'。如果用户按 Ctrl+Z(EOF),ch 会等于 -1,永远不等于 '#',程序就死循环了。更稳的写法是 int ch; 且 while ((ch = getchar()) != '#' && ch != EOF)。

例 10.2 把一个文件复制到另一个文件

📖 教材 p.340 · 例题 10.2

思路:从 file1.dat 逐个读字符,逐个写到 file2.dat。

c
#include <stdio.h>
#include <stdlib.h>
int main()
{
    FILE *in, *out;
    char ch, infile[10], outfile[10];
    printf("输入读入文件的名字:");
    scanf("%s", infile);
    printf("输入输出文件的名字:");
    scanf("%s", outfile);
    if ((in = fopen(infile, "r")) == NULL)
    {
        printf("无法打开此文件\n");
        exit(0);
    }
    if ((out = fopen(outfile, "w")) == NULL)
    {
        printf("无法打开此文件\n");
        exit(0);
    }
    ch = fgetc(in);
    while (!feof(in))
    {
        fputc(ch, out);
        putchar(ch);
        ch = fgetc(in);
    }
    putchar(10);
    fclose(in);
    fclose(out);
    return 0;
}

逐行解释:

  • 第 5~6 行:两个文件指针一读一写;两个文件名数组。
  • 第 11~15 行:r 打开输入文件(必须已存在),失败退出;第 16~20 行:w 打开输出文件(不存在新建,存在清空)。
  • 第 21 行:先读第一个字符;第 22 行:!feof(in) 为真说明还没到文件尾。
  • 第 24~26 行:写文件、显示、读下一个——第 26 行让 feof 有机会变真。
  • 第 28 行:换行;第 29~30 行:两个文件都关闭。

运行结果:

c
输入读入文件的名字:file1.dat
输入输出文件的名字:file2.dat
computer and c

要点:while 条件也可写成 while (ch != EOF),因为 EOF 就是 -1。用 feof 是为让你理解文件尾标志。

例 10.3 输入若干字符串,排序后存入文件

📖 教材 p.342 · 例题 10.3

思路:读入 n 个字符串到二维数组 → 按字母顺序选择法排序 → 逐个写入文件(每个后面补 \n)。

教材用 gets。gets 在 C11 已被删除,VS2022 会报错,下面改用 fgets 并去掉行末换行,思路不变。

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    FILE *fp;
    char str[3][10], temp[10];
    int i, j, k, n = 3, len;
    printf("Enter strings:\n");
    for (i = 0; i < n; i++)
    {
        fgets(str[i], 10, stdin);
        len = (int)strlen(str[i]);
        if (len > 0 && str[i][len - 1] == '\n')
            str[i][len - 1] = '\0';
    }
    for (i = 0; i < n - 1; i++)
    {
        k = i;
        for (j = i + 1; j < n; j++)
            if (strcmp(str[k], str[j]) > 0) k = j;
        if (k != i)
        {
            strcpy(temp, str[i]);
            strcpy(str[i], str[k]);
            strcpy(str[k], temp);
        }
    }
    if ((fp = fopen("D:\\CC\\string.dat", "w")) == NULL)
    {
        printf("can't open file!\n");
        exit(0);
    }
    printf("\nThe new sequence:\n");
    for (i = 0; i < n; i++)
    {
        fputs(str[i], fp);
        fputs("\n", fp);
        printf("%s\n", str[i]);
    }
    fclose(fp);
    return 0;
}

逐行解释:

  • 第 7 行:str[3][10] 是 3 行、每行最多 9 字符的二维数组;temp 用于交换。
  • 第 10~16 行:循环读 3 个字符串;fgets 最多读 9 个字符;末尾是 '\n' 就改成 '\0' 去掉换行。
  • 第 17~28 行:选择法排序。k=i 先假设第 i 个最小;内层用 strcmp 比较,谁小记谁;k != i 时用 strcpy 借 temp 交换。
  • 第 29 行:路径 "D:\\CC\\string.dat" 中的 \\ 是转义后的一个反斜杠;运行前要保证 D 盘有 CC 目录。
  • 第 37~38 行:写字符串;fputs 不自动换行,所以第 38 行人为补 "\n",否则以后读回来会连成一片。
  • 第 39 行:屏幕也显示一遍。

运行结果:

c
Enter strings:
CHINA
CANADA
INDIA

The new sequence:
CANADA
CHINA
INDIA

例 10.4 用二进制方式存 10 个学生的数据

📖 教材 p.346 · 例题 10.4

思路:定义含姓名、学号、年龄、地址的结构体数组;输入后用 fwrite 一次写一个学生。

c
#include <stdio.h>
#define SIZE 10
struct Student_type
{
    char name[10];
    int num;
    int age;
    char addr[15];
} stud[SIZE];

void save()
{
    FILE *fp;
    int i;
    if ((fp = fopen("stu.dat", "wb")) == NULL)
    {
        printf("cannot open file\n");
        return;
    }
    for (i = 0; i < SIZE; i++)
        if (fwrite(&stud[i], sizeof(struct Student_type), 1, fp) != 1)
            printf("file write error\n");
    fclose(fp);
}

int main()
{
    int i;
    printf("Please enter data of students:\n");
    for (i = 0; i < SIZE; i++)
        scanf("%s%d%d%s", stud[i].name, &stud[i].num, &stud[i].age, stud[i].addr);
    save();
    return 0;
}

逐行解释:

  • 第 3~9 行:结构体类型 + 全局数组 stud[SIZE],main 和 save 都能用。
  • 第 11~24 行 save:"wb" 打开 stu.dat;循环 10 次,fwrite(&stud[i], sizeof(struct Student_type), 1, fp) 把第 i 个学生整块写入;返回值 != 1 说明写失败;最后关闭。
  • 第 26~34 行 main:循环输入 10 人;name、addr 是数组名不加 &,num、age 是 int 要加 &。
  • 第 32 行:调用 save() 写盘。

运行结果(节选):

c
Please enter data of students:
Zhang 1001 19 room_101
Sun 1002 20 room_102
...

教材指出:成员长度之和 10+4+4+15 = 33,实际占 36 字节(4 的倍数,按 4 字节对齐)。

读回验证:教材紧接着给了一个对称的程序——用 "rb" 打开 stu.dat,循环 fread(&stud[i], sizeof(struct Student_type), 1, fp) 读回,再用 printf("%-10s%4d%4d%-15s\n", ...) 输出。写用 "wb",读必须用 "rb";用 "r" 读二进制数据会因换行转换把数据弄乱。fread 按块长度读,不认空格和换行,所以别用它直接从键盘读结构体。

例 10.5 用 rewind 让位置标记回到开头

📖 教材 p.351 · 例题 10.5

思路:第一次显示完,位置标记已在末尾、feof 为真;必须 rewind 才能第二次从头复制。

c
#include <stdio.h>
int main()
{
    FILE *fp1, *fp2;
    char ch;
    fp1 = fopen("file1.dat", "r");
    fp2 = fopen("file2.dat", "w");
    ch = getc(fp1);
    while (!feof(fp1))
    {
        putchar(ch);
        ch = getc(fp1);
    }
    putchar(10);
    rewind(fp1);
    ch = getc(fp1);
    while (!feof(fp1))
    {
        fputc(ch, fp2);
        ch = fgetc(fp1);
    }
    fclose(fp1);
    fclose(fp2);
    return 0;
}

逐行解释:

  • 第 6~7 行:打开输入、输出文件(本例为简化未判空,实际要加)。
  • 第 8~13 行:第一个循环显示全部字符;每轮末尾再读一个,读到文件尾使 feof 变真。
  • 第 14 行:换行;第 15 行 rewind(fp1);:位置回开头,同时把 feof 恢复为 0——没有这一行,下面循环一次都不执行。
  • 第 16~21 行:从开头重新读,逐个写进 file2.dat。
  • 第 22~23 行:关闭两个文件。

运行结果:屏幕显示 computer and c,file2.dat 里也是同样内容。

记住:feof(fp1) 为真的条件是——读完最后一个字符后再读一次文件,即要读一次 EOF。rewind 顺手把 feof 复位了。

例 10.6 用 fseek 随机读第 1、3、5、7、9 个学生

📖 教材 p.353 · 例题 10.6

思路:读例 10.4 生成的 stu.dat;fseek(fp, i * sizeof(struct Student_type), 0) 直接跳到第 i 条记录;fread 读一条并显示;i 每次加 2。

c
#include <stdio.h>
#include <stdlib.h>
struct Student_type
{
    char name[10];
    int num;
    int age;
    char addr[15];
} stud[10];

int main()
{
    int i;
    FILE *fp;
    if ((fp = fopen("stu.dat", "rb")) == NULL)
    {
        printf("can not open file\n");
        exit(0);
    }
    for (i = 0; i < 10; i += 2)
    {
        fseek(fp, i * sizeof(struct Student_type), 0);
        fread(&stud[i], sizeof(struct Student_type), 1, fp);
        printf("%-10s%4d%4d%-15s\n", stud[i].name, stud[i].num, stud[i].age, stud[i].addr);
    }
    fclose(fp);
    return 0;
}

逐行解释:

  • 第 3~9 行:结构体和数组必须和例 10.4 完全一样,否则 sizeof 不同,跳的位置就错。
  • 第 15 行:"rb" 打开当前目录下的 stu.dat。
  • 第 20 行:i 取 0、2、4、6、8,对应第 1、3、5、7、9 个学生。
  • 第 22 行:从文件开头(0 = SEEK_SET)跳过 i 条记录的字节数,正好落在第 i+1 条开头。
  • 第 23~24 行:读一条到 stud[i],按格式输出。

运行结果:

c
Zhang      1001  19 room_101
Tan        1003  21 room_103
Li         1006  22 room_105
Zhen       1008  16 room_107
Qin        1012  19 room_109

每条记录固定 36 字节,第 i 条起始位置就是 i * 36,直接算出来跳过去——这就是随机读写的效率优势。

四、书上习题精讲

📖 教材 p.355–356 · 习题

第 1 题(概念)

📖 教材 p.355 · 习题 1

题目:什么是文件型指针?通过文件指针访问文件有什么好处?

答案:FILE 是 stdio.h 中声明的结构体类型,存放文件的名字、状态、当前读写位置等信息。文件型指针是指向 FILE 类型变量的指针,定义形式 FILE *fp;。打开文件时系统在内存建立文件信息区(一个 FILE 变量),fopen 返回其起始地址赋给 fp,fp 就与文件建立联系。

好处:① 一个指针代表整个文件,读写函数只传一个参数,方便;② 不必关心 FILE 内部成员,不同编译系统的 FILE 可以不同而用法不变,可移植;③ 多个文件各用一个指针,互不干扰;④ 通过指针还能访问和修改文件状态(位置标记、出错标志)。

第 2 题(概念)

📖 教材 p.355 · 习题 2

题目:对文件的打开与关闭的含义是什么?为什么要打开和关闭文件?

答案:打开是为文件建立文件信息区和文件缓冲区,并把一个文件指针与文件联系起来,此后可通过该指针读写;关闭是撤销信息区和缓冲区,使指针不再指向该文件,此后不能再用它读写。

为什么要打开:读写前必须让系统知道操作哪个文件、用什么方式,并建立缓冲机制,否则无法高效读写。

为什么要关闭:① 关闭时系统会把缓冲区中尚未写盘的数据输出到磁盘,防止丢失;② 释放信息区和缓冲区占用的内存;③ 防止文件被误用。

第 3 题(编程)

📖 教材 p.355 · 习题 3

题目:从键盘输入一个字符串,将其中的小写字母全部转换成大写字母,然后输出到一个磁盘文件 test 中保存,输入的字符串以"!"结束。

思路:getchar 循环读,读到结束符停;是小写字母就减 32;fputc 写入 test。书上写的是中文"!",键盘上按英文半角 !。

c
#include <stdio.h>
#include <stdlib.h>
int main()
{
    FILE *fp;
    int ch;
    fp = fopen("test", "w");
    if (fp == NULL) { printf("cannot open file\n"); exit(0); }
    printf("请输入字符串,以 ! 结束:\n");
    while ((ch = getchar()) != '!' && ch != EOF)
    {
        if (ch >= 'a' && ch <= 'z')
            ch = ch - 32;
        fputc(ch, fp);
    }
    fclose(fp);
    printf("\n已存入文件 test\n");
    return 0;
}

关键行:第 7 行以 "w" 打开/新建 test 并判空;第 10 行一边读一边判断,&& ch != EOF 防止按 Ctrl+Z 后死循环;第 12~13 行小写字母 ASCII 比大写大 32,减 32 即转换;第 14 行写文件;第 16 行关闭。

运行结果:输入 Hello World!,文件 test 内容为 HELLO WORLD。

第 4 题(编程)

📖 教材 p.355 · 习题 4

题目:有两个磁盘文件 A 和 B,各存放一行字母,今要求把这两个文件中的信息合并(按字母顺序排列),输出到一个新文件 C 中去。

思路:从 A、B 各读一行;去掉行末换行;拼接到 c;冒泡排序;写入 C。

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    FILE *fa, *fb, *fc;
    char a[100] = "", b[100] = "", c[200], t;
    int i, j, n, la, lb;
    fa = fopen("A.txt", "r");
    if (fa == NULL) { printf("cannot open A\n"); exit(0); }
    fb = fopen("B.txt", "r");
    if (fb == NULL) { printf("cannot open B\n"); exit(0); }
    fgets(a, 100, fa);
    fgets(b, 100, fb);
    fclose(fa);
    fclose(fb);
    la = (int)strlen(a);
    if (la > 0 && a[la - 1] == '\n') a[la - 1] = '\0';
    lb = (int)strlen(b);
    if (lb > 0 && b[lb - 1] == '\n') b[lb - 1] = '\0';
    strcpy(c, a);
    strcat(c, b);
    n = (int)strlen(c);
    for (i = 0; i < n - 1; i++)
        for (j = 0; j < n - 1 - i; j++)
            if (c[j] > c[j + 1])
            { t = c[j]; c[j] = c[j + 1]; c[j + 1] = t; }
    fc = fopen("C.txt", "w");
    if (fc == NULL) { printf("cannot open C\n"); exit(0); }
    fputs(c, fc);
    fputs("\n", fc);
    fclose(fc);
    printf("%s\n", c);
    return 0;
}

关键行:第 13~14 行各读一行(fgets 会带上换行);第 17~20 行去掉行末 '\n',否则合并后中间夹换行;第 21~22 行 strcpy + strcat 拼接;第 24~27 行冒泡排序;第 28~32 行写 C 并补换行。

说明:A 是 ace、B 是 bdf,C 就是 abcdef——按字符 ASCII 码从小到大。

第 5 题(编程)

📖 教材 p.356 · 习题 5

题目:有 5 个学生,每个学生有 3 门课程的成绩,从键盘输入学生数据(包括学号、姓名、3 门课程成绩),计算出平均成绩,将原有数据和计算出的平均分数存放在磁盘文件 stud 中。

思路:结构体含学号、姓名、3 门成绩、平均分;循环输入 5 人并算平均;fwrite 二进制写入 stud。

c
#include <stdio.h>
#include <stdlib.h>
#define N 5
struct Student
{
    int num;
    char name[20];
    float score[3];
    float avg;
};
int main()
{
    FILE *fp;
    struct Student stu[N];
    int i;
    for (i = 0; i < N; i++)
    {
        scanf("%d %s %f %f %f", &stu[i].num, stu[i].name,
              &stu[i].score[0], &stu[i].score[1], &stu[i].score[2]);
        stu[i].avg = (stu[i].score[0] + stu[i].score[1] + stu[i].score[2]) / 3.0f;
    }
    fp = fopen("stud", "wb");
    if (fp == NULL) { printf("cannot open stud\n"); exit(0); }
    for (i = 0; i < N; i++)
        fwrite(&stu[i], sizeof(struct Student), 1, fp);
    fclose(fp);
    printf("已存入 stud\n");
    return 0;
}

关键行:第 4~10 行结构体把 avg 也放进去,才能"原数据+平均分"一次写入;第 16~21 行输入,name 不加 &;第 20 行用 3.0f 避免整数除法;第 22~25 行 "wb" 打开并写 5 条。

第 6 题(编程)

📖 教材 p.356 · 习题 6

题目:将第 5 题 stud 文件中的学生数据,按平均分进行排序处理,将已排序的学生数据存入一个新文件 stu_sort 中。

思路:从 stud 读 5 条到数组;按 avg 从高到低选择法排序;写入 stu_sort。

c
#include <stdio.h>
#include <stdlib.h>
#define N 5
struct Student
{
    int num;
    char name[20];
    float score[3];
    float avg;
};
int main()
{
    FILE *fp;
    struct Student stu[N], t;
    int i, j;
    fp = fopen("stud", "rb");
    if (fp == NULL) { printf("cannot open stud\n"); exit(0); }
    for (i = 0; i < N; i++)
        fread(&stu[i], sizeof(struct Student), 1, fp);
    fclose(fp);
    for (i = 0; i < N - 1; i++)
        for (j = i + 1; j < N; j++)
            if (stu[j].avg > stu[i].avg)
            { t = stu[i]; stu[i] = stu[j]; stu[j] = t; }
    fp = fopen("stu_sort", "wb");
    if (fp == NULL) { printf("cannot open stu_sort\n"); exit(0); }
    for (i = 0; i < N; i++)
        fwrite(&stu[i], sizeof(struct Student), 1, fp);
    fclose(fp);
    for (i = 0; i < N; i++)
        printf("%s %.2f\n", stu[i].name, stu[i].avg);
    return 0;
}

关键行:第 16~19 行以 "rb" 读 5 条;第 21~24 行选择法,stu[j].avg > stu[i].avg 就整体交换(结构体可以直接用 = 赋值);第 25~28 行写 stu_sort;第 30~31 行打印验证。

第 7 题(编程)

📖 教材 p.356 · 习题 7

题目:将第 6 题已排序的学生成绩文件进行插入处理。插入一个学生的 3 门课程成绩,程序先计算新插入学生的平均成绩,然后将它按成绩高低顺序插入,插入后建立一个新文件。

思路:把 stu_sort 的 5 条读进长度 6 的数组;输入新学生并算平均;找第一个比它低的位置 pos;从后往前后移;插入;写新文件。

c
#include <stdio.h>
#include <stdlib.h>
#define N 5
struct Student
{
    int num;
    char name[20];
    float score[3];
    float avg;
};
int main()
{
    FILE *fp;
    struct Student stu[N + 1], s;
    int i, pos;
    fp = fopen("stu_sort", "rb");
    if (fp == NULL) { printf("cannot open stu_sort\n"); exit(0); }
    for (i = 0; i < N; i++)
        fread(&stu[i], sizeof(struct Student), 1, fp);
    fclose(fp);
    printf("输入新学生:学号 姓名 3门成绩\n");
    scanf("%d %s %f %f %f", &s.num, s.name, &s.score[0], &s.score[1], &s.score[2]);
    s.avg = (s.score[0] + s.score[1] + s.score[2]) / 3.0f;
    pos = N;
    for (i = 0; i < N; i++)
        if (s.avg > stu[i].avg) { pos = i; break; }
    for (i = N; i > pos; i--)
        stu[i] = stu[i - 1];
    stu[pos] = s;
    fp = fopen("stu_sort_new", "wb");
    if (fp == NULL) { printf("cannot open stu_sort_new\n"); exit(0); }
    for (i = 0; i <= N; i++)
        fwrite(&stu[i], sizeof(struct Student), 1, fp);
    fclose(fp);
    for (i = 0; i <= N; i++)
        printf("%s %.2f\n", stu[i].name, stu[i].avg);
    return 0;
}

关键行:第 14 行数组开 N+1;第 24 行先假设插在最后;第 25~26 行找第一个平均分比它低的人;第 27~28 行从后往前后移(从前往后会覆盖数据);第 29 行放入新学生;第 30~34 行写新文件。

第 8 题(编程)

📖 教材 p.356 · 习题 8

题目:将第 7 题结果仍存入原有的 stu_sort 文件而不另建立新文件。

思路:与第 7 题相同,只是最后写回 stu_sort。因为数据已全部读进内存,先 fclose 读完的文件,再以 "wb" 重新打开(会清空),把 6 条写回即可——先读后写不会丢数据。文件很大放不进内存时,正确做法是写临时文件再 remove + rename 替换。

c
#include <stdio.h>
#include <stdlib.h>
#define N 5
struct Student
{
    int num;
    char name[20];
    float score[3];
    float avg;
};
int main()
{
    FILE *fp;
    struct Student stu[N + 1], s;
    int i, pos;
    fp = fopen("stu_sort", "rb");
    if (fp == NULL) { printf("cannot open stu_sort\n"); exit(0); }
    for (i = 0; i < N; i++)
        fread(&stu[i], sizeof(struct Student), 1, fp);
    fclose(fp);
    printf("输入新学生:学号 姓名 3门成绩\n");
    scanf("%d %s %f %f %f", &s.num, s.name, &s.score[0], &s.score[1], &s.score[2]);
    s.avg = (s.score[0] + s.score[1] + s.score[2]) / 3.0f;
    pos = N;
    for (i = 0; i < N; i++)
        if (s.avg > stu[i].avg) { pos = i; break; }
    for (i = N; i > pos; i--)
        stu[i] = stu[i - 1];
    stu[pos] = s;
    fp = fopen("stu_sort", "wb");
    if (fp == NULL) { printf("cannot open stu_sort\n"); exit(0); }
    for (i = 0; i <= N; i++)
        fwrite(&stu[i], sizeof(struct Student), 1, fp);
    fclose(fp);
    printf("已写回 stu_sort\n");
    return 0;
}

与第 7 题的差别只有两处:第 30 行仍打开 "stu_sort",第 35 行提示写回原文件。其余各行含义同第 7 题。关键顺序:必须先关闭读的文件,再以 "wb" 打开同一个名字;边读边用 "w" 打开会把没读完的数据清掉。

第 9 题(编程)

📖 教材 p.356 · 习题 9

题目:有一磁盘文件 employee,内存放职工的数据。每个职工的数据包括职工姓名、职工号、性别、年龄、住址、工资、健康状况、文化程度。今要求将职工名、工资的信息单独抽出来另建一个简明的职工工资文件。

思路:定义完整记录 Employee 和只有姓名、工资的 Salary;从 employee 逐条读,复制两个字段,写入 salary。

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 5
struct Employee
{
    char name[20];
    int num;
    char sex[4];
    int age;
    char addr[30];
    float wage;
    char health[10];
    char edu[10];
};
struct Salary
{
    char name[20];
    float wage;
};
int main()
{
    FILE *fin, *fout;
    struct Employee e;
    struct Salary s;
    int i;
    fin = fopen("employee", "rb");
    if (fin == NULL) { printf("cannot open employee\n"); exit(0); }
    fout = fopen("salary", "wb");
    if (fout == NULL) { printf("cannot open salary\n"); exit(0); }
    for (i = 0; i < N; i++)
    {
        if (fread(&e, sizeof(struct Employee), 1, fin) != 1) break;
        strcpy(s.name, e.name);
        s.wage = e.wage;
        fwrite(&s, sizeof(struct Salary), 1, fout);
    }
    fclose(fin);
    fclose(fout);
    printf("已生成 salary\n");
    return 0;
}

关键行:第 5~15 行完整记录,字段与题目一一对应;第 16~20 行简明记录;第 27~30 行打开两个文件;第 31~37 行循环读、strcpy 复制姓名(字符数组不能直接 =)、复制工资、写新文件;第 38~39 行关闭。

说明:employee 要先存在,生成它的录入程序与例 10.4 相同,只是把结构体换成 struct Employee。

第 10 题(编程)

📖 教材 p.356 · 习题 10

题目:从第 9 题的"职工工资文件"中删去一个职工的数据,再存回原文件。

思路:把 salary 全部读进数组(记录数未知,用 fread 返回值控制);输入要删的姓名;以 "wb" 重新打开同一文件(清空);把姓名不等于目标的记录写回。

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 100
struct Salary
{
    char name[20];
    float wage;
};
int main()
{
    FILE *fp;
    struct Salary s[MAX];
    char target[20];
    int n = 0, i, found = 0;
    fp = fopen("salary", "rb");
    if (fp == NULL) { printf("cannot open salary\n"); exit(0); }
    while (n < MAX && fread(&s[n], sizeof(struct Salary), 1, fp) == 1)
        n++;
    fclose(fp);
    printf("要删除的职工姓名:");
    scanf("%s", target);
    fp = fopen("salary", "wb");
    if (fp == NULL) { printf("cannot open salary\n"); exit(0); }
    for (i = 0; i < n; i++)
    {
        if (strcmp(s[i].name, target) == 0)
        {
            found = 1;
            continue;
        }
        fwrite(&s[i], sizeof(struct Salary), 1, fp);
    }
    fclose(fp);
    if (found) printf("已删除 %s\n", target);
    else printf("没找到 %s\n", target);
    return 0;
}

关键行:第 18~19 行 while (... fread(...) == 1) n++; 读到失败为止,不用事先知道人数;第 20 行读完必须关闭;第 23~24 行 "wb" 重新打开会清空,所以必须确认数据已在内存;第 25~33 行相同的跳过(continue),不同的写回;第 34~36 行提示结果。

第 11 题(编程)

📖 教材 p.356 · 习题 11

题目:从键盘输入若干行字符(每行长度不等),输入后把它们存储到一磁盘文件。再从该文件中读入这些数据,将其小写字母转换成大写字母后在显示屏上输出。

思路:fgets 一行一行读键盘(能处理长度不等的行),空行结束;写入文件;再 fgets 逐行读回,逐字符小写转大写并输出。

c
#include <stdio.h>
#include <stdlib.h>
int main()
{
    FILE *fp;
    char line[256];
    int i;
    fp = fopen("lines.txt", "w");
    if (fp == NULL) { printf("cannot open file\n"); exit(0); }
    printf("输入若干行,直接回车(空行)结束:\n");
    while (fgets(line, 256, stdin) != NULL)
    {
        if (line[0] == '\n') break;
        fputs(line, fp);
    }
    fclose(fp);
    fp = fopen("lines.txt", "r");
    if (fp == NULL) { printf("cannot open file\n"); exit(0); }
    while (fgets(line, 256, fp) != NULL)
    {
        for (i = 0; line[i] != '\0'; i++)
            if (line[i] >= 'a' && line[i] <= 'z')
                line[i] = line[i] - 32;
        printf("%s", line);
    }
    fclose(fp);
    return 0;
}

关键行:第 10~15 行从键盘读行,line[0]=='\n' 表示空行;fputs 写入(fgets 已带换行);第 17~18 行重新以 "r" 打开;第 19~26 行逐行读回,内层 for 扫每个字符,小写减 32,printf("%s", line) 输出。

🛠 随课练习(只给题不给答案)

📖 本课补充 · 非教材原文

🔮 猜输出

题 1:

c
#include <stdio.h>
int main(void)
{
    FILE *fp;
    int ch, n = 0;
    fp = fopen("q1.txt", "w");
    fputs("12345", fp);
    fclose(fp);
    fp = fopen("q1.txt", "r");
    fseek(fp, 2L, SEEK_SET);
    while ((ch = fgetc(fp)) != EOF)
    {
        putchar(ch);
        n++;
    }
    printf(" %d\n", n);
    fclose(fp);
    return 0;
}

题 2:

c
#include <stdio.h>
int main(void)
{
    FILE *fp;
    int a = 66;
    fp = fopen("q2.dat", "wb");
    fwrite(&a, sizeof(int), 1, fp);
    fclose(fp);
    a = 0;
    fp = fopen("q2.dat", "rb");
    fread(&a, sizeof(int), 1, fp);
    fclose(fp);
    printf("%d %c\n", a, a);
    return 0;
}

题 3:

c
#include <stdio.h>
int main(void)
{
    FILE *fp;
    long p1, p2;
    fp = fopen("q3.txt", "w");
    fputs("ABCDEF", fp);
    fclose(fp);
    fp = fopen("q3.txt", "r");
    fgetc(fp);
    fgetc(fp);
    p1 = ftell(fp);
    fseek(fp, -3L, SEEK_END);
    p2 = ftell(fp);
    printf("%ld %ld %c\n", p1, p2, fgetc(fp));
    fclose(fp);
    return 0;
}

🛠 变式题

变式 1(照猫画虎):把例 10.2 的文件复制程序改造成"统计文件有多少个字符",每读一个 count++,最后输出 count。

变式 2(改需求):把例 10.1 的结束符从 # 改成 @,文件名改为运行时键盘输入。

变式 3(翻译):把伪代码翻成 C:

c
打开 data.txt(只读)
sum = 0
for i = 1 to 10
    读一个整数 x
    sum = sum + x
关闭文件
输出 sum

变式 4(补全):

c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp;
    char s[50];
    fp = fopen("note.txt", "w");
    if (fp == NULL) { printf("cannot open\n"); exit(0); }
    fputs("hello\n", fp);
    /* TODO 1:再把 "world" 写进去,并换行 */
    fclose(fp);
    fp = fopen("note.txt", "r");
    if (fp == NULL) { printf("cannot open\n"); exit(0); }
    /* TODO 2:用 fgets 循环读,读到 NULL 为止,逐个 printf("%s", s) */
    fclose(fp);
    return 0;
}

🐛 改错题

改错 1:下面程序想复制文件,但 in.txt 可能不存在。找出错误并改正。

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main(void)
{
    FILE *fp;
    int ch;
    fp = fopen("in.txt", "r");
    while ((ch = fgetc(fp)) != EOF)
        putchar(ch);
    fclose(fp);
    return 0;
}

改错 2:下面程序为什么可能死循环?

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main(void)
{
    FILE *fp;
    char ch;
    fp = fopen("in.txt", "r");
    if (fp == NULL) return 1;
    while ((ch = fgetc(fp)) != EOF)
        putchar(ch);
    fclose(fp);
    return 0;
}

改错 3:下面程序想打开 D:\data\a.txt,为什么总打不开?

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main(void)
{
    FILE *fp;
    fp = fopen("D:\data\a.txt", "r");
    if (fp == NULL) { printf("cannot open\n"); return 1; }
    fclose(fp);
    return 0;
}

📈 加练题(你随时喊,我随时追加到这里)

📖 本课补充 · 非教材原文

(暂无。想加练就说"再出几道文件题",我出的题会标上日期追加在这块。)

💬 问答区(你问过的问题 + 我的解答)

📖 本课补充 · 非教材原文

(暂无。你问的问题和我的解答会按 Q/A 整理进这里。)

该带走的 6 句话

📖 本课补充 · 非教材原文
  1. 文件是存在外存上的数据集合,C 把它看成一串字节(流);FILE * 指向内存里的文件信息区,不是磁盘文件本身。
  2. fopen 要给三样:文件名、打开方式、接它的指针;返回 NULL 就必须 exit,这一步不能省。
  3. 打开方式记三句:r 要求文件已存在,w 会清空重建,a 从末尾追加;带 b 只是不做换行转换。
  4. 文本方式做换行转换、适合给人看;二进制方式原样搬运、省空间速度快,结构体用 fwrite/fread 配 wb/rb。
  5. feof 只在"读过文件尾之后"才为真,要先读再判断;rewind 回开头并清 feof,fseek 按 SEEK_SET/SEEK_CUR/SEEK_END 定位,ftell 报当前位置。
  6. fclose 不是礼貌动作:它负责把缓冲区里没写盘的数据真正写出去,不关就可能丢数据。

📚 教材覆盖清单(第10章,对文件的输入输出)

这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。

教材节号标题书页网站对应状态
10.1C文件的有关基本知识p.331–33310.1 C 文件的有关基本知识精讲
10.1.1什么是文件p.33110.1.1 什么是文件精讲
10.1.2文件名p.33210.1.2 文件名精讲
10.1.3文件的分类p.33210.1.3 文件的分类精讲
10.1.4文件缓冲区p.33310.1.4 文件缓冲区精讲
10.1.5文件类型指针p.33310.1.5 文件类型指针精讲
10.2打开与关闭文件p.334–33710.2 打开与关闭文件精讲
10.2.1用fopen函数打开数据文件p.335–33610.2.1 用 fopen 函数打开数据文件精讲
10.2.2用fclose函数关闭数据文件p.33710.2.2 用 fclose 函数关闭数据文件精讲
10.3顺序读写数据文件p.338–34910.3 顺序读写数据文件精讲
10.3.1怎样向文件读写字符p.338–34010.3.1 怎样向文件读写字符精讲
10.3.2怎样向文件读写一个字符串p.341–34410.3.2 怎样向文件读写一个字符串精讲
10.3.3用格式化的方式读写文本文件p.34510.3.3 用格式化的方式读写文本文件精讲
10.3.4用二进制方式向文件读写一组数据p.345–34910.3.4 用二进制方式向文件读写一组数据精讲
10.4随机读写数据文件p.350–35410.4 随机读写数据文件精讲
10.4.1文件位置标记及其定位p.350–35210.4.1 文件位置标记及其定位精讲
10.4.2随机读写p.353–35410.4.2 随机读写精讲
10.5文件读写的出错检测p.35510.5 文件读写的出错检测精讲
习题第10章书末习题p.355–356本课「书上习题精讲」逐题精讲(第1–11题)
插图本课范围内教材插图p.331–355用文字/ASCII 重画未逐张复刻
附录附录 A–Ep.357–370全书对照页未单独成课
  • 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
  • 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
  • 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。