C语言文件操作全攻略:从基础读写到随机访问与缓冲区原理
在C语言程序开发中数据的持久化存储是不可或缺的核心能力。程序运行时产生的所有数据都存储在内存中一旦程序退出或系统断电这些数据就会永久丢失。而文件操作正是解决这一问题的关键它允许我们将数据写入磁盘文件在需要时再读取回来实现数据的长期保存和跨程序共享。本文将系统讲解C语言文件操作的全部核心知识从文件的基本概念到高级的随机读写再到容易被忽略的文件缓冲区原理帮你全面掌握这一基础且重要的技能。一、为什么需要文件操作我们之前编写的所有程序数据都是存储在内存中的变量、数组、结构体等数据结构都在栈或堆上分配空间程序运行时可以读写这些数据但程序结束后操作系统会回收所有内存资源再次运行程序时无法获取上一次运行产生的任何数据这种临时性的存储方式无法满足很多实际需求比如保存用户的配置信息和程序状态存储大量的业务数据如学生信息、商品库存实现程序之间的数据交换记录程序运行日志以便排查问题文件操作的出现完美解决了这些问题它将数据存储在磁盘等持久化存储介质上即使程序退出或计算机重启数据依然存在。二、文件的基本概念2.1 文件的分类从程序设计的角度我们通常将文件分为两大类程序文件存储程序代码的文件包括源程序文件.c、目标文件Windows下.obj、可执行程序Windows下.exe等数据文件存储程序运行时读写的数据的文件这也是本章讨论的重点2.2 文件名每个文件都有一个唯一的文件标识也就是我们常说的文件名它由三部分组成文件路径 文件名主干 文件后缀例如C:\Luminous\code\student.txt文件路径C:\Luminous\code\指定文件在磁盘上的位置文件名主干student用于区分不同的文件文件后缀.txt标识文件的类型三、二进制文件与文本文件根据数据在磁盘上的存储方式数据文件可以分为文本文件和二进制文件两种。3.1 核心区别文本文件数据以ASCII码的形式存储每个字符占用一个字节文件内容可以直接用文本编辑器打开查看二进制文件数据直接以内存中的二进制形式存储不进行任何转换文件内容无法直接用文本编辑器查看3.2 存储示例以整数10000为例文本文件存储占用5个字节分别存储字符1、0、0、0、0的ASCII码二进制文件存储占用4个字节直接存储整数 10000 的二进制表示00000000 00000000 00100111 00010000二进制文件写入示例#include stdio.h int main() { int a 10000; FILE* Luminous fopen(test.bin, wb); fwrite(a, 4, 1, Luminous); // 以二进制形式写入文件 fclose(Luminous); Luminous NULL; return 0; }运行后生成的test.bin文件用文本编辑器打开会显示乱码但用二进制编辑器可以看到正确的十六进制值10 27 00 00小端存储。四、文件的打开与关闭C 语言中所有的文件操作都遵循先打开后操作最后关闭的流程。4.1 流与标准流为了统一对不同外部设备键盘、显示器、磁盘、打印机等的操作C语言抽象出了流的概念。我们可以把流想象成一个数据通道数据通过流在程序和外部设备之间传输。C语言程序在启动时会自动打开三个标准流stdin标准输入流默认关联键盘scanf函数从该流读取数据stdout标准输出流默认关联显示器printf函数向该流写入数据stderr标准错误流默认关联显示器用于输出错误信息这三个流的类型都是FILE*也就是文件指针。4.2 文件指针每个被打开的文件系统都会在内存中创建一个对应的文件信息区用于存储文件的名称、状态、当前读写位置等信息。这个文件信息区是一个结构体类型名为FILE。我们通过一个FILE*类型的指针来维护这个文件信息区进而操作对应的文件FILE* pf; // 定义一个文件指针变量4.3 fopen打开文件函数原型FILE* fopen(const char* filename, const char* mode);filename要打开的文件名可以是相对路径或绝对路径mode文件的打开方式决定了我们可以对文件进行哪些操作返回值打开成功返回指向文件信息区的指针失败返回NULL必须检查返回值文件打开可能会因为各种原因失败如文件不存在、权限不足、磁盘已满等如果直接对NULL指针进行操作会导致程序崩溃。4.4 文件打开模式大全模式含义文件不存在时文件存在时读写位置r只读文本文件出错保留原有内容文件开头w只写文本文件创建新文件清空原有内容文件开头a追加文本文件创建新文件保留原有内容文件末尾rb只读二进制文件出错保留原有内容文件开头wb只写二进制文件创建新文件清空原有内容文件开头ab追加二进制文件创建新文件保留原有内容文件末尾r读写文本文件出错保留原有内容文件开头w读写文本文件创建新文件清空原有内容文件开头a读写文本文件创建新文件保留原有内容文件末尾rb读写二进制文件出错保留原有内容文件开头wb读写二进制文件创建新文件清空原有内容文件开头ab读写二进制文件创建新文件保留原有内容文件末尾4.5 fclose关闭文件函数原型int fclose(FILE* stream);stream指向要关闭的文件的指针返回值关闭成功返回0失败返回EOF重要注意事项文件使用完毕后必须关闭否则会导致资源泄漏同时可能造成数据丢失关闭文件后原文件指针会变成野指针必须手动置为NULL完整的文件打开与关闭示例#include stdio.h #include stdlib.h int main() { FILE* fp fopen(test.txt, r); if (fp NULL) { perror(fopen failed); // 打印详细的错误信息 return 1; } printf(文件打开成功\n); // 这里进行文件读写操作 fclose(fp); // 关闭文件 fp NULL; // 置空指针 return 0; }五、文件的顺序读写顺序读写是指按照文件内容的先后顺序依次进行读写操作这是最常用的文件操作方式。C 语言提供了多组函数用于不同类型数据的顺序读写。5.1 字符读写fgetc和fputcfputc写入一个字符函数原型int fputc(int character, FILE* stream);功能将一个字符写入指定的输出流返回值成功返回写入的字符失败返回EOF示例写入26个英文字母到文件#include stdio.h int main() { FILE* fp fopen(char.txt, w); if (fp NULL) { perror(fopen); return 1; } for (char ch a; ch z; ch) { fputc(ch, fp); } fclose(fp); fp NULL; return 0; }fgetc读取一个字符函数原型int fgetc(FILE* stream);功能从指定的输入流读取一个字符返回值成功返回读取的字符到达文件末尾或发生错误返回EOF示例读取文件中的所有字符并打印#include stdio.h int main() { FILE* fp fopen(char.txt, r); if (fp NULL) { perror(fopen); return 1; } int ch; while ((ch fgetc(fp)) ! EOF) { putchar(ch); } fclose(fp); fp NULL; return 0; }5.2 字符串读写fgets和fputsfputs写入一个字符串函数原型int fputs(const char* str, FILE* stream);功能将一个字符串写入指定的输出流不包含结尾的\0返回值成功返回非负整数失败返回EOF示例fputs(Hello, World!\n, fp); fputs(C语言文件操作\n, fp);fgets读取一个字符串函数原型char* fgets(char* str, int num, FILE* stream);功能从指定的输入流读取字符串最多读取num-1个字符遇到换行符或文件末尾停止特点会将读取到的换行符\n也存入字符串中并在末尾自动添加\0返回值成功返回str到达文件末尾或发生错误返回NULL示例char buf[1024]; while (fgets(buf, sizeof(buf), fp) ! NULL) { printf(%s, buf); }5.3 格式化读写fscanf和fprintf这两个函数与我们熟悉的scanf和printf用法几乎完全相同只是多了一个文件指针参数可以对任意文件流进行格式化读写。fprintf格式化写入函数原型int fprintf(FILE* stream, const char* format, ...);示例将结构体数据写入文件#include stdio.h struct Student { char name[20]; int age; float score; }; int main() { struct Student s {张三, 20, 95.5f}; FILE* fp fopen(student.txt, w); if (fp NULL) { perror(fopen); return 1; } fprintf(fp, %s %d %.1f, s.name, s.age, s.score); fclose(fp); fp NULL; return 0; }fscanf格式化读取函数原型int fscanf(FILE* stream, const char* format, ...);示例从文件中读取结构体数据#include stdio.h struct Student { char name[20]; int age; float score; }; int main() { struct Student s {0}; FILE* fp fopen(student.txt, r); if (fp NULL) { perror(fopen); return 1; } fscanf(fp, %s %d %f, s.name, s.age, s.score); printf(姓名%s年龄%d成绩%.1f\n, s.name, s.age, s.score); fclose(fp); fp NULL; return 0; }5.4 数据块读写fread和fwrite这两个函数用于以二进制形式读写整块数据适合读写数组、结构体等复杂数据类型效率比格式化读写高。fwrite二进制写入函数原型size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);ptr指向要写入的数据的指针size每个数据项的大小字节count要写入的数据项的数量返回值实际成功写入的数据项数量示例写入一个整型数组到二进制文件#include stdio.h int main() { int arr[] {1, 2, 3, 4, 5}; FILE* fp fopen(array.bin, wb); if (fp NULL) { perror(fopen); return 1; } fwrite(arr, sizeof(int), 5, fp); fclose(fp); fp NULL; return 0; }fread二进制读取函数原型size_t fread(void* ptr, size_t size, size_t count, FILE* stream);返回值实际成功读取的数据项数量示例从二进制文件读取整型数组#include stdio.h int main() { int arr[5] {0}; FILE* fp fopen(array.bin, rb); if (fp NULL) { perror(fopen); return 1; } size_t n fread(arr, sizeof(int), 5, fp); printf(成功读取%d个整数\n, n); for (int i 0; i n; i) { printf(%d , arr[i]); } fclose(fp); fp NULL; return 0; }5.5 重要函数对比C语言中有三组非常相似的格式化输入输出函数很多初学者容易混淆这里做一个清晰的对比函数输入 / 输出源用途scanf标准输入流 (stdin)从键盘读取格式化数据printf标准输出流 (stdout)向屏幕输出格式化数据fscanf任意输入流从文件或键盘读取格式化数据fprintf任意输出流向文件或屏幕输出格式化数据sscanf字符串从内存中的字符串解析格式化数据sprintf字符串将格式化数据写入内存中的字符串sprintf和sscanf示例#include stdio.h struct Student { char name[20]; int age; float score; }; int main() { struct Student s1 {李四, 21, 88.0f}; char buf[100] {0}; // 将结构体数据格式化为字符串 sprintf(buf, %s %d %.1f, s1.name, s1.age, s1.score); printf(格式化后的字符串%s\n, buf); // 从字符串中解析出结构体数据 struct Student s2 {0}; sscanf(buf, %s %d %f, s2.name, s2.age, s2.score); printf(解析结果%s %d %.1f\n, s2.name, s2.age, s2.score); return 0; }六、文件的随机读写顺序读写只能从文件开头或末尾开始依次操作而随机读写允许我们在文件的任意位置进行读写大大提高了文件操作的灵活性。6.1 fseek定位文件指针函数原型int fseek(FILE* stream, long int offset, int origin);offset偏移量正数表示向后偏移负数表示向前偏移origin起始位置有三个可选值SEEK_SET文件开头SEEK_CUR文件指针当前位置SEEK_END文件末尾示例修改文件中的指定内容#include stdio.h int main() { FILE* fp fopen(example.txt, w); if (fp NULL) { perror(fopen); return 1; } fputs(This is an apple., fp); fseek(fp, 9, SEEK_SET); // 从文件开头向后偏移9个字节 fputs( sam, fp); // 写入内容覆盖原有数据 fclose(fp); fp NULL; return 0; }运行后文件内容变为This is a sample.6.2 ftell获取当前偏移量函数原型long int ftell(FILE* stream);功能返回文件指针相对于文件开头的当前偏移量常用用途获取文件的大小示例获取文件大小#include stdio.h int main() { FILE* fp fopen(example.txt, rb); if (fp NULL) { perror(fopen); return 1; } fseek(fp, 0, SEEK_END); // 将文件指针移到文件末尾 long size ftell(fp); // 获取当前偏移量即文件大小 printf(文件大小%ld字节\n, size); fclose(fp); fp NULL; return 0; }6.3 rewind回到文件开头函数原型void rewind(FILE* stream);功能将文件指针重新定位到文件的起始位置等价于fseek(stream,0,SEEK_SET);示例先写后读同一个文件#include stdio.h int main() { FILE* fp fopen(test.txt, w); if (fp NULL) { perror(fopen); return 1; } // 写入数据 for (char ch A; ch Z; ch) { fputc(ch, fp); } rewind(fp); // 回到文件开头 // 读取数据 char buf[27] {0}; fread(buf, 1, 26, fp); printf(%s\n, buf); fclose(fp); fp NULL; return 0; }七、文件读取结束的正确判定很多初学者会错误地使用feof函数来判断文件是否读取结束这是一个非常常见的误区。7.1 feof和ferror的正确用法feof(FILE* stream)检测流是否到达文件末尾到达返回非0值否则返回0ferror(FILE* stream)检测流是否发生读写错误发生错误返回非0值否则返回0核心原则feof函数是在读取函数返回 EOF 之后用来判断是因为到达文件末尾而结束还是因为发生错误而结束。不能用feof直接作为循环条件来读取文件。错误写法// 错误feof会在文件最后一个字符读取后才返回真导致多读取一次 while (!feof(fp)) { int ch fgetc(fp); putchar(ch); }正确写法int ch; while ((ch fgetc(fp)) ! EOF) { putchar(ch); } // 读取结束后判断是正常结束还是错误结束 if (feof(fp)) { printf(文件读取正常结束\n); } else if (ferror(fp)) { printf(文件读取发生错误\n); }八、文件缓冲区原理很多人可能会遇到这样的情况向文件写入数据后立即打开文件却发现没有内容这是因为 C 语言采用了缓冲文件系统。8.1 什么是文件缓冲区ANSI C标准规定系统会自动在内存中为每个正在使用的文件开辟一块缓冲区输出缓冲区程序向文件写入数据时先将数据写入输出缓冲区缓冲区满了之后才会一次性写入磁盘输入缓冲区程序从文件读取数据时先将磁盘上的数据读入输入缓冲区然后再从缓冲区逐个读取数据到程序变量缓冲区的存在大大提高了文件操作的效率因为磁盘的读写速度比内存慢得多频繁的磁盘读写会严重影响程序性能。8.2 fflush强制刷新缓冲区函数原型int fflush(FILE* stream);功能强制将输出缓冲区中的数据立即写入磁盘参数为NULL时会刷新所有打开的输出流缓冲区演示示例#include stdio.h #include unistd.h // 用于sleep函数 int main() { FILE* fp fopen(buffer.txt, w); if (fp NULL) { perror(fopen); return 1; } fputs(Hello, Buffer!, fp); // 数据先写入缓冲区 printf(数据已写入缓冲区现在打开文件看不到内容\n); sleep(5); // 睡眠5秒 fflush(fp); // 强制刷新缓冲区数据写入磁盘 printf(缓冲区已刷新现在打开文件可以看到内容了\n); sleep(5); fclose(fp); // 关闭文件时也会自动刷新缓冲区 fp NULL; return 0; }8.3 重要结论因为缓冲区的存在我们在操作文件时必须注意文件操作结束后一定要调用fclose关闭文件它会自动刷新缓冲区如果需要立即将数据写入磁盘可以调用fflush强制刷新程序崩溃时缓冲区中的数据会丢失无法写入磁盘九、文件更新模式详解在实际开发中我们经常需要同时读写同一个文件这时候就需要使用r、w、a这三种更新模式。它们的行为有很大区别使用时一定要注意模式文件不存在文件存在初始指针位置写入特点典型用途r打开失败保留内容文件开头可覆盖原有数据修改文件部分内容w创建新文件清空内容文件开头从头写入创建新文件或完全重写a创建新文件保留内容文件末尾只能在末尾追加日志记录使用更新模式的注意事项写完文件后要继续读文件必须先调用fflush刷新缓冲区或者用fseek/rewind重新定位文件指针读完文件后要继续写文件必须用fseek/rewind重新定位文件指针十、总结文件操作是C语言程序开发中不可或缺的技能它让我们能够实现数据的持久化存储。本文我们系统学习了文件操作的必要性解决内存数据临时性的问题文件的基本概念分类、文件名、二进制文件与文本文件的区别文件的打开与关闭fopen和fclose的用法各种打开模式的区别文件的顺序读写字符、字符串、格式化、数据块四种读写方式文件的随机读写fseek、ftell、rewind三个核心函数文件读取结束的正确判定feof和ferror的正确用法文件缓冲区原理理解缓冲区的作用和刷新时机文件更新模式r、w、a的区别和使用注意事项文件操作的核心原则打开文件必须检查返回值读写操作要符合文件的打开模式文件使用完毕必须及时关闭关闭文件后立即将指针置为NULL不要用feof直接作为循环条件读取文件掌握了这些知识你就能够熟练地进行各种文件操作写出稳定、高效的C语言程序了。