恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C语言文件读写从入门到避坑:FILE、打开模式与读写函数详解
首页
资讯中心
/
C语言文件读写从入门到避坑:FILE、打开模式与读写函数详解
C语言文件读写从入门到避坑:FILE、打开模式与读写函数详解
发布时间:2026/10/15 9:15:52
简介这份讲解C语言文件读写的入门doc文档专门面向刚接触文件操作的小白学习者内容直击fopen、fprintf、fgets、fclose等基础函数通过完整的写入与读取示例把打开文件、检查空指针、写入文本、逐行读取、关闭文件这几个关键步骤讲得清楚易懂。整个压缩包非常精简只有1个doc文件大小约14KB没有冗余素材适合随身查阅或快速过一遍知识点。文中的两段示例代码展示了w写模式与r读模式的差异同时解释了文件指针、缓冲区大小等容易困惑的细节并在代码旁边给出多行中文注释跟着敲一遍即可理解文件操作的标准流程还能学会用if (fp NULL)做错误处理避免程序崩溃。这份资料已有103人学习对于想避开晦涩理论、直接通过代码和注释入门文件读写的初学者来说是一个轻量又实用的选择掌握这些基础后还可以继续尝试二进制文件读写、文件定位等进阶操作也可以把读写模式灵活运用于日志记录、配置读取等实际场景。1. 为什么说C语言文件读写是小白最容易“看着会、写着废”的一章A同学上周写学生成绩管理程序fopen成功了fprintf也执行了程序正常退出。结果重开一读最后一条学生记录不见了拿到Windows上跑文件里多出一堆乱码字符。这个章节要讲的C语言文件读写本质上就是一套“把内存里的数据搬到磁盘再原样搬回来”的流程核心就三件事搞懂FILE指针是什么、打开模式怎么选、读写函数之间的边界在哪。它解决的问题也直白为什么代码没报错数据却丢了、怎么选对打开模式、怎么把结构体安全落盘再完整读回。适合刚学完指针和结构体、想做数据持久化的初学者也适合所有被乱码和丢数据折磨过的自学者。2. 把C语言文件读写拆开FILE、fopen模式与IO三条线路2.1 为什么所有文件操作都绕不开FILE指针刚接触文件读写时很容易把FILE当成“文件内容”本身这是个根深蒂固的误解。FILE指针指向的其实是一个由C运行时维护的“流”管理结构里面装着当前文件位置指示器、缓冲区指针、错误标志、是否到达文件末尾等一堆状态。你对文件做的每一次读写本质都是通过这个管理结构去操作底层文件描述符而不是直接碰磁盘。这也解释了为什么fopen之外的所有读写函数第一个参数几乎都是FILE *。你传入的不是“文件”是“这条流的身份凭证”。fputc、fgets、fprintf、fwrite全都要靠它找到正确的文件和正确的当前位置。搞懂这一层后面所有函数都只是往这条流里塞数据或取数据只不过每次塞的粒度不同。顺便说一下三个标准流stdin、stdout、stderr。它们也是FILE *只不过不用fopen程序启动时就由运行时帮你打开好了。很多小白写代码时对printf有直觉因为它不用传FILE参数但实际上printf就是在往stdout这条流里写。想通这一点文件读写就不再是“另一门玄学”而只是把流向从stdout换成了你自己的文件而已。2.2 fopen的六种打开模式一张表治选择困难fopen的模式参数是文件读写里第一个分水岭。选错模式轻则数据写不进去重则辛辛苦苦攒的文件被清空。常见模式一共六种下面这张表直接对照着选就行模式文件存在时文件不存在时初始写指针位置是否清空原内容r正常打开打开失败文件开头只读否r正常打开打开失败文件开头可读写否w正常打开创建新文件文件开头只写是w正常打开创建新文件文件开头可读写是a正常打开创建新文件文件末尾只写否a正常打开创建新文件文件末尾可读写否新手最容易踩的坑就是w系列。很多人的需求是“往已有文件里追加内容”顺手写了个w结果原文件瞬间被截断成空文件。追加需求应该用a既要读又要改现有内容用r只有明确要“重建”文件时才用w。另外注意r系要求文件必须存在不存在直接返回NULL而w和a系会自动创建文件这就是为什么有人r打开失败后一脸懵。模式后面还能拼b表示二进制例如rb、wb、ab。严格来说C标准里b是和t相对的模式修饰符在Linux上写不写都一样但在Windows上影响巨大文本模式下换行符会被自动翻译成\r\n二进制模式不做任何翻译。凡是读写结构体、图片、音频这类非纯文本数据一律用wb或rb别碰默认的文本模式。2.3 打开文件后第一件事不是读写失败处理与关闭习惯这里想强调一个很多教程不讲、但实战中最要命的细节fopen失败时返回NULL而且不会像printf那样向你报告。文件路径不存在、当前用户没有权限、磁盘出问题fopen全都静默返回NULL接下来你对NULL指针的任何读写操作都是未定义行为程序可能在毫秒级崩溃也可能悄悄写进一片未知内存。所以正确姿势是每次fopen后立即判断。#include stdio.h #include stdlib.h int main(void) { // 第一个参数是文件路径第二个参数是打开模式 // 这里用 r 打开一个只读文件文件不存在时返回NULL FILE *fp fopen(data.txt, r); if (fp NULL) { perror(fopen 失败); // 把最近一次的errno翻译成人话打印出来 return EXIT_FAILURE; } // 到这里才说明文件真的打开成功了在中间做读写 // 收尾关闭流并刷新缓冲区失败时fclose返回EOF if (fclose(fp) ! 0) { perror(fclose 失败); return EXIT_FAILURE; } return EXIT_SUCCESS; }这段代码是整个文件读写的地基。fopen的第一个参数是路径字符串第二个参数是模式字符串perror会根据系统维护的errno值打印出具体错误原因比如No such file or directory或Permission denied比自己写printf猜原因靠谱得多。fclose的作用是归还FILE结构并冲刷缓冲区它返回0表示成功返回EOF表示出错这个返回值同样不能丢。我见过太多人fclose后不检查返回值磁盘满了都不知道数据根本没落盘。另外有个习惯值得从第一天就养成文件操作完立刻关闭不要等到程序结尾。一个进程能打开的文件描述符数量有限循环里反复fopen不fclose几十次之后fopen就会稳定返回NULL。3. 从写入开始四个写入函数的真实边界3.1 fputc逐字符写入什么场景才值得用它fputc的作用是往流里写一个字符原型是int fputc(int ch, FILE *fp)。注意第一个参数虽然是int实际只取低8位当作unsigned char来写。它的返回值是被写入字符的unsigned char转换回int写失败时返回EOF。逐字符写入看起来最基础好像所有场景都能用实际上它的效率最低每调用一次fputc都会产生一次函数调用还要经过缓冲区的检查逻辑。如果你只是把内存里已有的字符串整段写下去完全没必要逐字拆开写。但有两个场景它不可替代一是流式生成字符比如边读边过滤、遇到某个字符就大写化的小工具二是你要一个字符一个字符拼接输出且单次数据量不大时代码意图最直白。#include stdio.h int main(void) { FILE *fp fopen(output.txt, w); if (fp NULL) { perror(打开失败); return 1; } const char *msg school data; // 逐字符写入直到碰到字符串结尾的\0 for (int i 0; msg[i] ! \0; i) { fputc(msg[i], fp); // 写单个字符失败时返回EOF } fclose(fp); return 0; }这段代码的重点在循环结束条件fputc按单字节写字符串的结尾标志\0不能也不该被写进文件否则文件末尾会多出一个值为0的字节。很多小白在这里把msg[i] ! \0写成i strlen(msg)效果一样但每次循环都要算一遍字符串长度纯属浪费。真正需要警惕的是fputc的返回值正常返回写入的字符失败返回EOF别拿返回值跟字符本身比较去判断成功。3.2 fputs和fprintf文本写入的两把主力枪fputs把一个字符串写入流原型int fputs(const char *str, FILE *fp)。它和fputc最大的区别是fputs不自动加换行符而且它写的是整个字符串直到\0为止。很多初学者以为fputs会自动换行结果每行数据都粘连成一长串这就是没搞清楚“fputs只负责搬字符不负责排版”。fprintf则是格式化写入和printf唯一的区别就是多了一个FILE *参数写在文件里的内容跟你打印到屏幕上的完全一样。写文本类数据我默认首选fprintf因为它把数字、字符串、浮点数的转换一次做完可读性远高于一长串fputc调用。下面的代码演示了最典型的结构化文本写入CSV格式。#include stdio.h typedef struct { int id; char name[32]; float score; } Student; int main(void) { FILE *fp fopen(students.csv, w); if (fp NULL) { perror(文件打开失败); return 1; } Student list[2] { {1001, zhangsan, 92.5f}, {1002, lisi, 87.0f} }; for (int i 0; i 2; i) { // %d写学号%s写姓名%.1f写成绩并保留一位小数 fprintf(fp, %d,%s,%.1f\n, list[i].id, list[i].name, list[i].score); } fclose(fp); return 0; }fprintf的第一个参数是目标流后面跟格式化字符串和数据。这里的%.1f表示输出浮点数且保留一位小数因为成绩一般没必要带着浮点误差全精度输出。写完每一行加一个\n方便后续用fgets逐行读回。选择fprintf而不是fputs的原因是fputs直接写字符串遇到id这种int变量还得先把数字转成字符串数组步骤繁琐且容易在数组长度上翻车fprintf用格式控制一步到位这也是它成为文本写入首选的根本原因。3.3 fwrite批量落盘结构体数组一次写完如果数据是结构体数组还要走fprintf逐字段格式化那就太慢了。fwrite是二进制层面的批量写入原型size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp)它把内存中一块区域原样拷贝到文件里不做任何字符转换。这是结构体持久化的主力函数。#include stdio.h #define STUDENT_NUM 2 typedef struct { int id; char name[32]; float score; } Student; int main(void) { FILE *fp fopen(students.dat, wb); // 必须用二进制模式写 if (fp NULL) { perror(文件打开失败); return 1; } Student list[STUDENT_NUM] { {1001, zhangsan, 92.5f}, {1002, lisi, 87.0f} }; // 一次写STUDENT_NUM个元素每个元素大小是sizeof(Student) size_t written fwrite(list, sizeof(Student), STUDENT_NUM, fp); if (written ! STUDENT_NUM) { fprintf(stderr, 写入元素数量不对只写了 %zu 个\n, written); } fclose(fp); return 0; }fwrite的四个参数分别是起始地址、单个元素字节数、元素个数、目标流。这里把size设成sizeof(Student)、count设成STUDENT_NUM语义是一个元素一个元素地写另一种常见写法是把size设成1、count设成sizeof(Student) * STUDENT_NUM两者的最终写入字节数一样但前者能告诉你“成功写入了几个完整元素”后者只能说“成功写入了多少字节”。文件大小不敏感时怎么选都行但强烈建议保留元素语义因为错误排查时这个数字非常关键。返回值written是成功写入的元素个数不等于字节数。只要written不等于期望的count就说明中间有写入失败可能是磁盘满、也可能是fp已经是无效指针。很多人在这一步偷懒不检查返回值程序跑完才发现文件缺了一半再回去定位就晚了。二进制模式wb在这里是硬性要求用w在Windows上跑结构体里的某些字节会被换行翻译破坏具体原因在第5章展开。3.4 数据不是立刻落盘的缓冲区与fflush前面所有写入函数写入后数据并不是马上到达磁盘。C标准库默认对普通文件采用全缓冲策略数据先攒在内存缓冲区里等缓冲满了或明确调用fflush时才一次性写入磁盘。这意味着“fprintf执行完”跟“文件里真的有这些内容”之间存在时间差。全缓冲的后果在两种场景下特别严重一是程序异常崩溃比如写文件写一半断电或进程被杀缓冲区里没来得及冲刷的数据直接消失二是A进程写完数据后B进程立刻去读同一文件读到的可能还是旧内容。解决方式就是主动冲刷fflush(fp)在读写切换和关键节点强制把缓冲区内容推送到磁盘。注意fflush的调用时机写完后读同一文件前必须调用否则你读写切换时位置指针和缓冲状态都可能不一致。其实fclose本身包含冲刷动作这也是为什么“记得fclose”不只是优雅更是数据完整性的保证。我习惯在每次比较关键的fprintf之后主动fflush一次虽然多几次系统调用会牺牲一点性能但在存档、日志这类要求数据及时落盘的场景里这笔开销完全值得。性能敏感的批量写场景则可以反过来攒够一批再fflush把系统调用次数降下来。4. 把数据读回来四种读取函数的边界和返回值含义4.1 先纠正根深蒂固的误解feof不是“读之前”的判断feof可能是文件读写里被误解得最深、翻车率最高的函数。它的作用是“检查是否曾尝试读过文件末尾之后的位置”而不是“预测下一次读取会不会到达末尾”。换句话说feof返回非零值的前提是你已经有一次读取操作越过了文件末尾。这个区别直接导致了经典的while (!feof(fp))无限多读一行问题。典型错误写法是这样循环条件先判断feof发现还没到末尾就进循环体读数据。问题出在最后一次迭代读完最后一个数据后文件位置已经在末尾但feof标志还没置位因为“越过末尾”的尝试还没发生。于是循环体再进去一次读到失败返回了旧值或垃圾数据。修正方案很简单不要用feof当循环条件而是用读取函数的返回值判断是否应该继续。feof的正确使用场景是“读取失败后用它来判断失败原因”如果feof非零说明是正常读到末尾如果feof为零则多半是真正的IO错误。4.2 fgets行读取最稳的文本读取方式fgets按行读取文本是工程里的首选方案原型char *fgets(char *str, int size, FILE *fp)。它最多读取size-1个字符遇到换行符就停并在末尾自动补上\0。这句话拆开有四个坑要记牢一是“最多size-1个字符”如果一行特别长会分多次读完每次返回的是不完整的行二是换行符会被保留在缓冲区里你得自己决定要不要去掉三是它永远在末尾加\0所以缓冲区的实际容量至少要等于size四是读取成功返回str指针失败或到末尾返回NULL这个返回值就是循环的天然判断条件。#include stdio.h int main(void) { FILE *fp fopen(students.csv, r); if (fp NULL) { perror(打开失败); return 1; } char line[128]; // fgets返回NULL表示读取结束这个条件比!feof(fp)可靠得多 while (fgets(line, sizeof(line), fp) ! NULL) { printf(读到: %s, line); // line里自带换行符所以printf不用加\n } fclose(fp); return 0; }逻辑说明循环在fgets返回NULL时终止这个NULL只在两种情况下出现——读到文件末尾或读取出错正好避免了feof多读一次的坑。sizeof(line)作为第二个参数传入保证fgets最多写入127个字符加一个\0不会溢出缓冲区。第三个参数是目标流。注意printf(读到: %s, line)这里故意没加\n因为fgets已经把换行符读进line了再加\n会输出一个空行。这段代码可以直接处理前面第3.2节生成的CSV每行一个学生记录。fgets还有个隐藏的Windows兼容问题在Windows文本模式下打开文件行尾的\r\n会被运行时翻译成\n所以读出来的内容里只有\n没有\r如果文件用二进制模式rb打开\r就会原样留在缓冲区里打印时可能出现光标跳到行首的“乱码”效果。文本数据用文本模式读是最省心的默认选择。4.3 fscanf格式化读取方便但对脏数据没任何抵抗力fscanf是fprintf的逆向操作最大的好处是代码简短一个函数调用就把学号、姓名、成绩这些不同类型的变量全部填充完毕。最阴险的地方也在它的简短里fscanf对输入数据的格式有严格预期一旦实际内容和格式串对不上匹配失败的字段后面所有内容全部留在缓冲区不动返回值告诉你匹配了几个字段但如果你不看返回值程序就拿着旧值继续跑数据错到自己都发现不了。#include stdio.h int main(void) { FILE *fp fopen(students.csv, r); if (fp NULL) { perror(打开失败); return 1; } int id; char name[32]; float score; // 返回值是成功匹配并赋值的字段数量每次读之前先清零重建变量 while (fscanf(fp, %d,%31s,%f, id, name, score) 3) { printf(id%d name%s score%.1f\n, id, name, score); } fclose(fp); return 0; }这段代码把返回值等于3设成循环继续的条件只要有一个字段没匹配上循环就会终止。这里用了%31s限制字符串读取长度防止姓名超过缓冲区大小造成溢出。fscanf的三个数据参数后面分别带了取地址符或直接传数组名name本身就是数组首地址不需要再加。这套写法的缺陷也暴露得很清楚如果文件中间某一行格式不对fscanf匹配失败后不会再自动跳过那行而是卡死在原地。所以工程里更常见的做法是用fgets整行读进来再用sscanf从字符串里解析行读不进缓冲区时可以单独报错不会连累整个循环。4.4 fread读回结构体怎么写的就怎么读用fwrite写进去的二进制结构体读回最匹配的当然是fread。原型size_t fread(void *ptr, size_t size, size_t count, FILE *fp)参数含义和fwrite一一对应。核心原则就一句话当初怎么写的就得怎么读。用wb写、按结构体数组组织读时就按rb打开、按同样大小的结构体一个个读回byte对齐、成员顺序、填充字节全都替你原样还原。#include stdio.h #define STUDENT_NUM 2 typedef struct { int id; char name[32]; float score; } Student; int main(void) { FILE *fp fopen(students.dat, rb); if (fp NULL) { perror(打开失败); return 1; } Student list[STUDENT_NUM]; // 一次读取STUDENT_NUM个元素返回值是实际读到的元素个数 size_t read fread(list, sizeof(Student), STUDENT_NUM, fp); if (read ! STUDENT_NUM) { fprintf(stderr, 实际读取 %zu 个文件可能不完整\n, read); } for (size_t i 0; i read; i) { printf(id%d name%s score%.1f\n, list[i].id, list[i].name, list[i].score); } fclose(fp); return 0; }fread的参数说明和fwrite对称第一个参数list是接收数据的起始地址第二个参数是单个结构体的字节大小第三个参数是期望读取的元素个数第四个参数是流。返回值和fwrite一样是成功读入的“完整元素个数”不是字节数。这里存在一个常见误用有人把size设成sizeof(Student) * STUDENT_NUM、count设成1想一次性灌满整个数组。这样做文件读到最后只剩半个结构体时fread只能返回0你根本不知道是哪半个数据缺失。按元素读取则能明确指出“读到了几个完整结构体”排查半截文件时信息量大得多。二进制读回还有几件事必须注意不能跨平台直接交换文件因为不同机器的int字节序和结构体对齐可能不同fread后的结构体里如果含指针成员指针值只是当初写入时的内存地址恢复不了也不该恢复。不是所有结构体都适合二进制持久化带指针、带长度字段的复杂结构建议还是老老实实用文本格式。5. C语言文件读写避坑指南五条翻车经验每条都有后悔药5.1 模式选错w把旧数据清空了现象程序原本用来更新学生信息的记录运行一次之后原文件内容全没了只剩刚写入的那几条新记录。原因fopen(data.txt, w)本身就是“创建或截断”操作一旦文件已存在打开瞬间内容就被清空后面fprintf写入的只是新数据。很多教程里演示用的都是w因为每次运行生成一个全新文件最省事但这个习惯带到真实项目里就是事故。解决更新已有文件的场景看清楚需求再选模式。要保留原内容只追加用a或a要保留原内容又能改中间的某几条用r只有确定要重建整个文件时才用w。如果不确定目标文件是否存在先以r试探性打开成功就改用r失败说明文件不存在再改用w创建两步判断能避免绝大多数误清空。已经清空了没有后悔药但从这次开始凡是带清空语义的模式打开前先备份一次旧文件成本极低。5.2 路径反斜杠与工作目录为什么我的文件“找不到”现象在Windows下写fopen(D:\data\file.txt, r)返回NULL同一个程序在IDE里运行能读文件双击exe运行就找不到文件。原因两个问题混在一起。第一C语言字符串里反斜杠是转义符\d、\f实际上被解析成了别的字符路径根本不对正确写法是D:\\data\\file.txt或D:/data/file.txt正斜杠在Windows下完全兼容。第二相对路径是相对于进程的当前工作目录这个目录由启动方式决定从IDE运行时通常是项目源码目录双击exe运行时则是exe所在目录目录一变相对路径就失效。解决调试阶段直接用绝对路径并把路径里的反斜杠改成正斜杠先把程序跑通发布阶段改成相对路径同时意识到相对基准是当前工作目录而不是exe位置。更稳妥的做法是在程序启动时就打印当前工作目录确认自己到底站在哪再决定文件路径怎么写。在Linux和macOS上反斜杠转义问题不存在但工作目录问题一样存在换平台后先检查路径别一上来就怀疑读写函数出了问题。5.3 fscanf匹配失败还不检查返回值程序陷入死循环现象文件里有一行数据少了个逗号程序非但不报错反而在同一行数据上反复输出停都停不下来。原因fscanf匹配失败时未匹配成功的内容原封不动留在文件缓冲区里文件位置指针没有前进。循环条件里如果只写了类似fscanf(fp, %d,%s, ...)而没检查返回值失败后下一轮循环读取的还是同一块内容于是无限失败、无限循环。输出看起来像是程序“卡死”了其实它在疯狂空转。解决每次fscanf都必须检查返回值。如果匹配失败先想办法把这一行剩余内容消费掉再继续下一行通常用fgetc循环读到换行符为止while (fscanf(fp, %d,%f, id, score) 2) { // 正常的解析逻辑 }另一种更稳妥的方案是彻底绕开fscanf改用fgets加上sscanf。fgets负责把一整行搬进内存sscanf负责从字符串里解析解析失败时这行已经消费掉了不会阻塞后续读取。这个组合对脏数据的容忍度比fscanf高一个量级处理格式不严格的配置文件时几乎是标配。5.4 二进制模式写结构体Windows下文件字节数不对现象结构体数据在Linux下用fwrite写得整整齐齐同一份代码放到Windows下编译运行生成的二进制文件比预期大了读回时字段错位、数据全是乱的。原因文本模式和二进制模式的换行翻译机制不同。Windows下用文本模式w打开文件写入的每个\n都会在落盘时被翻译成\r\n。结构体成员的值里如果恰好有字节0x0A它就会被当成换行符自动扩展成两个字节文件大小自然膨胀。更糟的是0x0A后面若跟着0x0D读取时还会被反向翻译数据彻底损坏。Linux不做这种翻译所以同样的代码在Linux上看起来一切正常。解决处理结构体、图像等非纯文本数据时打开模式强制用rb和wb。这是硬规则没有任何例外。文本模式只留给本来就以文本为载体的数据——日志、CSV、ini配置等。还有一个排查技巧如果二进制文件读回来字段错乱先看文件大小和预期大小是否一致。不一致十有八九就是换行翻译在捣乱查一下打开模式有没有带b后缀就行了。5.5 fclose返回值被忽略磁盘满时数据悄悄丢现象程序跑完没报错文件也打开了但检查文件内容发现只有前半部分后半段数据没了程序退出码还是0。原因fclose失败时返回EOF把错误信息留在errno里。很多人的收尾代码只写了fclose(fp);忽略返回值于是磁盘写满、IO错误这类问题在最后一步悄悄发生程序自己都不知道数据没写全。缓冲区里的数据刷新到磁盘这一步恰恰是fclose内部完成的这一步失败等于之前所有写操作全部白费。解决关键存档场景给fclose补上检查if (fclose(fp) EOF) { perror(关闭文件失败数据可能未完整落盘); return 1; }同时建议在写日志、存档这类场景写入过程中主动调用fflush(fp)把数据分批冲刷到磁盘缩小数据丢失的窗口。fclose后还要再检查一次警惕性别让收尾成为整个流程里最薄弱的一环。高频写入场景可以反过来做先fflush失败就立即报错不要等到fclose才暴露问题不然你根本不知道是哪一段写入出的错。6. 进阶把文件读写收进自己的工具箱6.1 fseek和ftell定位读取与文件大小普通读写是顺序的但文件操作必须支持“跳着读”。fseek负责移动流的位置指针原型int fseek(FILE *fp, long offset, int origin)origin可以取SEEK_SET文件开头、SEEK_CUR当前位置、SEEK_END文件末尾offset是相对origin的偏移字节数。ftell则返回当前字节偏移量。这两个函数组合起来能拿到文件大小fseek(fp, 0, SEEK_END); // 跳到文件末尾 long size ftell(fp); // 拿到末尾位置的字节数就是文件大小 fseek(fp, 0, SEEK_SET); // 跳回开头方便从头读注意这个方式只对普通文件可靠管道、设备文件等特殊流上ftell可能返回无意义值。超过2GB的大文件还要谨慎因为long在Windows下是32位上限不够可以换用fgetpos和fsetpos或平台相关的ftello具体取舍取决于你目标平台。6.2 一个顺手的小封装把换行符收拾干净既然fgets会把换行符读进来与其每次都手动处理不如封一个小工具。这个封装同时处理Linux的\n和Windows的\r\n#include string.h // 去掉fgets读进来的行尾换行符原地修改buf void trim_line(char *buf) { size_t len strlen(buf); while (len 0 (buf[len - 1] \n || buf[len - 1] \r)) { buf[len - 1] \0; len--; } }逻辑说明从字符串尾部往前扫描把\n和\r都替换成\0直到遇到第一个非换行字符为止。这样从文本模式或二进制模式读到的行都能统一得到一个干净的字符串。strlen每调用一次是O(n)的但对长度有限的行缓冲来说开销微不足道。把它和fgets搭配就得到了一个比fscanf健壮得多的逐行解析基础工具后面解析CSV、日志时都用它来收尾。我的习惯是文件代码写完后第一件事永远是打印fopen的结果和目标文件大小页面显示正常再谈后续逻辑。开头多花这一分钟能省掉后面数小时的迷茫排查。希望帮到你。本文还有配套的精品资源点击获取