教材《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 文件的有关基本知识
10.1.1 什么是文件
一句话:文件(file)一般指存储在外部介质上的数据的集合。
拆名字:file,英文"文件、档案"。操作系统以"文件"为单位管理外存数据。
| 种类 | 内容 | 例子 |
|---|---|---|
| 程序文件 | 程序代码 | 源程序 .c、目标文件 .obj、可执行文件 .exe |
| 数据文件 | 供程序运行时读写的数据 | 学生成绩、货物交易记录 |
本章讨论数据文件。屏幕留不住结果,所以要输出到磁盘、需要时再读回。操作系统把设备也当作文件:键盘是输入文件,显示器、打印机是输出文件。
输入输出是数据传送的过程,数据像流水一样从一处流向另一处,所以常称为流(stream),即数据流。 输入时数据从文件流向内存,输出时从内存流向文件。
C 把文件看作一个字符(或字节)的序列,叫流式文件:① 由一连串字节组成,不考虑行的界限;② 两行之间不会自动加分隔符;③ 存取以字节为单位;④ 开始和结束只受程序控制,不受回车换行这类物理符号控制。
考点程序文件 vs 数据文件;"流"的含义;C 的文件是流式文件、按字节处理。
易错以为文件是"一行一行"存的。不是——记事本里的换行只是文件里存了一个 '\n'(Windows 文本文件里是 '\r' '\n' 两个字节),行要你自己用 fgets 或判断 '\n' 切出来。
10.1.2 文件名
一句话:文件标识 = 文件路径 + 文件名主干 + 文件后缀。如 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 文件的分类
一句话:按数据组织形式,数据文件分 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 文件缓冲区
一句话:系统在内存里给每个正在使用的文件开辟一块文件缓冲区,数据先攒着,攒够一批再和磁盘交换。
磁盘慢、内存快,每写一字节就真写一次盘太亏。ANSI C 采用"缓冲文件系统":输出时数据先送到缓冲区,装满后才一起送到磁盘;输入时一次读一批把缓冲区充满,程序再从缓冲区逐个取。每个文件在内存中只有一个缓冲区,读写共用。
比喻:缓冲区是仓库门口的小货车——搬一个小箱子就往仓库跑太亏,先堆到车上,装满再开进去。
考点缓冲文件系统的作用;缓冲区满才写盘;fclose 会把缓冲区数据写出去。
易错不关文件就结束程序,缓冲区里没装满的数据可能丢失。有的系统会自动冲刷,但别依赖——养成"用完就 fclose"的习惯。
10.1.5 文件类型指针
一句话:FILE 是 stdio.h 里声明好的结构体类型,存放一个文件的信息;FILE * 是指向它的指针,习惯叫文件指针。
每个被使用的文件都在内存中开辟一个文件信息区,存放文件名、状态、当前读写位置等,这些信息保存在一个 FILE 类型(typedef struct {...} FILE;)的结构体变量里。不同编译系统的 FILE 内容大同小异,成员不必深究。程序里一般不直接定义 FILE 变量,而是定义指向它的指针:
FILE *fp; // fp 是指向 FILE 类型数据的指针变量fopen 返回文件信息区的起始地址赋给 fp,fp 就与文件关联;有 n 个文件就设 n 个指针。
注意:指向文件的指针变量并不是指向外部介质上的数据文件的开头,而是指向内存中的文件信息区的开头。
系统自动打开三个标准流文件并定义三个指针:stdin(标准输入,键盘)、stdout(标准输出,显示器)、stderr(标准出错,显示器)。所以 printf、scanf 其实都在操作已经打开好的文件。
考点FILE 必须大写;FILE *fp; 的定义;文件指针指向内存中的文件信息区而非磁盘文件;三个标准流指针。
易错FILE 是类型名不是变量;FILE fp;(少 *)定义的是结构体变量,一般不用。别把"文件指针(FILE *)"和"文件位置标记(读到第几字节)"混为一谈。
10.2 打开与关闭文件
打开:为文件建立文件信息区和文件缓冲区,并让一个指针变量指向它;关闭:撤销信息区和缓冲区,使指针不再指向该文件。
10.2.1 用 fopen 函数打开数据文件
一句话:fopen(文件名, 使用文件方式) 按指定方式打开文件,成功返回指向该文件信息区的指针,失败返回 NULL。
拆名字:f = file(文件)+ open(打开)。返回的指针通常赋给 FILE *fp;,打开时通知系统三件事:文件名、使用方式、哪个指针指向它。
使用文件方式(表 10.1):
| 方式 | 含义 | 文件不存在时 |
|---|---|---|
r 只读 | 输入数据,打开已存在的文本文件 | 出错 |
w 只写 | 输出数据,打开一个文本文件 | 建立新文件 |
a 追加 | 向文本文件尾添加数据 | 出错 |
rb 只读 | 输入数据,打开二进制文件 | 出错 |
wb 只写 | 输出数据,打开二进制文件 | 建立新文件 |
ab 追加 | 向二进制文件尾添加数据 | 出错 |
r+ 读写 | 读写,打开文本文件 | 出错 |
w+ 读写 | 读写,建立新的文本文件 | 建立新文件 |
a+ 读写 | 读写,打开文本文件 | 出错 |
rb+ 读写 | 读写,打开二进制文件 | 出错 |
wb+ 读写 | 读写,建立新的二进制文件 | 建立新文件 |
ab+ 读写 | 读写,打开二进制文件 | 出错 |
教材说明里最要命的几条:
r:只能读;文件必须已存在且有数据,否则出错。w:只能写;不存在就新建,已存在则先删掉再重建——原内容被清空!a:向末尾追加,不删原数据,位置标记移到末尾;教材指出文件应已存在,否则出错。- 带
+:可读可写。r+要求文件存在;w+新建(或清空);a+位置在末尾,可添加也可读。 - 失败:返回空指针
NULL(stdio.h中定义为 0),原因可能是文件不存在、磁盘故障、磁盘已满等。 b只影响换行符处理:文本方式输出时把'\n'转成'\r' '\n',读入时又转回'\n';二进制方式不转换。wb不代表自动把 ASCII 转成二进制:fprintf/fscanf按 ASCII,fread/fwrite按二进制,b只是通知系统"不必转换回车符"。stdin、stdout、stderr程序启动时自动打开;有的系统方式名不同(如rw),注意所用系统的规定。
必须背的判空骨架:
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 函数关闭数据文件
一句话:fclose(文件指针) 关闭文件,成功返回 0,失败返回 EOF(-1)。
拆名字:f = file + close(关闭)。关闭就是撤销文件信息区和缓冲区,使指针与文件"脱钩",除非重新打开。
为什么必须关? 写数据时先送到缓冲区,缓冲区充满才正式写盘;数据没充满时程序结束,缓冲区里的数据可能丢失。fclose 会先把缓冲区数据输出到磁盘,再撤销文件信息区。
考点返回值(成功 0,失败 EOF);关闭的两个作用(冲刷缓冲区 + 撤销信息区)。
易错只 fopen 不 fclose;以为关闭只是礼貌动作。
10.3 顺序读写数据文件
顺序写先写的在前、后写的在后;顺序读先读前面的、后读后面的——读写顺序与数据在文件中的物理顺序一致。
10.3.1 怎样向文件读写字符
| 函数 | 调用 | 功能 | 返回值 |
|---|---|---|---|
fgetc | fgetc(fp) | 从 fp 指向的文件读入一个字符 | 成功带回所读字符;失败返回 EOF(-1) |
fputc | fputc(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 怎样向文件读写一个字符串
fgets(str, n, fp) 从文件读入长度 n-1 的字符串,最后加 '\0',共 n 个字符存到 str。
| 函数 | 调用 | 功能 | 返回值 |
|---|---|---|---|
fgets | fgets(str, n, fp) | 从文件读入长度 n-1 的字符串,存到 str | 成功返回地址 str;失败返回 NULL |
fputs | fputs(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 用格式化的方式读写文本文件
一句话:fprintf/fscanf 就是带第一个"文件指针"参数的 printf/scanf。f = file,print = 打印,f = formatted 格式化;scan = 扫描。作用相仿,唯一不同是读写对象不是终端而是文件。
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 用二进制方式向文件读写一组数据
一句话:fread/fwrite 按数据块读写,以二进制形式、原样搬运。f = file,read/write = 读/写。
fread(buffer, size, count, fp);
fwrite(buffer, size, count, fp);| 参数 | 含义 |
|---|---|
buffer | 一个地址。fread 是存放读入数据的存储区地址;fwrite 是要输出数据的存储区起始地址 |
size | 要读写的字节数(每个数据项长度) |
count | 要读写多少个数据项(每项长 size) |
fp | FILE 类型指针 |
例:fread(f, 4, 10, fp); 中 f 是 float 型数组名,从文件读入 10 个 4 字节数据存到 f。对结构体数组:
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 随机读写数据文件
顺序读写效率有时很低:文件有 1000 个数据,只查第 1000 个,也得先读前 999 个。随机访问可以直接访问任何位置的数据。
10.4.1 文件位置标记及其定位
一句话:文件位置标记指示"接下来要读写的下一个字符的位置"。
- 顺序读时,标记从文件开头逐字节后移;顺序写时,每写完一个数据也后移;
- 人为把标记移到任意位置再读写,就是随机读写——读写顺序与数据在文件中的物理顺序一般不一致。
教材特意区分:有的书叫它"文件位置指针",但它和 C 的指针不是一回事——文件在外存上,没有内存地址。本书统一叫"文件位置标记",别和
FILE *混。
(1)rewind——回到文件开头
rewind(fp);使位置标记重新返回文件开头,没有返回值,同时把 feof 的值恢复为 0(假)。
(2)fseek——改变位置标记
fseek(文件类型指针, 位移量, 起始点);起始点用 0、1、2 代替(表 10.4):
| 起始点 | 名字 | 数字 |
|---|---|---|
| 文件开始位置 | SEEK_SET | 0 |
| 文件当前位置 | SEEK_CUR | 1 |
| 文件末尾位置 | SEEK_END | 2 |
"位移量"是以起始点为基点向前移动的字节数,应是 long 型(数字末尾加 L)。fseek 一般用于二进制文件:fseek(fp, 100L, 0) 移到离开头 100 字节处;fseek(fp, -10L, 2) 从末尾后退 10 字节。
(3)ftell——报告当前位置
得到文件位置标记的当前位置,用相对文件开头的位移量表示。调用出错(如 fp 指向的文件不存在)返回 -1L:
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 随机读写
有了 rewind 和 fseek 就能随机读写。教材例 10.6:文件里有 10 个学生数据,要求读第 1、3、5、7、9 个并显示。做法:以 rb 打开 → 读第 1 个 → 用 fseek 移到第 3、5、7、9 个记录开头逐个读 → 关闭。关键一行:
fseek(fp, i * sizeof(struct Student_type), 0); // i 取 0,2,4,6,8i 是下标,乘一条记录的字节数,正好跳到第 i+1 条记录开头。
考点随机读写的关键是 fseek 的偏移量算对:第 i 条记录(从 0 数)的偏移 = i * 每条记录的字节数。
易错偏移量忘了乘 sizeof;或 sizeof 用的类型和文件里实际写的类型不一致,跳过去就错位了。
10.5 文件读写的出错检测
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)
本章书末共 11 题:第 1、2 题概念题,第 3~11 题编程题。完整原文、思路和可编译代码见第四节,一题不漏。
三、现场写代码
例 10.1 键盘输入字符,逐个存盘,直到输入 '#'
思路:getchar 逐个读键盘字符,fputc 逐个写到文件。
#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 码)。
运行结果:
请输入所用的文件名:file1.dat
请输入一个准备存储到磁盘的字符串(以#结束):computer and c#
computer and c补充:教材这里用 char ch 且只判断 '#'。如果用户按 Ctrl+Z(EOF),ch 会等于 -1,永远不等于 '#',程序就死循环了。更稳的写法是 int ch; 且 while ((ch = getchar()) != '#' && ch != EOF)。
例 10.2 把一个文件复制到另一个文件
思路:从 file1.dat 逐个读字符,逐个写到 file2.dat。
#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 行:两个文件都关闭。
运行结果:
输入读入文件的名字:file1.dat
输入输出文件的名字:file2.dat
computer and c要点:while 条件也可写成 while (ch != EOF),因为 EOF 就是 -1。用 feof 是为让你理解文件尾标志。
例 10.3 输入若干字符串,排序后存入文件
思路:读入 n 个字符串到二维数组 → 按字母顺序选择法排序 → 逐个写入文件(每个后面补 \n)。
教材用
gets。gets在 C11 已被删除,VS2022 会报错,下面改用fgets并去掉行末换行,思路不变。
#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 行:屏幕也显示一遍。
运行结果:
Enter strings:
CHINA
CANADA
INDIA
The new sequence:
CANADA
CHINA
INDIA例 10.4 用二进制方式存 10 个学生的数据
思路:定义含姓名、学号、年龄、地址的结构体数组;输入后用 fwrite 一次写一个学生。
#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()写盘。
运行结果(节选):
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 让位置标记回到开头
思路:第一次显示完,位置标记已在末尾、feof 为真;必须 rewind 才能第二次从头复制。
#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 个学生
思路:读例 10.4 生成的 stu.dat;fseek(fp, i * sizeof(struct Student_type), 0) 直接跳到第 i 条记录;fread 读一条并显示;i 每次加 2。
#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],按格式输出。
运行结果:
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,直接算出来跳过去——这就是随机读写的效率优势。
四、书上习题精讲
第 1 题(概念)
题目:什么是文件型指针?通过文件指针访问文件有什么好处?
答案:FILE 是 stdio.h 中声明的结构体类型,存放文件的名字、状态、当前读写位置等信息。文件型指针是指向 FILE 类型变量的指针,定义形式 FILE *fp;。打开文件时系统在内存建立文件信息区(一个 FILE 变量),fopen 返回其起始地址赋给 fp,fp 就与文件建立联系。
好处:① 一个指针代表整个文件,读写函数只传一个参数,方便;② 不必关心 FILE 内部成员,不同编译系统的 FILE 可以不同而用法不变,可移植;③ 多个文件各用一个指针,互不干扰;④ 通过指针还能访问和修改文件状态(位置标记、出错标志)。
第 2 题(概念)
题目:对文件的打开与关闭的含义是什么?为什么要打开和关闭文件?
答案:打开是为文件建立文件信息区和文件缓冲区,并把一个文件指针与文件联系起来,此后可通过该指针读写;关闭是撤销信息区和缓冲区,使指针不再指向该文件,此后不能再用它读写。
为什么要打开:读写前必须让系统知道操作哪个文件、用什么方式,并建立缓冲机制,否则无法高效读写。
为什么要关闭:① 关闭时系统会把缓冲区中尚未写盘的数据输出到磁盘,防止丢失;② 释放信息区和缓冲区占用的内存;③ 防止文件被误用。
第 3 题(编程)
题目:从键盘输入一个字符串,将其中的小写字母全部转换成大写字母,然后输出到一个磁盘文件 test 中保存,输入的字符串以"!"结束。
思路:getchar 循环读,读到结束符停;是小写字母就减 32;fputc 写入 test。书上写的是中文"!",键盘上按英文半角 !。
#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 题(编程)
题目:有两个磁盘文件 A 和 B,各存放一行字母,今要求把这两个文件中的信息合并(按字母顺序排列),输出到一个新文件 C 中去。
思路:从 A、B 各读一行;去掉行末换行;拼接到 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 题(编程)
题目:有 5 个学生,每个学生有 3 门课程的成绩,从键盘输入学生数据(包括学号、姓名、3 门课程成绩),计算出平均成绩,将原有数据和计算出的平均分数存放在磁盘文件 stud 中。
思路:结构体含学号、姓名、3 门成绩、平均分;循环输入 5 人并算平均;fwrite 二进制写入 stud。
#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 题(编程)
题目:将第 5 题 stud 文件中的学生数据,按平均分进行排序处理,将已排序的学生数据存入一个新文件 stu_sort 中。
思路:从 stud 读 5 条到数组;按 avg 从高到低选择法排序;写入 stu_sort。
#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 题(编程)
题目:将第 6 题已排序的学生成绩文件进行插入处理。插入一个学生的 3 门课程成绩,程序先计算新插入学生的平均成绩,然后将它按成绩高低顺序插入,插入后建立一个新文件。
思路:把 stu_sort 的 5 条读进长度 6 的数组;输入新学生并算平均;找第一个比它低的位置 pos;从后往前后移;插入;写新文件。
#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 题(编程)
题目:将第 7 题结果仍存入原有的 stu_sort 文件而不另建立新文件。
思路:与第 7 题相同,只是最后写回 stu_sort。因为数据已全部读进内存,先 fclose 读完的文件,再以 "wb" 重新打开(会清空),把 6 条写回即可——先读后写不会丢数据。文件很大放不进内存时,正确做法是写临时文件再 remove + rename 替换。
#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 题(编程)
题目:有一磁盘文件 employee,内存放职工的数据。每个职工的数据包括职工姓名、职工号、性别、年龄、住址、工资、健康状况、文化程度。今要求将职工名、工资的信息单独抽出来另建一个简明的职工工资文件。
思路:定义完整记录 Employee 和只有姓名、工资的 Salary;从 employee 逐条读,复制两个字段,写入 salary。
#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 题(编程)
题目:从第 9 题的"职工工资文件"中删去一个职工的数据,再存回原文件。
思路:把 salary 全部读进数组(记录数未知,用 fread 返回值控制);输入要删的姓名;以 "wb" 重新打开同一文件(清空);把姓名不等于目标的记录写回。
#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 题(编程)
题目:从键盘输入若干行字符(每行长度不等),输入后把它们存储到一磁盘文件。再从该文件中读入这些数据,将其小写字母转换成大写字母后在显示屏上输出。
思路:fgets 一行一行读键盘(能处理长度不等的行),空行结束;写入文件;再 fgets 逐行读回,逐字符小写转大写并输出。
#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:
#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:
#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:
#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:
打开 data.txt(只读)
sum = 0
for i = 1 to 10
读一个整数 x
sum = sum + x
关闭文件
输出 sum变式 4(补全):
#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 可能不存在。找出错误并改正。
// ❌ 错误示例(故意)
#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:下面程序为什么可能死循环?
// ❌ 错误示例(故意)
#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,为什么总打不开?
// ❌ 错误示例(故意)
#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 句话
- 文件是存在外存上的数据集合,C 把它看成一串字节(流);
FILE *指向内存里的文件信息区,不是磁盘文件本身。 fopen要给三样:文件名、打开方式、接它的指针;返回NULL就必须exit,这一步不能省。- 打开方式记三句:
r要求文件已存在,w会清空重建,a从末尾追加;带b只是不做换行转换。 - 文本方式做换行转换、适合给人看;二进制方式原样搬运、省空间速度快,结构体用
fwrite/fread配wb/rb。 feof只在"读过文件尾之后"才为真,要先读再判断;rewind回开头并清feof,fseek按SEEK_SET/SEEK_CUR/SEEK_END定位,ftell报当前位置。fclose不是礼貌动作:它负责把缓冲区里没写盘的数据真正写出去,不关就可能丢数据。
📚 教材覆盖清单(第10章,对文件的输入输出)
这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。
| 教材节号 | 标题 | 书页 | 网站对应 | 状态 |
|---|---|---|---|---|
| 10.1 | C文件的有关基本知识 | p.331–333 | 10.1 C 文件的有关基本知识 | 精讲 |
| 10.1.1 | 什么是文件 | p.331 | 10.1.1 什么是文件 | 精讲 |
| 10.1.2 | 文件名 | p.332 | 10.1.2 文件名 | 精讲 |
| 10.1.3 | 文件的分类 | p.332 | 10.1.3 文件的分类 | 精讲 |
| 10.1.4 | 文件缓冲区 | p.333 | 10.1.4 文件缓冲区 | 精讲 |
| 10.1.5 | 文件类型指针 | p.333 | 10.1.5 文件类型指针 | 精讲 |
| 10.2 | 打开与关闭文件 | p.334–337 | 10.2 打开与关闭文件 | 精讲 |
| 10.2.1 | 用fopen函数打开数据文件 | p.335–336 | 10.2.1 用 fopen 函数打开数据文件 | 精讲 |
| 10.2.2 | 用fclose函数关闭数据文件 | p.337 | 10.2.2 用 fclose 函数关闭数据文件 | 精讲 |
| 10.3 | 顺序读写数据文件 | p.338–349 | 10.3 顺序读写数据文件 | 精讲 |
| 10.3.1 | 怎样向文件读写字符 | p.338–340 | 10.3.1 怎样向文件读写字符 | 精讲 |
| 10.3.2 | 怎样向文件读写一个字符串 | p.341–344 | 10.3.2 怎样向文件读写一个字符串 | 精讲 |
| 10.3.3 | 用格式化的方式读写文本文件 | p.345 | 10.3.3 用格式化的方式读写文本文件 | 精讲 |
| 10.3.4 | 用二进制方式向文件读写一组数据 | p.345–349 | 10.3.4 用二进制方式向文件读写一组数据 | 精讲 |
| 10.4 | 随机读写数据文件 | p.350–354 | 10.4 随机读写数据文件 | 精讲 |
| 10.4.1 | 文件位置标记及其定位 | p.350–352 | 10.4.1 文件位置标记及其定位 | 精讲 |
| 10.4.2 | 随机读写 | p.353–354 | 10.4.2 随机读写 | 精讲 |
| 10.5 | 文件读写的出错检测 | p.355 | 10.5 文件读写的出错检测 | 精讲 |
| 习题 | 第10章书末习题 | p.355–356 | 本课「书上习题精讲」 | 逐题精讲(第1–11题) |
| 插图 | 本课范围内教材插图 | p.331–355 | 用文字/ASCII 重画 | 未逐张复刻 |
| 附录 | 附录 A–E | p.357–370 | 全书对照页 | 未单独成课 |
- 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
- 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
- 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。