
1. 为什么使用文件2. 什么是文件3. 二进制文件和文本文件4. 文件的打开和关闭5. 文件的顺序读写6. 文件的随机读写7. 文件缓冲区8.更新文件1. 为什么使用文件文件的核心作用就是把数据从易失的内存存到持久的硬盘里程序退出了数据也不会丢。2. 什么是文件在程序设计中我们⼀般谈的文件有两种程序文件、数据文件2.1程序文件程序文件就是和 “程序本身”相关的文件比如.c源代码、编译生成的.obj目标文件、最终的.exe可执行文件这些都是用来构成和运行程序的。2.2数据文件数据文件就是程序运行时用来读写数据的文件内容不一定是代码比如程序运行需要从中读取数据的文件或者输出内容的文件程序从里面读数据也能把结果写到里面去。2.3文件名文件名其实是文件的唯一标识用来定位和区分不同文件完整的标识包含三部分文件路径文件在磁盘上的位置比如 c:\code\文件名主干文件的名字比如 test文件后缀表示文件类型比如 .txt举个例子c:\code\test.txt路径c:\code\主干test后缀.txt3. 二进制文件和文本文件存储方式存储规则示例整数10000占用空间文本文件ASCLL形式所有数据都按 ASCII 字符存储数值会被拆成字符1 0 0 0 05 字节二进制文件直接把内存中的二进制数据原封不动写入文件00000000 00000000 00100111 000100004 字节int 类型占 4 字节关键区别文本文件人能直接看懂比如用记事本打开但会额外占用空间读写时需要转换。二进制文件和内存中的数据结构完全一致读写速度快空间更省但人无法直接看懂。3.1整数10000的两种存储方式1.内存中的形式10000 作为 int 类型在内存中是 4 字节的二进制00000000 00000000 00100111 00010000十进制转二进制结果。2.ASCII 形式文本文件会把 10000 拆成 5 个字符1 0 0 0 0每个字符的 ASCII 码是1 → 00110001490 → 0011000048所以文件里会存这 5 个字节00110001 00110000 00110000 00110000 00110000共 5 字节。3.二进制形式二进制文件直接把内存中的 4 字节原封不动写入文件文件里就是00000000 00000000 00100111 00010000共 4 字节。用 VS 的二进制编辑器打开会看到十六进制00 00 27 10和二进制一一对应。3.2演示代码解析#include stdio.h int main() { int a 10000; FILE* pf fopen(test.txt, wb); // wb 表示以二进制模式写文件 fwrite(a, 4, 1, pf); // 从变量a的地址开始写4个字节写1次 fclose(pf); pf NULL; return 0; }fopen的 wb 模式是关键w 表示写b 表示二进制binary不加 b 就是文本模式。fwrite直接把 a 的内存数据原封不动写入文件所以生成的是二进制文件。我们发现文件会产生乱码的情况这是因为文本文档中不是在二进制文件中并且直接打开是会产生弹窗。这时我们就要改变它的打开方式将其改为二进制的打开方式这时生成结果就是下面的二进制序列3.3VS 中查看二进制文件的方法1.在 VS 的「解决方案资源管理器」中右键 test.txt。2.选择「打开方式」。3.在弹出的窗口中选择「二进制编辑器」。4.打开后就能看到文件的十六进制内容00 00 27 10和我们前面分析的一致。4. 文件的打开和关闭4.1流和标准流4.1.1流1流是 C 语言中对外部设备文件、键盘、屏幕等数据交互的抽象概念可以理解为数据流动的通道。2.所有输入输出操作如键盘输入、屏幕打印、文件读写都是通过 “流” 来完成的。3.流操作自带缓冲区目的是减少直接读写硬盘的次数提高效率。4.1.2标准流C 程序启动时会自动打开 3 个流无需手动 fopen流名含义用途stdin标准输入流从键盘读取数据scanf 函数使用它stdout标准输出流向屏幕打印数据printf 函数使用它stderr标准错误流向屏幕输出错误信息这三个流的类型都是 FILE*文件指针和文件操作的指针是同一种类型。4.2文件指针每个被打开的文件都会在内存中创建一个文件信息区FILE 结构体变量存储文件名、状态、读写位置等信息。FILE* 就是指向这个结构体的指针我们通过它来间接操作文件。不同编译器的 FILE 结构体细节不同如 VS 的 struct _iobuf但用法一致开发者无需关心内部实现。4.3文件的打开和关闭文件在读写之前应该先打开文件在使用结束之后应该关闭文件。同时ANSI C 规定使用fopen函数来打开文件fclose 来关闭文件。4.3.1fopen//打开⽂件 FILE * fopen ( const char * filename, const char * mode );1.参数filename要打开的文件名可以是相对路径或绝对路径。绝对路径盘符:/文件夹/文件名.后缀 E:/project/data.txt相对路径同级直接写文件名 子级文件夹/文件名 上级../文件名mode文件打开方式核心参数决定读写权限和文件类型。2.返回值成功返回一个 FILE* 类型的指针指向该文件的信息区。失败返回 NULL必须做判空处理否则会引发程序崩溃。#includestdio.h int main() { FILE* pf fopen(test.txt,r); if (pf NULL) { perror(fopen); return 1; } //读文件 //关闭为文件 fclose(pf); pf NULL; }常用mode 对照表模式含义文本不存在时文本存在时“r”只读文本文件报错打开只读“w”只写文本文件创建新文件清空原有内容只写“a”追加文本文件创建新文件在文件末尾追加数据“rb”只读文本文件报错打开只读“wb”只写文本文件创建新文件清空原有内容只写“ab”追加文本文件创建新文件在文件末尾追加数据“r”读写文本文件报错打开可读写“w”读写文本文件创建新文件清空原有内容可读写“a”读写文本文件创建新文件末尾追加 读4.3.2fcloseint fclose(FILE *stream);功能关闭文件刷新缓冲区释放文件信息区的资源。返回值成功返回 0失败返回 EOF。注意文件使用完必须关闭否则会导致数据未写入、资源泄漏。关闭后建议将指针置为 NULL避免野指针。代码演示#include stdio.h int main() { // 1. 打开文件只读模式 FILE* fp fopen(test.txt, r); if (fp NULL) // 必须判空 { perror(fopen); // 打印错误信息 return 1; } printf(文件打开成功可以进行读写操作\n); // 2. 关闭文件 fclose(fp); fp NULL; // 置空避免野指针 return 0; }4.4避坑指南与关键细节1.必须判空fopen 失败会返回 NULL如果直接使用空指针操作文件会导致程序崩溃。2.关闭文件不关闭文件会导致缓冲区数据未写入硬盘文件内容丢失同时占用系统资源。3.文本和二进制模式文本模式下Windows 系统会自动将 \n 转换为 \r\n 写入文件读取时再转换回来。二进制模式下数据原封不动读写不会做任何转换适合图片、视频等非文本数据。4.路径问题Windows 下路径分隔符是 \但 C 语言中 \ 是转义字符所以路径要写成 D:\\test.txt 或 D:/test.txt。5.w 模式会清空文件使用 w 或 wb 打开已存在的文件时文件内容会被直接清空操作前一定要确认。5.文件的顺序读写5.1函数总览函数名功能适用范围fgetc从输入流读取单个字符所有输入流文件、stdinfputc从输出流读取单个字符所有输出流文件、stdinfgets从输入流读取字符串所有输入流fputs从输出流读取字符串所有输出流fscanf从输入流读取格式化数据所有输入流fprintf从输出流读取格式化数据所有输出流fread读取一块二进制数据仅文件输入流fwrite写入一块二进制数据仅文件输出流5.2字符读写函数5.2.1fputcint fputc ( int character, FILE * stream );1.参数character待写入字符stream文件指针 / 输出流stdout--屏幕2.返回值成功返回写入的字符int 型失败返回EOF(-1)代码演示写入单个字符#include stdio.h int main() { FILE* fp fopen(test.txt, w); // w模式清空原有内容新建文件写入 if(fp NULL) { perror(fopen); return 1; } fputc(a, fp); fputc(b, fp); fputc(c, fp); fclose(fp); // 关闭文件 fp NULL; // 置空避免野指针 return 0; }此时test.txt文件中将是abc代码演示2循环写入 a-z 所有字符#include stdio.h int main() { FILE* fp fopen(test.txt, w); if(fp NULL) { perror(fopen); return 1; } int ch 0; for(ch a; ch z; ch) { fputc(ch, fp); } fclose(fp); fp NULL; return 0; }此时test.txt文件中将是a到z所有字符5.2.2fgetcint fgetc ( FILE * stream );参数stream文件指针 / 输入流stdin--键盘返回值成功返回读取的字符int 型读到末尾 / 出错返回EOF(-1)代码演示1读取文件前 10 个字符并打印前 10 个字符#include stdio.h int main() { FILE* fp fopen(test.txt, r); // r模式只读 if(fp NULL) { perror(fopen); return 1; } int i 0; for(i 0; i 10; i) { int c fgetc(fp); fputc(c, stdout); // stdout标准输出控制台 } fclose(fp); fp NULL; return 0; }代码演示2读取文件中所有数据并打印文件中所有数据#includestdio.h int main() { FILE* pf fopen(test.tst, r);//读文件 if (pf NULL) { perror(fopen); return 1; } int ch 0; while ((ch fgetc(pf)) ! EOF) { printf(%c,ch); } fclose(pf);//关闭文件 pf NULL; return 0; }5.2.3feof和ferrorfeofint feof ( FILE * stream ); //检测stream指针指向的流是否遇到⽂件末尾参数stream文件指针返回值到文件末尾返回非 0未到末尾返回 0ferrorint ferror ( FILE * stream ); //检测stream指针指向的流是否发⽣读/写错误参数stream文件指针返回值读写出错返回非 0无错误返回 0代码演示1feof 判断文件末尾#include stdio.h int main() { FILE* fp fopen(test.txt, r); if(fp NULL) { perror(fopen); return 1; } int c 0; while((c fgetc(fp)) ! EOF) // 循环读取直到末尾 { fputc(c, stdout); } if(feof(fp)) printf(读到文件末尾\n); else if(ferror(fp)) printf(读取出错\n); fclose(fp); fp NULL; return 0; }代码演示2ferror 判断读写错误#include stdio.h int main() { FILE* fp fopen(test.txt, w); // 只写模式不能读会触发错误 if(fp NULL) { perror(fopen); return 1; } int c fgetc(fp); // 尝试读取触发错误 if(c EOF) { if(ferror(fp)) printf(读取出错\n); } fclose(fp); fp NULL; return 0; }5.3字符串读写函数5.3.1fputsint fputs ( const char * str, FILE * stream );参数str待写入字符串stream文件指针 / 输出流返回值成功返回非负数失败返回EOF(-1)代码演示1写入字符串//fputs输入字符串到文件中 #includestdio.h int main() { FILE* pf fopen(test.tst, w);//写文件 if (pf NULL) { perror(fopen); return 1; } fputs(hello, pf); fputs(world, pf); fclose(pf);//关闭文件 pf NULL; return 0; }此时test.txt文件中将是helloworld5.3.2fgetschar * fgets ( char * str, int num, FILE * stream );参数str存储读取内容的字符数组num最大读取字符数包含结尾的\0,实际最多放num-1个字符stream文件指针 / 输入流返回值成功返回str 地址末尾 / 出错返回NULL代码演示1读取文件两行内容#include stdio.h int main() { FILE* fp fopen(test.txt, r); if(fp NULL) { perror(fopen); return 1; } char arr1[10] {0}; char arr2[10] {0}; fgets(arr1, sizeof(arr1), fp); // 读取第一行 fgets(arr2, sizeof(arr2), fp); // 读取第二行 printf(%s, arr1); printf(%s, arr2); fclose(fp); fp NULL; return 0; }或者运用循环//用fgets读取文件 #includestdio.h int main() { FILE* pf fopen(test.tst, r);//写文件 if (pf NULL) { perror(fopen); return 1; } char arr[20] -----------; /*fgets(arr, 5, pf);*///abcd\0 //如果有多行数据 运用循环 while ((fgets(arr, 20, pf)) ! NULL) { printf(%s , arr); } fclose(pf);//关闭文件 pf NULL; return 0; }5.4格式化读写函数5.4.1fprintfint fprintf ( FILE * stream, const char * format, ... ); //类⽐printf函数学习参数stream文件指针 / 输出流format格式字符串...可变参数待写入数据返回值成功返回写入字符个数失败返回负数代码演示1写入结构体数据//用fprintf写入文件 #includestdio.h struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s { zhangsan, 20, 95.5f }; FILE* pf fopen(test.tst, w);//写文件 if (pf NULL) { perror(fopen); return 1; } fprintf(pf,名字:%s 年龄;%d 成绩:%f\n,s.name ,s.age,s.score ); fclose(pf);//关闭文件 pf NULL; return 0; }此时test.txt文件中将是zhangsan, 20, 95.5f5.4.2fscanfint fscanf ( FILE * stream, const char * format, ... ); //类⽐scanf函数来学习参数stream文件指针 / 输入流format格式控制字符串同scanf...变量地址列表返回值成功函数返回成功填充到参数列表中的项数。如果在成功读取任何数据之前发生1.发生读取错误会在对应流上设置错误指示符则返回EOF。2.到达文件末尾会在对应流上设置⽂件结束指⽰符则返回EOF。代码演示1读取文件中结构体数据//用fscanf读取数据 #includestdio.h struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s { zhangsan, 20, 95.5f }; FILE* pf fopen(test.tst, w);//写文件 if (pf NULL) { perror(fopen); return 1; } fscanf(pf, 名字:%s 年龄:%d 成绩:%f, s.name, (s.age), (s.score)); fprintf(stdout, 名字:%s 年龄:%d 成绩:%.2f\n, s.name, s.age, s.score); fclose(pf);//关闭文件 pf NULL; return 0; }5.5sprintf 和 sscanf5.5.1sprintfint sprintf ( char * str, const char * format, ... );功能把格式化数据写入字符数组字符串相当于把 printf 的输出从屏幕改成内存字符串。参数str字符数组指针存储生成的字符串空间要足够防止溢出format格式字符串%d、%f、%s 等...可变参数对应格式的数据返回值成功返回写入的字符数不含末尾\0失败返回负值示例核心就是把内容转换成字符串 并存放struct Stu s {zhangsan, 100, 85.5f}; char arr[100] {0}; sprintf(arr, %s %d %.1f, s.name, s.age, s.score);这个代码就是将结构体s中的内容转换成一个字符串5.5.2sscanfint sscanf ( const char * str, const char * format, ...);功能从字符串里读取格式化数据相当于把 scanf 的输入从键盘改成内存字符串。参数str源字符串数据来源format格式字符串...变量地址列表返回值成功返回成功读取赋值的参数个数失败 / 到末尾返回 EOF(-1)示例核心是从字符串中解析出一个格式化的数据char str[] zhangsan 100 85.5; struct Stu s {0}; sscanf(str, %s %d %f, s.name, s.age, s.score);把sprintf和sscanf结合起来使用//ssprintf //把格式化数据 转换成字符串并存放在内存中 #includestdio.h struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s { zhangsan, 20, 95.5f }; char arr1[30] { 0 }; sprintf(arr1, %s %d %.2f, s.name, s.age, s.score);//存放进字符串内存区 printf(%s\n, arr1); //sscanf //从arr1中解析处一个结构体数据 struct Stu t { 0 }; sscanf(arr1, %s %d %f, t.name, (t.age), (t.score)); sprintf(stdout, %s %d %.2f, s.name, s.age, s.score);//存放进字符串内存区 return 0; }此时输出结果是下面这张图5.5.3 6组格式化函数总对比函数作用操作对象scanf格式化输入标准输入 stdinprintf格式化输出标准输出 stdoutfscanf格式化输入文件流 / stdinfprintf格式化输出文件流 / stdoutsprintf格式化输出内存字符串sscanf格式化输入内存字符串5.6二进制批量读写函数5.6.1fwrite从文件中读取数据到格式化变量中size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );参数ptr待写入数据的内存首地址size单个数据的字节大小count要写入的数据个数stream文件指针示例写入数组二进制数据//fwrite的使用 #includestdio.h struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s { zhangsan, 20, 95.5f }; FILE* pf fopen(test.tst, wb);//写二进制文件 if (pf NULL) { perror(fopen); return 1; } //写一个结构体类型的内容 到文件中去 fwrite(s, sizeof(struct Stu), 1, pf); fclose(pf); pf NULL; return 0; }5.6.2fread 二进制读取从文件中读取数据到格式化变量中size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );功能批量写二进制参数const void* buf, size_t size, size_t count, FILE* stream返回实际写入的块数示例读取二进制结构体数据//fread读取二进制数据 #includestdio.h struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s { 0 }; FILE* pf fopen(test.tst, rb);//读取二进制文件 if (pf NULL) { perror(fopen); return 1; } //读取文件中的数据 fread(s, sizeof(struct Stu), 1, pf); printf(%s %d %.2f\n, s.name, s.age, s.score); fclose(pf); pf NULL; return 0; }此时输出结果是5.7核心编码规范1.打开文件必须判断NULL用perror(fopen)打印错误2.文本模式r/w/a二进制模式rb/wb/ab3.用完必须fclose(fp); fpNULL;4.读取后用feof、ferror判断状态6. 文件的随机读写6.1fseek移动文件指针int fseek ( FILE * stream, long int offset, int origin );功能定位文件指针实现随机读写参数stream文件指针offset偏移量正数向后、负数向前、0 不动origin基准位置1.SEEK_SET文件开头2.SEEK_CUR当前指针位置3.SEEK_END文件末尾返回值成功返回0失败返回非 0-1注意二进制文件可随意偏移文本文件仅支持SEEK_SET或ftell返回值偏移//用fseek定位文件指针实现随机打印 //文件中是 abcdefghi #includestdio.h int main() { FILE* fp fopen(test.txt, r); if (fp NULL) { perror(fopen); return 1; } int c fgetc(fp);//a fputc(c, stdout);//使用fputc在标准输入流上打印字符 //定位文件指针 fseek(fp, 5, SEEK_SET);//从首个开始定位 向右5个偏移量 fseek(fp, 4, SEEK_CUR);//这是从a开始定位 向右4个偏移量 fseek(fp, -4, SEEK_END);//这是从末尾开始定位 向左4个偏移量 c fgetc(fp); fputc(c, stdout);//f fclose(fp); fp NULL; return 0; }此时test.txt文件中将是abcdefghi标准输出的结果是af6.2ftelllong int ftell ( FILE * stream );功能返回文件指针相对于文件开头的字节偏移量参数stream文件指针返回值成功返回字节数失败返回-1L用途常和fseek配合计算文件大小fseek(fp,0,SEEK_END); long sizeftell(fp); // 得到文件总字节大小6.2.1fseek和ftell的共同使用//fseek和 ftell的共同使用 #includestdio.h int main() { FILE* fp fopen(test.txt, r); if (fp NULL) { perror(fopen); return 1; } int c fgetc(fp); fputc(c, stdout);//输出a int pos ftell(fp);//这里记录的是b相对于起始位置的偏移量1 //定位文件指针 fseek(fp, 4, SEEK_CUR); c fgetc(fp); fputc(c, stdout);//这里的是f相对b往后偏移4 fseek(fp, pos, SEEK_SET);//这里回到初始 偏移量为1 c fgetc(fp); fputc(c, stdout);//这里就是b fclose(fp); fp NULL; return 0; }输出结果如下其中pos记录了第一次的字节数移动偏移量用于最后打印b时从f返回到起点每次移动后都想打印就要重新用fgetc和fputc6.3rewindvoid rewind ( FILE * stream );功能文件指针重置到文件开头关键区别rewind(fp)移动指针 清除错误标志、文件末尾标志fseek(fp,0,SEEK_SET)只移动指针不清除标志接着上面的代码用rewind回到起始位置 fputc(c, stdout);//这里就是b rewind(fp);//回到起始位置 c fgetc(fp); fputc(c, stdout);//这里就是a fclose(fp); fp NULL; return 0;输出结果如下7. 文件缓冲区7.1文件缓冲区概念ANSI C 规定系统自动在内存给每个打开的文件开辟一块缓冲区输出程序数据 → 输出缓冲区 → 缓冲区满 / 刷新 → 写入硬盘输入硬盘数据 → 输入缓冲区 → 程序读取作用减少磁盘 IO提升读写效率7.2 fflush 函数强制刷新缓冲区int fflush ( FILE * stream );功能输出流强制把缓冲区数据立刻写入硬盘输入流行为不标准一般不用streamNULL刷新所有打开的输出流参数stream文件指针 / stdout返回值成功输出0 失败输出EOF刷新缓冲区的 4 种方式缓冲区写满自动刷新调用 fflush(fp)手动强制刷新调用 fclose(fp) 关闭文件自动刷新程序正常结束自动刷新核心结论文件操作必须刷新缓冲区或关闭文件否则数据可能只在内存没写到硬盘。示例代码FILE* pf fopen(test.txt, w); fputs(abcdef, pf); fflush(pf); // 强制写入磁盘 fclose(pf);8.更新文件在文件的打开模式中有三种方式值得注意分别是: r, w, a。行为rwa解释可读/可写可读/可写可读/可写文件不存在时打开失败自动创建新文件自动创建新文件文件存在时保留内容清空内容保留内容初始文件指针位置文件开头文件开头文件末尾写入是否覆盖所有数据是可定位覆盖是内容已清空从头写入)否默认是在文件末尾写数据典型用途修改文件部分内容创建新文件或完全重写旧文件在文件末尾追加数据比如记录日志关键要点1. 在写完文件后要继续读文件的时候在读取之前⼀定要使用 fflush() 刷新文件缓冲区或者使用 fseek() rewind() 重新定位文件指示器的位置。2. 在读完文件后需要继续写文件之前在写文件之前使⽤ fseek() rewind() 重新定位文件指示器的位置。#include stdio.h int main() { FILE* fp fopen(test.txt, w); if (fp NULL) { perror(fopen for w); return 1; } //写abcdefghi到⽂件中 fputs(abcdefghi, fp); //刷新缓冲区保证数据写⼊⽂件 fflush(fp); //要读取数据b字符先定位⽂件指针 fseek(fp, 1, SEEK_SET); int ch fgetc(fp);//读取字符 printf(%c\n, ch); //在b的位置开始写⼊hello fseek(fp, -1, SEEK_CUR); //解释因为前⾯读取⼀个字符后,⽂件指⽰器现在指向了c,需要从当前位置退回⼀个字符fputs(hello, fp); //关闭⽂件 fclose(fp); fp NULL; return 0; }