前面二十三篇文章我们写的所有程序都有一个共同特征数据只在运行时存在。变量在内存里程序一退出一切烟消云散。但真正的软件不是这样的。游戏要保存存档编辑器要读写文档数据库要把数据永久保存到磁盘。这就需要一个程序与外部存储世界沟通的桥梁——文件操作。C 语言通过标准库提供了一套简洁的文件操作函数核心思路是把文件看作一个字节流打开它、读/写它、关闭它。今天我们就来掌握这套“磁盘功夫”。一、文件指针FILE *操作文件的“手柄”在 C 语言里操作文件不是靠文件名而是靠一个文件指针——FILE *。你打开一个文件操作系统会返回一个不透明的指针之后所有的读写操作都通过这个指针进行。可以把FILE *理解为“遥控器”你用它来操控对应的文件不需要知道内部细节。#includestdio.hintmain(void){FILE*fp;// 声明文件指针fpfopen(test.txt,w);// 打开文件// ... 使用 fp ...fclose(fp);// 关闭文件return0;}二、打开文件fopenFILE*fopen(constchar*filename,constchar*mode);filename文件路径字符串。mode打开模式字符串决定读还是写、是否清空、是否追加等。返回值成功返回FILE*失败返回NULL。常用模式一览模式含义文件不存在时文件存在时r只读返回 NULL从头读w只写创建新文件清空内容从头写a追加写创建新文件从末尾追加不清空r读写返回 NULL从头读写w读写创建新文件清空内容a读追加创建新文件从末尾追加永远检查fopen的返回值如果文件打不开比如路径不存在、权限不够、磁盘满fopen返回NULL。不检查就直接用会导致空指针解引用程序崩溃。FILE*fpfopen(data.txt,r);if(fpNULL){perror(打开文件失败);// perror 打印系统错误信息return1;}perror是个很方便的函数它会根据全局错误码errno打印出具体的错误原因比如 “No such file or directory”。三、关闭文件fcloseintfclose(FILE*fp);将缓冲区中尚未写入磁盘的数据刷新到文件。释放系统资源文件描述符。返回 0 表示成功EOF通常是 -1表示失败。省略fclose在程序正常退出时操作系统通常会帮你关掉。但养成手动关闭的习惯非常必要——尤其是写操作不关可能导致数据丢失长期运行的程序不关会耗尽文件描述符。四、写文件fprintf和fputs1.fprintf—— 格式化写入fprintf和printf几乎一模一样只是多了一个FILE*参数指明写入到哪里。fprintf(fp,格式字符串,参数...);示例把学生信息写入文件#includestdio.hintmain(void){FILE*fpfopen(students.txt,w);if(fpNULL){perror(打开文件失败);return1;}fprintf(fp,%-20s %-10s %-6s\n,姓名,学号,成绩);fprintf(fp,%-20s %-10d %-6.1f\n,Alice,1001,92.5);fprintf(fp,%-20s %-10d %-6.1f\n,Bob,1002,85.0);fprintf(fp,%-20s %-10d %-6.1f\n,Carol,1003,78.5);fclose(fp);printf(数据已写入 students.txt\n);return0;}运行后打开students.txt你会看到格式整齐的表格。%-20s表示左对齐占 20 列和终端输出完全一样。2.fputs—— 写入整个字符串fputs(一行文字\n,fp);比fprintf(fp, %s, str)更简洁但不带格式化功能也不会自动加换行你需要手动\n。五、读文件fscanf和fgets1.fscanf—— 格式化读取和scanf类似从文件指针读取而非键盘fscanf(fp,格式字符串,地址...);读回刚才保存的学生数据#includestdio.hintmain(void){FILE*fpfopen(students.txt,r);if(fpNULL){perror(打开文件失败);return1;}charname[50];intid;floatscore;// 跳过标题行charheader[100];fgets(header,sizeof(header),fp);printf(读取的学生数据:\n);while(fscanf(fp,%s %d %f,name,id,score)3){printf(姓名: %s, 学号: %d, 成绩: %.1f\n,name,id,score);}fclose(fp);return0;}注意fscanf遇到空格、换行会停止读字符串。如果name包含空格会被截断——这时用fgets逐行读更可靠。判断读取是否结束fscanf的返回值是成功匹配并赋值的项数。读到文件末尾返回EOF通常是 -1匹配失败返回小于期望值的数。2.fgets—— 安全地读一行char*fgets(char*buffer,intsize,FILE*fp);buffer存放读取内容的字符数组。size最多读取size-1个字符留一位给\0。读到换行符会保留它读到文件末尾返回NULL。charline[256];while(fgets(line,sizeof(line),fp)!NULL){printf(%s,line);// line 里已包含换行符}fgets不会溢出是读取文本文件的推荐方式。六、文本文件 vs 二进制文件上面的例子都是文本文件——数据以人类可读的字符形式存储数字 92.5 被写成 ‘9’ ‘2’ ‘.’ ‘5’ 四个字符。文本文件优点是直接用编辑器打开查看缺点是有转换开销、精度可能丢失、文件较大。二进制文件则把内存中的位模式原样写入磁盘。int就写 4 字节double写 8 字节。优点更紧凑、读写更快、精度不丢失。缺点用编辑器打开是乱码。我们现在先只关注文本文件下一篇文章会系统讲fread/fwrite的二进制操作。现在你只需知道C 把文件都看作字节流文本和二进制只是解释方式不同。七、完整实战学生成绩文件的读写把结构体第二十一篇和文件操作结合起来做一个简单的成绩单保存/读取工具。#includestdio.h#includestdlib.h#includestring.h#defineMAX_NAME50#defineFILENAMEgrades.txttypedefstruct{charname[MAX_NAME];floatscore;}Student;voidsave_students(Student*students,intcount){FILE*fpfopen(FILENAME,w);if(fpNULL){perror(保存失败);return;}fprintf(fp,%d\n,count);// 第一行记录学生数量for(inti0;icount;i){fprintf(fp,%s %.1f\n,students[i].name,students[i].score);}fclose(fp);printf(已保存 %d 条记录到 %s\n,count,FILENAME);}intload_students(Student*students,intmax_count){FILE*fpfopen(FILENAME,r);if(fpNULL){perror(加载失败);return0;}intcount;fscanf(fp,%d,count);if(countmax_count)countmax_count;for(inti0;icount;i){fscanf(fp,%s %f,students[i].name,students[i].score);}fclose(fp);returncount;}intmain(void){Student students[100];intcount0;intchoice;while(1){printf(\n1.添加学生 2.显示全部 3.保存 4.加载 0.退出\n);printf(选择: );scanf(%d,choice);if(choice0)break;elseif(choice1){if(count100){printf(已满\n);continue;}printf(姓名 成绩: );scanf(%s %f,students[count].name,students[count].score);count;}elseif(choice2){printf(当前 %d 条记录:\n,count);for(inti0;icount;i){printf( %s: %.1f\n,students[i].name,students[i].score);}}elseif(choice3){save_students(students,count);}elseif(choice4){countload_students(students,100);printf(已加载 %d 条记录\n,count);}}return0;}这个程序综合了结构体、数组、循环、分支、文件读写。运行它添加几个学生保存后退出再重新运行加载文件数据就恢复了——你第一次实现了真正的“持久化”。八、常见错误与陷阱1. 忘记检查fopen的返回值FILE*fpfopen(missing.txt,r);fprintf(fp,hello);// fp 是 NULL崩溃任何文件操作前先判空。2. 用w打开已有文件内容被清空FILE*fpfopen(important.txt,w);// 旧内容瞬间消失如果只是想添加用a追加模式。3. 读写后忘记fclose尤其是在写操作后不关可能导致缓冲区里的数据还没写到磁盘造成文件内容不完整。4. 用fscanf读字符串不限制宽度charname[20];fscanf(fp,%s,name);// 文件里若有一行超长就溢出了应该用%19s限制宽度或者用fgets。5. 多次调用fgets/fscanf时没考虑上一行的换行符残留fscanf(fp,%d,n);// 读完数字换行符还在fgets(line,100,fp);// 立即读完那个换行符得到空行混合使用fscanf和fgets时可以在fscanf后用getchar()或fgetc(fp)吃掉换行符。或者统一用fgetssscanf解析。九、小结今天你让程序有了“记忆力”。核心知识FILE *是操作文件的手柄用fopen获取用fclose归还。r、w、a等模式控制了读、写、追加行为。fprintf/fputs写文件fscanf/fgets读文件。fgets是安全读行的首选混合输入时要小心换行符残留。结构体 文件操作 简单的数据持久化系统。文本文件足以应对配置、日志、简单数据存储的需求。但如果你想高效存储大量数值、实现随机读写、或者构建一个简易数据库文件就需要二进制文件与随机读写。下一篇我们就来学习fread、fwrite、fseek、ftell——打开文件操作的另一扇大门。课后小练习编写一个程序把 1 到 100 的整数逐行写入numbers.txt每行一个数。读取上一题生成的numbers.txt计算所有整数的总和并打印。实现一个简单的“日志记录器”每次运行程序让用户输入一行文字以追加模式写入log.txt并在每行前加上时间戳可以用time库的time()和ctime()获取当前时间字符串。陷阱题下面的代码有什么问题如何修正FILE*fpfopen(data.txt,w);if(fpNULL){printf(错误\n);}fprintf(fp,Hello);fclose(fp);我们下期见