1. 本章概述文件是程序与外部存储设备交换数据的桥梁。前面章节中程序的数据都保存在内存里一旦程序结束数据随之消失而文件可以把数据持久化到磁盘上供下次运行或其它程序使用。《C程序设计第五版》第 10 章围绕「文件的输入输出」展开核心内容包括文件的基本概念与分类文本文件 / 二进制文件文件类型指针FILE与文件操作的一般流程文件的打开与关闭fopen、fclose常用的读写函数fgetc/fputc、fgets/fputs、fscanf/fprintf、fread/fwrite文件的随机定位fseek、ftell、rewind文件读写出错检测ferror、feof、clearerr。本章是 C 语言从「内存数据处理」走向「实际应用开发」的关键一步无论读写配置文件、处理日志、存储结构化数据都离不开文件操作。2. 知识要点2.1 文件的概念与分类C 语言把文件看作一个字节流byte stream数据的读写不依赖具体物理设备统一通过「文件指针」操作。从存储编码角度看文件分为两类文本文件ASCII 文件每个字节存放一个字符的 ASCII 码内容可直接用文本编辑器查看。例如数字123在文本文件中占 3 个字节分别存放字符1、2、3的 ASCII 码。二进制文件按数据在内存中的存储形式原样输出到磁盘通常占用空间更小、读写更快。例如int型整数123在二进制文件中占 4 个字节与机器sizeof(int)有关。2.2 文件类型指针C 语言通过FILE结构体保存文件的有关信息文件状态、缓冲区位置、当前读写位置等。FILE类型在stdio.h中定义程序中通过指向该类型的指针来操作文件FILE*fp;fp是一个指向某个文件的指针后续所有读写操作都围绕它展开。一个程序可以同时打开多个文件每个文件对应一个独立的FILE指针。2.3 打开与关闭文件对文件进行操作前必须先「打开」使用完毕必须「关闭」这是文件操作最基本也是最重要的规范。打开文件fopenFILE*fopen(constchar*filename,constchar*mode);常见打开方式如下打开方式含义r以只读方式打开一个已存在的文本文件w以只写方式打开文本文件若文件不存在则创建若存在则清空原内容a以追加方式打开文本文件若文件不存在则创建写入的数据追加到末尾rb/wb/ab与上述对应但针对二进制文件r/w/a以读写方式打开表示可读可写rb/wb/ab二进制读写方式fopen成功时返回文件指针失败如用r打开不存在的文件时返回NULL。因此打开文件后必须进行判空处理FILE*fpfopen(data.txt,r);if(fpNULL){printf(文件打开失败\n);return1;}关闭文件fcloseintfclose(FILE*fp);关闭成功返回0失败返回EOF。关闭操作会刷新缓冲区、断开文件指针与文件的联系。不及时关闭文件会造成数据丢失、资源泄漏因此养成「用完即关」的习惯非常重要。2.4 顺序读写函数2.4.1 字符读写fgetc与fputcintfgetc(FILE*fp);/* 从文件读取一个字符 */intfputc(intch,FILE*fp);/* 向文件写入一个字符 */fgetc读到文件末尾或出错时返回EOF。常配合while循环逐字符处理文件intch;while((chfgetc(fp))!EOF){fputc(ch,stdout);}注意fgetc返回的是int而不是char。这是因为EOF通常定义为-1用char接收可能无法正确区分「文件结束」与合法的字符。2.4.2 字符串读写fgets与fputschar*fgets(char*str,intn,FILE*fp);intfputs(constchar*str,FILE*fp);fgets从文件中读取最多n-1个字符存入str读到换行符、文件末尾或达到n-1个字符时停止并自动在末尾补\0。fgets会保留读到的换行符如果读到了这个细节在字符串处理时需留意。fputs向文件写入字符串不自动追加换行符。2.4.3 格式化读写fprintf与fscanfintfprintf(FILE*fp,constchar*format,...);intfscanf(FILE*fp,constchar*format,...);fprintf/fscanf与printf/scanf用法几乎相同只是多了一个文件指针参数指定读写目标是文件而非终端。例如把变量写入文件fprintf(fp,%d %f %s\n,a,b,str);注意fscanf读到文件末尾返回EOF即-1而不是0。判断读取结束时常用while (fscanf(fp, %d, x) ! EOF)或while (fscanf(fp, %d, x) 1)。2.4.4 数据块读写fread与fwrite字符、字符串、格式化读写适合文本数据对于结构体、数组等成块数据用fread/fwrite更高效、更贴合二进制文件场景size_tfread(void*ptr,size_tsize,size_tcount,FILE*fp);size_tfwrite(constvoid*ptr,size_tsize,size_tcount,FILE*fp);ptr内存中数据存放/来源的地址size每个数据块的大小字节count要读写的数据块个数两个函数都返回实际成功读写的数据块个数。例如把一个结构体数组整体写入文件Student stus[3]{/* 略 */};fwrite(stus,sizeof(Student),3,fp);读出时通常配合fread的返回值判断实际读到的元素个数。2.5 文件的随机定位前面的读写都是「顺序读写」即从文件当前位置依次向后。通过定位函数可以随机访问文件中的任意位置。rewind使文件位置指针回到文件开头。voidrewind(FILE*fp);fseek将位置指针移动到指定位置。intfseek(FILE*fp,longoffset,intorigin);offset相对origin的偏移量字节数可正可负正表示向后移动origin基准位置可取三个宏SEEK_SET文件开头SEEK_CUR当前位置SEEK_END文件末尾。例如把位置指针从文件头向后移动 10 个字节fseek(fp,10L,SEEK_SET);ftell返回文件当前位置指针相对文件开头的偏移量字节数常用于获取文件长度longftell(FILE*fp);2.6 出错检测文件读写过程中应能区分「正常结束」「读写出错」等情况feof(fp)若最近一次读操作因到达文件末尾而失败返回真ferror(fp)若文件操作发生错误返回真clearerr(fp)清除文件结束标志和出错标志。一个常见的坑是仅用while (!feof(fp))控制循环可能因为「先检测后读」的时序问题多处理一次建议养成「先读、再判断返回值」的写法。3. 案例分析3.1 案例一从键盘读入字符并写入文件需求从键盘输入若干字符以#结束把所有字符写入output.txt随后把文件内容读回并显示在屏幕上。#includestdio.hintmain(void){FILE*fp;charch;/* 1. 以只写方式打开或创建文件 */fpfopen(output.txt,w);if(fpNULL){printf(打开文件失败\n);return1;}printf(请输入字符以 # 结束\n);chgetchar();while(ch!#){fputc(ch,fp);/* 写入文件 */chgetchar();}fclose(fp);/* 写完关闭 *//* 2. 以只读方式重新打开读回并显示 */fpfopen(output.txt,r);if(fpNULL){printf(打开文件失败\n);return1;}while((chfgetc(fp))!EOF){putchar(ch);/* 逐个输出到屏幕 */}fclose(fp);return0;}分析本案例演示了fopen的两种模式、fputc/fgetc的基本用法以及「关闭后再以另一种模式打开」的常见流程。需要注意的是getchar读取的回车符也会被写入文件。3.2 案例二文件复制需求把source.txt的全部内容复制到target.txt并在屏幕上输出复制出的字符总数。#includestdio.hintmain(void){FILE*in,*out;charch;longcount0;infopen(source.txt,r);if(inNULL){printf(源文件打开失败\n);return1;}outfopen(target.txt,w);if(outNULL){printf(目标文件打开失败\n);fclose(in);return1;}while((chfgetc(in))!EOF){fputc(ch,out);count;}fclose(in);fclose(out);printf(共复制 %ld 个字符。\n,count);return0;}分析这个案例是字符级文件复制的标准写法同时也展示了「打开多个文件时必须分别判空、失败时把已打开的文件关闭」的资源管理意识。若复制二进制文件只需把r/w换成rb/wb。3.3 案例三用fread/fwrite读写学生信息需求定义学生结构体将 3 名学生的信息写入二进制文件stu.dat再读回并显示。#includestdio.htypedefstruct{intid;charname[20];floatscore;}Student;intmain(void){FILE*fp;Student stus[3]{{1,张三,89.5f},{2,李四,92.0f},{3,王五,76.5f}};Student readback[3];inti;/* 写入二进制文件 */fpfopen(stu.dat,wb);if(fpNULL){printf(文件打开失败\n);return1;}fwrite(stus,sizeof(Student),3,fp);fclose(fp);/* 读回 */fpfopen(stu.dat,rb);if(fpNULL){printf(文件打开失败\n);return1;}fread(readback,sizeof(Student),3,fp);fclose(fp);/* 显示 */for(i0;i3;i){printf(学号%d姓名%s成绩%.1f\n,readback[i].id,readback[i].name,readback[i].score);}return0;}分析fwrite一次写一个结构体大小sizeof(Student)共写 3 个数据块fread同理。用二进制方式读写成块数据既简洁又能保持字段值的精确性适合存储固定格式的记录。实际应用中应关注fread的返回值以免在文件记录不足时越界访问数组。4. 课后习题解析说明以下选取第 10 章典型题型进行解析重点讲清解题思路和易错点代码可作为参考模板迁移到同类题目。4.1 统计文本文件中的各类字符题目类型从文本文件读取内容分别统计其中的大写字母、小写字母、数字、空格及其它字符的个数。解题思路逐字符读取文件利用字符分类条件或ctype.h中的isupper、islower、isdigit累计各类别计数最后输出统计结果。#includestdio.h#includectype.hintmain(void){FILE*fp;intch;intupper0,lower0,digit0,space0,other0;fpfopen(text.txt,r);if(fpNULL){printf(文件打开失败\n);return1;}while((chfgetc(fp))!EOF){if(isupper(ch))upper;elseif(islower(ch))lower;elseif(isdigit(ch))digit;elseif(isspace(ch))space;elseother;}fclose(fp);printf(大写字母%d\n,upper);printf(小写字母%d\n,lower);printf(数字%d\n,digit);printf(空白字符%d\n,space);printf(其它%d\n,other);return0;}易错点fgetc返回值要用int接收否则EOF判断可能出错不要忘记关闭文件若文件不存在用r打开必须判空否则后续读写会导致程序崩溃。4.2 由键盘输入文件名实现复制题目类型从键盘输入源文件名和目标文件名完成文件的复制并处理「源文件不存在」等异常情况。解题思路用scanf/fgets获取两个文件名依次打开源文件只读和目标文件只写逐字符复制。若源文件打开失败应给出提示并结束不要继续打开目标文件。#includestdio.hintmain(void){FILE*in,*out;charsource[100],target[100];intch;printf(请输入源文件名);scanf(%s,source);printf(请输入目标文件名);scanf(%s,target);infopen(source,r);if(inNULL){printf(无法打开源文件 %s\n,source);return1;}outfopen(target,w);if(outNULL){printf(无法创建目标文件 %s\n,target);fclose(in);return1;}while((chfgetc(in))!EOF){fputc(ch,out);}fclose(in);fclose(out);printf(复制完成。\n);return0;}易错点目标文件打开失败时必须把已经打开的源文件fclose避免资源泄漏。4.3 将学生成绩排序后写入文件题目类型从键盘输入若干学生的成绩按成绩从高到低排序把排序结果写入文件并验证读回。解题思路使用结构体数组存储学生信息用选择排序或冒泡排序对数组排序排序后以文本或二进制形式写入文件。为便于查看这里用fprintf写入文本文件。#includestdio.h#defineN5typedefstruct{charname[20];floatscore;}Student;intmain(void){FILE*fp;Student stus[N];Student tmp;inti,j,max;/* 输入 */for(i0;iN;i){printf(请输入第 %d 个学生的姓名和成绩,i1);scanf(%s %f,stus[i].name,stus[i].score);}/* 选择排序按成绩从高到低 */for(i0;iN-1;i){maxi;for(ji1;jN;j){if(stus[j].scorestus[max].score){maxj;}}if(max!i){tmpstus[i];stus[i]stus[max];stus[max]tmp;}}/* 写入文件 */fpfopen(rank.txt,w);if(fpNULL){printf(文件打开失败\n);return1;}for(i0;iN;i){fprintf(fp,%s %.1f\n,stus[i].name,stus[i].score);}fclose(fp);printf(成绩已排序并写入 rank.txt\n);return0;}易错点比较成绩时应使用浮点数值直接比较不要把成绩先转成整数再比较结构体整体交换要保证类型一致。4.4 用fseek与ftell获取文件长度题目类型编写程序利用文件定位函数求出一个文件占用的字节数。解题思路打开文件后把位置指针移到文件末尾再用ftell返回相对文件头的偏移量即为文件长度最后别忘了用rewind或再次fseek恢复指针位置若后续还要从头读写。#includestdio.hintmain(void){FILE*fp;longlen;fpfopen(data.txt,rb);if(fpNULL){printf(文件打开失败\n);return1;}fseek(fp,0L,SEEK_END);/* 移到文件末尾 */lenftell(fp);/* 得到文件长度 */rewind(fp);/* 移回文件开头 */printf(文件长度为 %ld 字节\n,len);fclose(fp);return0;}易错点以文本方式打开文件时不同平台对换行符的处理可能影响ftell结果的可移植性计算文件大小时一般用二进制方式rb打开fseek的偏移量应写成long类型如0L。5. 常见错误与调试建议打开文件不判空文件不存在、路径错误、权限不足都会使fopen返回NULL后续读写会因操作空指针而崩溃。每次打开后都要判断fp ! NULL。忘记fclose文件用完后未关闭缓冲区中的数据可能未写入磁盘造成数据丢失同时会占用系统资源。建议「打开即考虑关闭」。用char接收fgetc返回值EOF是-1放在char中会被截断导致循环无法正常结束。应使用int。fscanf返回值误判fscanf读取失败返回EOF而不是0判断读到几个有效数据项时看它返回的「成功赋值的项数」。fgets的换行符残留fgets会把换行符一并读入字符串末尾比较、拼接字符串时应先去除多余的\n。文本 / 二进制模式混用读写二进制数据时用rb/wb避免某些平台因换行符转换破坏数据。不要用文本模式打开二进制文件再去fread。6. 小结第 10 章的核心是「文件指针 打开/关闭 读写 定位 出错检测」这条主线了解文本文件与二进制文件的区别掌握fopen的常用模式和fclose的使用规范熟练使用fgetc/fputc、fgets/fputs、fprintf/fscanf处理文本数据掌握fread/fwrite进行成块数据读写会用fseek、ftell、rewind实现随机访问善用feof、ferror进行读写状态判断。文件操作是 C 语言进入实际项目开发的重要基础。掌握好本章内容不仅有助于应对课程考试更能为后续编写配置文件解析、数据持久化、简单数据库等程序打下扎实基础。动手把上面的案例和习题都跑一遍重点观察「判空、返回值、关闭文件」这三处最容易出错的地方理解会更深。