C 语言文件操作:让数据从内存走向持久化

发布时间:2026/10/3 19:58:41
C 语言文件操作:让数据从内存走向持久化 C 语言文件操作让数据从内存走向持久化程序运行时变量和数组通常保存在内存中。内存访问速度快但它的内容会随着进程结束而消失。一个通讯录、配置文件或日志程序如果只把数据放在内存里关闭程序后所有修改都会丢失。要让数据在下次运行时仍然存在就需要把它保存到磁盘文件或者保存到数据库。文件操作正是 C 语言连接程序和外部存储的基础。程序可以打开文件、读取和写入数据、移动读写位置并在结束时关闭文件。掌握这些操作后就能把一次性的内存数据变成可以长期保存、重复读取和持续更新的文件数据。一、程序文件与数据文件从用途看文件大致可以分为两类程序文件保存程序本身例如.c源文件、编译产生的目标文件以及可执行文件。数据文件保存程序运行过程中需要读取或产生的数据例如用户信息、成绩记录、配置项和日志。文件名通常由三部分组成路径、主文件名和后缀。例如C:\data\contacts.txt路径决定文件所在位置主文件名用于区分内容后缀则常常提示文件的组织形式或用途。实际开发中建议明确使用绝对路径还是相对路径。相对路径是相对于程序启动时的当前工作目录而言的并不一定是源代码所在目录。二、文件为什么需要打开和关闭C 语言通过FILE类型描述一个正在使用的文件。FILE是标准库内部定义的结构类型保存文件状态、缓冲区信息以及当前读写位置等细节。程序一般不直接操作这个结构而是使用FILE *指针作为文件句柄#includestdio.hFILE*fp;打开文件使用fopenFILE*fopen(constchar*filename,constchar*mode);关闭文件使用fcloseintfclose(FILE*stream);打开可能失败因此必须检查返回值。失败时fopen返回NULL可以使用perror输出更具体的系统错误FILE*fpfopen(data.txt,r);if(fpNULL){perror(data.txt);return1;}/* 在这里读写 fp */if(fclose(fp)!0){perror(fclose);}fpNULL;关闭文件不仅是释放句柄也通常会刷新输出缓冲区。只要文件已经成功打开就应该保证每一条执行路径最终都会调用fclose。三、fopen 的模式选择第二个参数决定文件的访问方式。模式中的b表示二进制方式没有b时通常按文本方式处理。模式用途文件不存在时r只读打开文本文件打开失败w只写打开文本文件创建新文件原内容会被清空a只写并追加到文本文件末尾创建新文件rb只读打开二进制文件打开失败wb只写打开二进制文件创建新文件原内容会被清空ab只写并追加到二进制文件末尾创建新文件r读写已有文本文件打开失败w读写文本文件创建新文件原内容会被清空a读写文本文件写入位置在末尾创建新文件rb读写已有二进制文件打开失败wb读写二进制文件创建新文件原内容会被清空ab读写二进制文件写入位置在末尾创建新文件选择模式时要特别留意w和w它们会截断原文件。如果只是想在日志末尾增加内容应使用a或ab不要误用w。一个最小的写文件例子如下#includestdio.hintmain(void){FILE*fpfopen(message.txt,w);if(fpNULL){perror(message.txt);return1;}fputs(file I/O starts here\n,fp);if(fclose(fp)!0){perror(fclose);return1;}return0;}四、顺序读写从当前位置向前处理顺序读写按照文件指针当前所在位置一次处理一个字符、一行文本、一个格式化字段或一组二进制对象。常用函数可以分成以下几组操作文本或流常用函数字符输入输入流fgetc字符输出输出流fputc行输入输入流fgets行输出输出流fputs格式化输入输入流fscanf格式化输出输出流fprintf二进制输入文件流fread二进制输出文件流fwrite1. 字符和字符串fputc写入一个字符fgetc读取一个字符。fgets读取一行或指定数量的字符fputs写入一个字符串#includestdio.hintmain(void){FILE*fpfopen(note.txt,w);if(fpNULL){return1;}fputc(A,fp);fputs(\nC language file I/O\n,fp);fclose(fp);fpfopen(note.txt,r);if(fpNULL){return1;}charline[128];while(fgets(line,sizeofline,fp)!NULL){fputs(line,stdout);}fclose(fp);return0;}使用fgets时缓冲区大小必须传给函数函数会在空间允许时保留换行符并自动在末尾补上字符串结束符。读取长度不确定的行时固定数组仍可能被截断需要根据业务设计更大的缓冲区或采用动态拼接策略。2. 格式化读写fprintf和fscanf的用法分别类似printf和scanf但数据源或目标变成了文件fprintf(fp,%s %d\n,Alice,95);fscanf(fp,%31s %d,name,score);格式化输入必须检查返回值因为输入内容可能缺少字段或格式不符合预期。字符串转换还必须限制最大长度避免写出数组边界。可以把三组函数这样理解scanf、printf面向标准输入和标准输出。fscanf、fprintf面向FILE *文件流。sscanf、sprintf面向内存中的字符串。3. 二进制块读写fwrite按字节写入一组对象fread按字节读出一组对象size_twrittenfwrite(values,sizeofvalues[0],count,fp);if(written!count){/* 写入失败或只完成了部分写入 */}size_treceivedfread(values,sizeofvalues[0],count,fp);if(receivedcount){/* 可能遇到文件尾也可能发生读取错误 */}函数返回实际完成的元素个数而不是字节数。判断结果时必须使用正确的单位不能把返回值简单当作布尔值。五、随机读写移动文件指针顺序读取适合从头到尾处理日志或文本当程序需要直接访问某个位置时就要移动文件指针。fseek按偏移量定位intfseek(FILE*stream,longoffset,intorigin);origin常用的基准有SEEK_SET从文件开头计算。SEEK_CUR从当前位置计算。SEEK_END从文件末尾计算。下面的例子先写入一段文本再从开头偏移 9 个字节的位置覆盖内容#includestdio.hintmain(void){FILE*fpfopen(example.txt,wb);if(fpNULL){return1;}fputs(This is an apple.,fp);if(fseek(fp,9,SEEK_SET)!0){fclose(fp);return1;}fputs(sam,fp);fclose(fp);return0;}二进制文件中偏移量通常以字节计算。文本模式在不同平台上可能存在换行符转换因此不要轻易假设任意文本偏移都能跨平台复用。ftell获取当前位置longftell(FILE*stream);它返回当前位置相对于文件起始位置的偏移量。常见做法是先移动到末尾再调用ftell估算文件大小FILE*fpfopen(data.bin,rb);if(fpNULL){return1;}if(fseek(fp,0,SEEK_END)!0){fclose(fp);return1;}longsizeftell(fp);if(size0){perror(ftell);}fclose(fp);对于需要严格跨平台的程序应结合具体文件类型和平台接口选择更合适的文件大小获取方式不要把long能表示的范围当成所有文件都适用。rewind回到文件开头voidrewind(FILE*stream);rewind会把文件指针移动到起始位置并清除相关错误和文件尾状态。它适合在同一个文件上先写入、再从头读取FILE*fpfopen(letters.txt,w);if(fpNULL){return1;}for(intchA;chZ;ch){fputc(ch,fp);}rewind(fp);charbuffer[27];size_tnfread(buffer,1,26,fp);buffer[n]\0;puts(buffer);fclose(fp);六、文本文件与二进制文件内存中的数据本质上以二进制形式存储。写入磁盘时如果先把数值转换成可读字符就是文本文件如果直接保存内存中的字节就是二进制文件。例如整数 10000以文本形式保存时需要保存字符 1、0、0、0、0通常占 5 个字节。以二进制形式保存时常见的int可能只占 4 个字节。文本文件便于人工查看、调试和跨工具交换例如配置文件和日志。二进制文件体积更紧凑读写速度通常更高适合图片、音视频、索引和结构化数据。但二进制数据会受到类型大小、字节序、对齐方式和浮点表示等因素影响不能简单假定不同平台上的内存布局完全相同。以二进制方式保存一个整数#includestdio.hintmain(void){intvalue10000;FILE*fpfopen(number.bin,wb);if(fpNULL){return1;}if(fwrite(value,sizeofvalue,1,fp)!1){perror(fwrite);}fclose(fp);return0;}如果要设计长期保存、跨平台交换的格式更稳妥的做法是明确字段宽度、字节序和编码而不是直接把包含指针的结构体整体写入文件。指针保存的是进程地址在下一次运行或另一台机器上没有意义。七、正确判断文件是否读取结束feof经常被误用。它不是“下一次读取会不会结束”的预判函数而是用于在读取操作失败或停止后判断是否确实到达了文件尾。因此不能写成while(!feof(fp)){/* 读取数据 */}这种写法通常会多处理一次无效数据。正确方式是直接检查具体读取函数的返回值。文本字符检查 EOFfgetc返回int而不是char。这样它才能同时表示所有有效字符和特殊的EOF值intch;while((chfgetc(fp))!EOF){putchar(ch);}读取停止后再区分是正常到达文件尾还是发生了 I/O 错误if(ferror(fp)){puts(I/O error while reading);}elseif(feof(fp)){puts(end of file reached);}文本行检查 NULLfgets成功返回传入的缓冲区地址失败或到达文件尾时返回NULLwhile(fgets(line,sizeofline,fp)!NULL){process_line(line);}if(ferror(fp)){perror(fgets);}二进制块检查元素个数fread返回实际读到的元素数量。请求读取 10 个元素时返回值小于 10 就表示没有完成整批读取此时再用feof和ferror判断具体原因size_tgotfread(items,sizeofitems[0],10,fp);if(got!10){if(feof(fp)){puts(file ended before all items were read);}elseif(ferror(fp)){perror(fread);}}八、文件缓冲区与 fflush标准 C I/O 通常使用缓冲区。程序向文件写入时数据可能先进入内存中的输出缓冲区达到一定条件后才真正写到磁盘。读取时系统也可能先把一批数据装入输入缓冲区再交给程序逐步处理。缓冲区减少了频繁访问磁盘的次数但也意味着“调用写函数返回”不一定等于“数据已经落盘”。需要提前让输出可见时可以调用if(fflush(fp)!0){perror(fflush);}关闭文件时fclose通常会自动刷新输出缓冲区所以正常结束文件操作必须调用fclose。如果程序异常终止缓冲区中的内容可能来不及写入对于真正重要的数据还需要考虑操作系统和存储设备层面的持久化保证不能只依赖 C 标准库的刷新。标准输入输出流也可能有缓冲。交互程序中常见的现象是提示文字没有立即显示可以在适当位置刷新stdoutprintf(Input: );fflush(stdout);不要频繁无目的地刷新缓冲区否则会削弱缓冲带来的性能优势。应在需要与用户交互、需要观察文件最新内容或即将关闭文件时刷新。九、一个更稳健的文件复制示例下面的程序使用块读写复制二进制文件。它把打开、读取、写入和关闭的错误都纳入处理并且不使用feof作为循环条件#includestdio.hintcopy_file(constchar*source,constchar*target){FILE*infopen(source,rb);if(inNULL){perror(source);return0;}FILE*outfopen(target,wb);if(outNULL){perror(target);fclose(in);return0;}unsignedcharbuffer[4096];intok1;for(;;){size_tnfread(buffer,1,sizeofbuffer,in);if(n0fwrite(buffer,1,n,out)!n){perror(write);ok0;break;}if(nsizeofbuffer){if(ferror(in)){perror(read);ok0;}break;}}if(fclose(in)!0){ok0;}if(fclose(out)!0){ok0;}returnok;}这个例子使用rb和wb因此不会因为文本模式的换行转换而改变二进制内容。循环先处理实际读到的字节再通过ferror判断短读的原因适合复制任意类型的文件。十、文件操作中的常见问题打开文件后没有检查 NULL文件路径错误、权限不足、文件不存在或资源耗尽都可能导致打开失败。直接使用空的FILE *会造成未定义行为。用 w 更新已有文件w会清空原内容。如果只想增加内容使用a如果要修改某个位置使用适当的读写模式和fseek。把 char 用来接收 fgetcEOF需要通过int表示。使用char接收可能导致某些合法字节和EOF混淆。用 feof 控制读取循环文件尾状态通常是在一次读取尝试之后才被设置。直接以!feof(fp)作为循环条件容易多处理一次数据。忘记关闭文件不关闭文件会造成句柄泄漏输出缓冲区也可能没有及时写入。每次fopen都应对应一次fclose。把结构体内存直接当成通用文件格式结构体可能包含填充字节、指针和平台相关类型。短期同平台程序可以使用二进制写入但需要长期保存或跨平台交换时应逐字段序列化。十一、实用检查清单写文件代码时可以按照下面的顺序自检文件路径是否正确当前工作目录是否符合预期fopen的模式是否会意外清空原文件fopen的返回值是否检查过每个成功打开的文件是否都能走到fclose文本读取是否检查EOF或NULL二进制读取是否比较了fread返回的元素数量是否在读取停止后用ferror和feof区分原因fwrite是否检查了实际写入的数量是否在需要的时机刷新输出缓冲区二进制格式是否考虑类型大小、字节序和指针不可持久化的问题总结文件操作解决的是数据持久化问题程序把内存中的信息写到磁盘下一次运行时再把它读回内存。fopen和fclose管理文件生命周期fgetc、fgets、fprintf、fread等函数完成不同粒度的顺序读写fseek、ftell和rewind支持随机定位。写出可靠的文件程序关键不只是记住函数名还要始终检查返回值正确判断文件尾和 I/O 错误理解文本与二进制的差异并在合适的时候刷新和关闭文件。把这些原则落实到每一次打开、读取、写入和关闭中文件就能成为稳定可靠的数据存储接口。