简介面向零基础初学者的C语言文件读写入门文档重点解决初学者对文件打开、写入、读取、关闭流程不熟悉的问题。内容覆盖文件基本概念、标准操作流程详细讲解fopen、fprintf、fgets、fclose等常用函数的作用与调用方式并配有可直接上机运行的示例代码写操作使用fprintf写入文本并检查指针是否为NULL读操作使用while循环配合fgets逐行读取文件内容后打印到控制台读者可对照代码边看边练在本地编译运行验证结果较快形成文件读写操作的实操手感同时理解文件打开失败时的错误处理思路。压缩包内为单篇DOC格式文档共1个文件整体仅约14KB内容精炼、打开即读文档按“打开—读写—关闭”组织层次分明。目前已有103人学习下载适合课程作业、期末复习或零散时间自学使用。1. 这个标题到底在讲什么把文件当成一个“水管”来理解很多刚开始学C语言的朋友一看到fopen、fread、fseek这些函数就头皮发麻觉得文件读写是门玄学。其实说白了文件操作就是你跟硬盘上的数据打个交道要么把内存里的东西存进去要么把存好的东西读出来。整个过程可以类比成水管——打开阀门fopen让数据流过去fread/fwrite用完关上阀门fclose。就这么简单。这篇文章不会上来就甩一堆几十行的工程代码而是从最贴近初学者的角度把文件读写拆成「打开 → 读/写 → 关闭」三步走每一步都配上能直接跑通的最小示例。我会把fopen的几种模式、fread和fwrite的字节数到底怎么算、文本文件和二进制文件有什么区别这些「劝退点」一次讲透。适合谁看刚学完指针和结构体、正要往文件操作过渡的同学以及那些会写printf但一碰文件就翻车的自学者。读完你能独立写出一个带存取功能的小工具还能避开至少五个最常见的坑。2. 先把地基打牢fopen 的参数和那个「看不见的返回值」2.1 模式字符串不只是 r 和 w六个模式三个坑fopen是文件操作的起点它的原型就一句话FILE *fopen(const char *filename, const char *mode)。第一个参数是文件路径第二个参数是打开模式。很多初学者只记得r是读、w是写等你真正上手会发现模式选错是文件读写翻车的第一大原因。我见过有个同学用r去打开一个不存在的文件结果程序没崩溃但所有读取操作都静默失败他查了半天不知道问题出在哪。原因很简单r模式要求文件必须存在否则fopen返回空指针而w模式则会「无脑创建」——如果文件不存在就新建一个如果文件存在就直接清空。这两个特性在实际开发里比你想的更重要用w之前一定要想清楚这个文件里的旧数据你还留不留不留才能用w否则就用a追加或者r读写不清空。还有一个容易被忽略的参数是b它代表二进制模式。写代码的时候我建议你加上b写成rb、wb而不是r、w尤其是你要处理非文本文件图片、结构体数组的时候。在 Windows 下不加b时写入的\n会被自动转换成\r\n读出来的时候又转回去。这个自动转换看着挺贴心但它会让你用fseek定位文件位置时算不准偏移量做二进制读写时数据对不上。在 Linux 和 macOS 下b加不加都一样但养成加b的习惯能让你在跨平台时少踩一个坑。下面这张表是我随手整理的你把它存下来比死记硬背强得多模式文件不存在时文件存在时可读可写写位置r报错返回NULL正常打开是否从文件头w新建清空原内容否是从文件头a新建保留原内容否是只能在末尾追加r报错正常打开是是从文件头w新建清空原内容是是从文件头a新建保留原内容是是写只能在末尾2.2 判断打开是否成功这不是加分项是底线fopen返回的是FILE *类型——它是一个指向文件结构体的指针。如果打开失败它返回NULL。很多初学者在示例代码里见过但不当回事然后自己在写的时候就不写判断直接往下调用fprintf结果程序在某个莫名其妙的行崩溃了。这其实是「失败时看什么」里最基础的一条你永远应该先检查fopen的返回值。我一般会这么写FILE *fp fopen(data.txt, r); if (fp NULL) { perror(文件打开失败); return 1; }perror这个函数会把系统记录的错误原因打印出来比如「No such file or directory」或者「Permission denied」。比起你自己写一句「打开失败」perror能告诉你为什么失败这是排查问题时的第一手线索。养成这个习惯之后你会发现调试文件代码的时间至少省下一半。另外一个关于fopen的细节是文件路径既可以用相对路径比如data.txt表示当前程序所在目录也可以用绝对路径比如/tmp/data.txt。初学者最容易犯的错是把 Windows 的路径写法C:\Users\test\data.txt直接写成字符串结果因为反斜杠被当成转义符文件永远打不开。// 错误写法\t 会被当成转义字符 FILE *fp fopen(C:\Users\test\data.txt, r); // 正确写法一双反斜杠 FILE *fp fopen(C:\\Users\\test\\data.txt, r); // 正确写法二正斜杠也能被 Windows 识别 FILE *fp fopen(C:/Users/test/data.txt, r);这就是为什么我强调检查返回值——如果你检查了NULL你会立刻发现路径有问题如果你不检查程序可能从头到尾都没报错因为也许你碰巧用w模式成功创建了一个名字诡异的文件在错误的位置。到这里最基础的打开操作就说完了模式选对、返回值必查、路径别写错。这三件事你记住了文件读写的一半翻车现场就已经避开了。3. 读写三件套fprintf / fread / fgets 到底怎么选3.1 fprintf 和 fscanf像 printf 一样写文件fprintf是文件操作里最接近printf的函数它把格式化好的字符串写进文件。比如你要存一个学生的姓名和分数#include stdio.h int main(void) { FILE *fp fopen(score.txt, w); if (fp NULL) { perror(文件打开失败); return 1; } char name[] zhangsan; int score 92; fprintf(fp, %s %d\n, name, score); fclose(fp); return 0; }写完注释里说的逻辑你也得懂fprintf的第一个参数是文件指针后面的格式串和变量跟printf一模一样只是输出目的地从「屏幕」换成了「这个文件」。文件里存下来的内容是一行文本zhangsan 92用记事本打开就能看见这也是初学者最直观的成就感来源。那读出来怎么办用fscanf它的参数和fscanf类似但是要传变量的地址FILE *fp fopen(score.txt, r); if (fp NULL) { perror(文件打开失败); return 1; } char name[50]; int score; fscanf(fp, %s %d, name, score); printf(读到的数据%s%d 分\n, name, score); fclose(fp);注意两个细节%s对应的传参是name不是name因为数组名本身就是地址%d对应的传参是score这跟scanf的规则一模一样。如果fscanf的返回值不等于你要读的变量数量这里是 2那说明格式对不上——比如文件里只有一行zhangsanfscanf会读走字符串但读不到整数返回 1而score的值不被赋值程序继续跑就会用垃圾数据。每次读文件都要检查返回值这是从scanf时代就该养成的习惯在文件操作里它更重要。3.2 fread 和 fwrite新手觉得难其实参数个数是固定的fprintf适合存「人能看的文本」但如果你要存一组结构体、或者一个数组的原始字节fread和fwrite才是正解。这两个函数的原型是size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);下面我来拆解这两个参数。初学者不会背原型但需要理解ptr是数据在内存里的起始地址size是每一个元素的字节数nmemb是要读写多少个元素stream是文件指针。所以「写一个包含 10 个 int 的数组」就是int data[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; FILE *fp fopen(data.bin, wb); if (fp NULL) { perror(文件打开失败); return 1; } fwrite(data, sizeof(int), 10, fp); fclose(fp);读出来同样简单int buf[10]; FILE *fp fopen(data.bin, rb); if (fp NULL) { perror(文件打开失败); return 1; } // 返回值是成功读到的元素个数不是字节数 size_t count fread(buf, sizeof(int), 10, fp); printf(实际读到了 %zu 个元素\n, count); fclose(fp);参数说明为什么不直接写fwrite(data, 40, 1, fp)而写fwrite(data, sizeof(int), 10, fp)因为一次写 1 个 40 字节的元素和一次写 10 个 4 字节的元素在磁盘上的结果是一样的但后者在某些平台上对「部分写入」的处理更友好。fread的返回值是成功读到的元素个数不是字节数。当你读到文件末尾只读到一半时它返回的可能是 5 而不是 10所以count就告诉你文件是不是提前结束了。3.3 fgets 和 fputs逐行处理文本的朴素利器如果你要处理的文件是「一行一条数据」的文本比如一份用户名单fgets是最稳的选择。它每次读一行最多读n-1个字符留一个位置给字符串结尾的\0遇到换行停止char line[100]; FILE *fp fopen(names.txt, r); if (fp NULL) { perror(文件打开失败); return 1; } while (fgets(line, sizeof(line), fp) ! NULL) { // 注意line 里会包含末尾的换行符 \n printf(读到%s, line); } fclose(fp);为什么说它稳因为fgets指定了缓冲区大小sizeof(line)就绝不会发生缓冲区溢出——这是gets被废弃的根本原因也是我推荐你永远不要碰gets的原因。但fgets有个新手容易忽略的细节它会把换行符也读进字符串里。所以很多场景你需要手动去掉末尾的\nwhile (fgets(line, sizeof(line), fp) ! NULL) { line[strcspn(line, \n)] \0; // 把换行符替换成字符串结束符 printf(处理后的数据%s\n, line); }strcspn(line, \n)这个写法是标准做法它找到换行符在字符串里的下标然后把那个位置改成\0。如果这一行没有换行符比如文件最后一行没以换行结束strcspn返回的会是字符串长度替换后等于没动所以这个写法是安全的。这三个函数各管一摊文本数据用fprintf/fscanf二进制数据用fread/fwrite逐行读文本用fgets。下一章我们把这些拼起来写一个真正完整的记事本程序。4. 组装一个最小记事本把「写进去」和「读出来」打通4.1 需求很简单让用户输入三行内容存起来再原样读出来说了这么多是时候动手了。这个模拟项目不搞花架子就是一个小程序让用户敲三句话保存到文件里然后重新打开文件把三句话逐行读出来打印在屏幕上。它能完整覆盖「打开 → 写入 → 关闭 → 再打开 → 读取 → 关闭」这一个闭环你跑通了文件读写的基本功就过关了。下面是完整代码我分成两半来讲。先看「写入」一半#include stdio.h #include string.h #define MAX_LINE 3 #define LINE_BUF 256 int main(void) { FILE *fp; char line[LINE_BUF]; int i; // 阶段一写入 fp fopen(note.txt, w); if (fp NULL) { perror(无法创建 note.txt); return 1; } printf(请输入三句话每行一句\n); for (i 0; i MAX_LINE; i) { if (fgets(line, sizeof(line), stdin) NULL) { printf(输入被中断提前结束\n); break; } // 去掉末尾换行避免文件里出现空行 line[strcspn(line, \n)] \0; fprintf(fp, %s\n, line); } fclose(fp);这段代码里fgets从stdin读用户输入去掉换行后重新拼上\n写入文件——你可能觉得绕但这样做的意义在于无论用户输入是hello\n还是hello文件里的格式都是统一的hello\n后续读回来时解析逻辑只用写一遍。fclose(fp)在for循环外面是很重要的所有数据都写完了才关文件如果有数据没来得及落盘fclose会替你冲刷缓冲区。然后是「读取」一半// 阶段二读取 fp fopen(note.txt, r); if (fp NULL) { perror(无法打开 note.txt); return 1; } printf(\n文件里的内容是\n); while (fgets(line, sizeof(line), fp) ! NULL) { // 文件里每行末尾有 \n所以用 %s 打印不会串行 printf(%s, line); } fclose(fp); return 0; }两个阶段放在同一个main函数里用注释分隔。跑一遍这个程序你会在屏幕上看到自己刚输入的三句话原样返回而note.txt就是这三句话的持久化结果——重启电脑都还在。这就是文件读写的意义程序结束数据不丢。4.2 参数怎么调缓冲区大小和换行处理上面这段代码里有几个参数是值得你动手改改看的不是只抄完就完了第一个是LINE_BUF。我设了 256如果用户输入的一行超过 255 个字符fgets只会读走前 255 个字符剩下的留在输入缓冲区里下一次fgets会接走它们的后半截导致「一行被拆成两行存」。在真实场景里你应当对输入长度做防御要么用更大的缓冲区要么在检测到输入没以换行结束时把剩余字符读完丢弃。这是文件读写里最常见的「缓冲区不够」问题。第二个是fprintf(fp, %s\n, line)里的那个\n。如果我不用%s\n而直接fputs(line, fp)那line里的流式内容也会被原样写入但你得保证line里确实有换行符。两种写法的分歧点在于你保存的数据是「每行固定一条」还是「原文照搬」。记事本场景一般希望固定格式所以我选了前一种。第三个是最后读取用的while (fgets(...))只要fgets没返回NULL就说明文件还没有结束。这是「读文件读到末尾」的标准循环结构比用feof做循环判断稳妥得多——feof只有当读取操作已经越过了文件末尾才返回真用它做循环条件往往会让最后一个数据被处理两次。跑通这个例子之后你已经能应付百分之九十的初学者期文件读写任务了。不过真正的实战里你再小心也会撞上一些让人摸不着头脑的鬼问题下一章我把这些坑集中扒一遍。5. 常见问题的避坑清单五个让新手崩溃的典型翻车现场5.1 写入后立刻读取文件却是空的现象你写完fwrite或者fprintf紧接着又在同一个程序里打开同一个文件去读可能读出来的内容不完整甚至为空。原因fwrite等函数把数据写进的是内存缓冲区stdio 缓冲区它不会马上就落到磁盘上。你在同一个进程里立刻用另一个流去读读的是磁盘上还没来得及更新的文件。解决写入后如果马上要读先fclose再fopen或者用fflush(fp)强制把缓冲区内容刷到磁盘。fprintf(fp, hello); fflush(fp); // 强制落盘但缓冲区机制仍然存在 fseek(fp, 0, SEEK_SET); // 然后可以继续读也可以用另一个流重新打开我在实际项目里见过有人为了省一次fopen调用写完不关直接读结果查了半小时。记住文件流的缓冲区是黑匣子越早关闭越省心。5.2 用 fscanf 读字符串时中文或带空格的数据被截断现象文件里是一行zhangsan 92你用fscanf(fp, %s %d, name, score)读没问题。改成zhangsan li 92名字中间有空格name只读到了zhangsan后面的li被当成了下一个格式串的输入。原因%s在fscanf里的行为是「读到空白字符停止」空格、制表符、换行都是分隔符。它不会帮你把带空格的一整行当做一个字段。解决如果一行是一整条数据不要用fscanf搭配%s改用fgets读整行然后再解析。这个坑在 C 语言文件处理里极其常见血的教训。5.3 二进制文件用 fwrite 写结构体换台电脑读不出来现象你在 Windows 上用fwrite把一个结构体数组写进文件拿到 Linux 上读数据乱了。原因结构体存在内存对齐不同平台对结构体的填充字节padding可能不同sizeof(struct)在 Windows 和 Linux 上可能是 12 和 16 的区别另外小端序和大端序的差异会让多字节数据如int的字节序不一样。解决如果文件要在不同平台间交换就别直接写整个结构体对象而是把每个成员用fprintf写成文本或者用固定宽度的整数类型如stdint.h里的int32_t并自行控制对齐。这算是二进制读写的进阶课但建议新手从第一天就意识到「二进制文件不跨平台」。5.4 读取时用 feof 判断文件结束结果多读了一次现象最后一条数据被打印了两遍。原因feof的语义是「上一次读取操作是否已经越过了文件末尾」而不是「当前位置是否到了文件末尾」。你在读完最后一份数据之后位置停在末尾但feof返回的是假直到你再一次读取失败feof才变成真。如果你用while (!feof(fp))做循环条件循环体会多执行一次。解决用读取函数的返回值判断fgetc返回EOFfgets返回NULLfread返回 0。这是我每隔一阵子就会在某处看到的错误统称「feof 玄学」。// 正确写法 while (fgets(line, sizeof(line), fp) ! NULL) { process(line); } // 错误写法循环体会多跑一次 while (!feof(fp)) { fgets(line, sizeof(line), fp); process(line); }5.5 fopen 成功但读写失败错误被吞掉了现象fopen返回的指针非空但fread就是读不到东西或者fwrite没写进去程序也不报错。原因fopen成功不代表文件可读可写比如以r模式打开一个目录、或一个权限受限的文件权限问题在 Linux 下特别常见。fread这类函数返回的计数才是真正的结果。解决不要只看fopen的返回值fread返回的个数小于请求个数时用ferror(fp)检查文件流是否出错这能定位到具体的 I/O 错误。size_t n fread(buf, sizeof(int), 10, fp); if (n 10) { if (ferror(fp)) { perror(读取过程中发生错误); } else { printf(文件提前到了末尾实际读到 %zu 个\n, n); } }这五条是我认为新手期命中率最高的坑每条我都现场见过有人踩。前四条属于认知问题第五题属排查习惯问题都是从「跑通」到「跑稳」之间的必经之路。6. 把文件当数据库用两个让代码立刻实用的进阶技巧这一章不做总结就讲两个能在你下一个项目里直接派上用场的进阶动作——「文件里存结构体」和「按偏移量精确读取」。结构体的二进制存取假设你要保存一批学生记录每一条包含学号、姓名、成绩。把结构体成员逐个写比把整个结构体一次写更稳妥这在前一章也提过。但如果你百分之百确定这个文件只在自己电脑上、只用你自己的程序读那直接fwrite整个结构体数组是完全可接受的typedef struct { int id; char name[64]; int score; } Student; Student students[100]; // 填充 students ... fwrite(students, sizeof(Student), 100, fp);配合fseek和fread你就拥有了一套「按键取值」的迷你数据库Student one; // 跳到第 5 条记录的位置 fseek(fp, 5 * sizeof(Student), SEEK_SET); fread(one, sizeof(Student), 1, fp); printf(第 5 条记录%d %s %d\n, one.id, one.name, one.score);参数说明5 * sizeof(Student)是偏移量单位是字节。SEEK_SET表示从文件头开始算。如果你做的程序需要「在文件中间插入一条记录」文本文件做起来几乎不可能但二进制文件配合fseek可以精确定位这是文本方式给不了的能力。另一个实用技巧是「统计文件行数」这个经典面试题。用fgetc逐字符读数换行符的个数FILE *fp fopen(note.txt, r); if (fp NULL) { perror(文件打开失败); return 1; } int lines 0; int ch; while ((ch fgetc(fp)) ! EOF) { if (ch \n) lines; } fclose(fp); printf(文件共有 %d 行\n, lines);注意ch必须是int而不是char因为EOF通常是 -1是一个整数char可能无法正确表示它。这也是个高频出错点。我自己的一个习惯是任何文件操作写完先跑一个「写 - 关 - 读 - 关」的闭环测试再去做业务逻辑。别相信眼睛看到的一次性运行成功文件这种持久化状态出问题通常是下一次运行的时候。希望这篇笔记能帮你把文件读写这条水管彻底打通。只要坚持用返回值判断错误、记得关闭每一个fopen这个方向你很快就能上手而且不会再翻车。本文还有配套的精品资源点击获取