1. 为什么使用文件?
我们平时写的程序的数据是存储在电脑的内存当中的,如果程序退出,内存被回收,那么数据就丢失,等再次运行程序时,上次程序的数据我们是看不到的,那么我们如果想将数据持久化的保存,
那么我们就可以使用文件。
2. 什么是文件呢?
磁盘(硬盘)上的文件就是文件
但在程序设计中,我们谈的文件一般分为两种:程序文件,数据文件,这是我们从文件的功能的角度来分的。
2.1 程序文件
程序文件就是我们平常所接触的源程序文件(.c),目标文件(.obj),可执行程序(.exe)
2.2 数据文件
其实文件的内容不仅可以是程序,还可以是数据,比如当程序运行时可以从文件中读取数据,或者输出数据。我们在之前处理的数据的输入输出对象是面向终端的,即从键盘输入数据,运行结果输出到显示屏上。
而我们也会把信息输出到硬盘上,当我们需要时,再从硬盘上读取数据到内存中使用。
2.3 文件名
当我们想要使用文件时,我们如何去找到文件呢?就是通过文件名,
文件名 包含三个部分 :文件路径 + 文件主干 + 文件后缀
例如 c:\\code\\test.txt
即该文件在 c 盘 code 文件夹 里。
3. 二进制文件和文本文件
我们根据数据的组织形式,又把数据文件分为文本文件和二进制文件
3.1 二进制文件
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件。

如图.exe为后缀的文件,里面存放的是二进制的信息,如果我们以文本形式打开会得到乱码。

3.2 文本文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是⽂ 本⽂件。
如图

3.3 一个数据在文件中是怎么存储的
之前的文章介绍过,数据在内存中的存储,而现在是数据在文件中是如何存储的
1.字符一律以ASCII值形式存储
2.数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
例如:如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),⽽ ⼆进制形式输出,则在磁盘上只占4个字节。

3.31 代码演示
//将一个数据以二进制形式写入文件
int main()
{
int a = 10000;
//打开文件
FILE* pf = fopen("test.txt", "wb");
//写入数据
fwrite(&a, 4, 1, pf);//以二进制的形式写入文件
//关闭文件
fclose(pf);
pf = NULL;//防止成为野指针
return 0;
}
以文本形式打开,是乱码

我们如果想要观察到二进制,应该以二进制编辑器的形式打开

4. 文件的打开和关闭(重点)

4.1程序与文件之间的联系
当程序打开文件时,会创建一个和文件相关的流,为了维护流,同时就会创建文件信息区,返回一个文件指针,我们可以通过文件指针来找到文件信息区,从而维护文件.
4.2 流和标准流
上述提及到流,那么流是什么呢,接下来介绍流的相关概念
4.2.1 流
其实可以把流简要理解为,流淌着字符的河,C程序对数据的输入和输出操作都是通过流来完成的
我们可以向流里写入数据,也可以读取数据,都是要先打开流,如何操作的
4.2.2 标准流
回想我们从键盘上输入数据和向屏幕上输出数据,为什么没有打开流呢?
其实是因为,C语言程序在启动时,自动打开了三个流;
1.stdin—- 标准输入流,大多数环境中是从键盘输入数据,scanf函数就是从标准输入流中来读取数据的
2.stdout—标准输出流,大多数环境输出到屏幕,printf函数就是将信息输出到标准输入流中
3.stderr—-标准错误流,大多数环境输出到屏幕
由于默认打开了这三个流,所以我们就可以使用 printf函数 和 scanf函数 来进行输入输出操作。
stdin,stdout,stderr,这三个流的类型都是 FILE*,通常成为文件指针
4.3 文件指针(概念及作用)
缓冲⽂件系统中,关键的概念是“⽂件类型指针”,简称“⽂件指针”
每个文件被使用时,都会在内存中开辟一个相应的文件信息区,存放文件的信息,这些信息其实是放在一个结构体中的,而这个结构体被系统重命名为 FILE,
如VS2013 编译环境提供的 stdio.h 头⽂件中有以下的⽂件类型申明:

所以我们通过FILE的指针来维护这个FILE结构的变量,使用起来会很方便
FILE* pf ;//这就是一个文件指针变量
pf是一个指向FILE类型数据的指针,也就是说可以使pf指向文件信息区,由此就可以访问文件,
总结:通过⽂件指针变量能够间接找到与 它关联的⽂件。
通过以下图解,可以帮助理解

4.4 文件的打开与关闭
有了前置知识,我们就可以更好的理解文件的打开与关闭和问文件指针之间的关系了。
首先,文件在读写前需要先打开文件,进行一系列操作后,需要关闭文件
重点是文件的打开与关闭,
ANSI C 规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。
4.4.4 文件的打开 — fopen
//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
简单介绍功能:可以打开参数 filename 指定的⽂件,同时将打开的⽂件和⼀个流进⾏关联, 后续对流的操作是通过 fopen 函数返回的指针来维护
其实就是通过返回一个流,通过对流的操作,从而对文件进行操作
参数:filename :表示要被打开的文件名,mode 表示打开文件的方式
返回值:打开成功,返回一个指向FILE对象的指针,打开失败,返回NULL,所以必须对返回值进行检查。
文件的打开方式
mode表⽰对打开的⽂件的操作⽅式:

不仅仅只有这几种,重点介绍 “r" 和 "w" 和”rb“和”wb“,
1. "r" 是只读,即打开文件后只能读取文件的数据,不能写入数据
2.”w“ 是只写,即打开文件后只能写入数据,不能读取数据
两者的区别 :在文件不存在的情况下,使用 “r“打开文件 会报错,而使用 ”w"会自动创建以filename命名的文件
如图:当前文件目录里并不存在test.text文件
int main()
{
//以读的形式打开文件,要保证文件在打开前已经存在
FILE* pf = fopen("test.txt", "r");
//判断返回值
if (NULL == pf)
{
perror("fopen");
return 1;
}
else
printf("打开文件成功");
//对文件的操作
//…………….
//关闭文件
fclose(pf);
pf = NULL;//防止产生野指针
return 0;
}
我们以只读的形式打开文件,由于test.txt文件不存在,故报错

而如果我们以只写的形式打开文件,则会自动创建文件test.text
int main()
{
//以读的形式打开文件,要保证文件在打开前已经存在
FILE* pf = fopen("test.txt", "w");
//判断返回值
if (NULL == pf)
{
perror("fopen");
return 1;
}
else
printf("打开文件成功");
//对文件的操作
//…………….
//
//关闭文件
fclose(pf);
pf = NULL;//防止产生野指针
return 0;
}

4.4.5 文件的关闭 — fclose
fclose函数原型
int fclose ( FILE * stream );
参数: stream :指向要关闭的流的 FILE 对象的指针
返回值:成功关闭 stream 指向的流会返回0,否则会返回 EOF 。
在文件使用结束后我们要关闭文件
代码演示
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "r"); //以“r”的形式打开⽂件,如果⽂件不存在,则打开
失败
if(fp == NULL)
{
perror("fopen\\n");
return 1;
}
printf("打开⽂件成功,可以对⽂件进⾏操作\\n");
//不再使⽤⽂件时,需要关闭⽂件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}
最重要的是编译忘记把指针置为空指针,下图是成功打开文件后,返回的地址

当关闭文件后,我们发现指针pf 仍然指向那块空间,所以如果不把pf置为NULL,会产生野指针

5. ⽂件的顺序读写(重点)
那讲完了文件的打开与关闭,接下来就要讲解文件的读写
在进⾏⽂件读写的时候,会涉及下⾯这些函数,我们⼀⼀给⼤家介绍:

上⾯说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流);所有输出流⼀ 般指适⽤于标准输出流和其他输出流(如⽂件输出流)。
5.1 fget 和 fput
1.fput
fputc函数原型
int fputc ( int character, FILE * stream );
即将字符characer 写入到 stream指向的输出流中,同时位置指示器(即光标)向前移动一位
返回值: • 成功时返回写⼊的字符(以 int 形式)。
• 失败时返回 EOF (通常是 -1),错误指⽰器会被设置,可通过ferror() 检查具体错误
使用: 向文件中写入二十六个字母


注意 fput是向输出流中写入字符,所以要以写文件的形式打开文件,不能以读的形式打开
上面是对文件流中写入字符
还可以向标准输出流(stdout)写入字符,即向屏幕写入字符


2.fget
fgetc函数的原型
int fgetc ( FILE * stream );
功能:读取字符,从stream指向的输入流在读取字符,每读取一个字符,光标向前移动一次
stream : FILE*类型的⽂件指针,可以是 stdin ,也可以是其他输⼊流的指针。如果是 stdin 就 从标准输⼊流读取数据。如果是⽂件流指针,就从⽂件读取数据。
返回值:
1.• 成功时返回读取的字符(以 int 形式)。 • 若调⽤时流已处于⽂件末尾,函数返回 EOF 并设置流的⽂件结束指⽰器( feof )。
2. 若发⽣读取错误,函数返回 EOF 并设置流的错误指⽰器( ferror )。
关键点
我们注意到,当我们读取字符读到文件末尾时,返回EOF,当我们读取遇到错误时,也返回EOF。那么当返回值是EOF时,那究竟是读到文件末尾还是发生读取错误了呢?
那我们就会涉及到判断这两种情况,由于读取到文件末尾是会设置文件结束指示器,读取错误时也会设置错误指示器,那么我们就可以使用一对函数通过判断指示器来检测是否遇到文件末尾还是发生读取错误,那我们引入 feof函数和 ferror函数
5.1.1 feof和ferror
int feof ( FILE * stream );
//检测stream指针指向的流是否遇到⽂件末尾
int ferror ( FILE * stream );
//检测stream指针指向的流是否发⽣读/写错误
1. feof检测文件指示器,如果 feof 函数检测到⽂ 件结束指⽰符已经被设置,则返回⾮0的值,如果没有设置则返回0
2.ferror检测错误指示器,如果 ferror 函数检测到错误 指⽰符已经被设置,则返回⾮0的值,如果没有设置则返回0。
应用: 结合fgetc函数
1.测试feof函数
如图在该文件写入4个字符

执行下面这段代码


表明当读完第4个字符后,遇到文件末尾,设置了文件结束指示符,feof函数遇到文件结束符,返回非0值,则打印遇到文件末尾
2.测试ferror函数
//以写的形式打开⽂件后,再去读⽂件,就会发⽣错误


5.1.2 fgetc和fputc结合使用,实现拷贝文件



5.2 fputs 和 fgets
1.fputs
fputs函数原型
int fputs ( const char * str, FILE * stream );
将参数 str 指向的字符串写⼊到参数 stream 指定的流中(不包含结尾的 \\0 ),适⽤于⽂ 件流或标准输出(stdout)
返回值: • 成功时返回⾮负整数。
• 失败时返回EOF (-1),同时会设置流的错误指⽰器,可以使⽤ ferror() 检查错误原因。


注意fputs的细节,它遇到\\0就会停止写入


我们可以发现\\0后面的字符并没有写入到屏幕上
2. fgets
fgets函数原型
char * fgets ( char * str, int num, FILE * stream );
功能,简述,从stream指向的输入流中,读取num个字符(包含\\0),存入str指向的字符数组中去,建议读参数时从右往左读,更易于理解。
返回值:
• 成功时返回 str 指针。 (观察数组str)
• 若在尝试读取字符时遇到⽂件末尾,则设置⽂件结束指⽰器,并返回 NULL ,需通过 feof() 检测。
• 若发⽣读取错误,则设置流错误指⽰器,并返回 NULL,通过 ferror() 检测。
细节点:num :最⼤读取字符数(包含结尾的 \\0 ,实际最多读取 num-1 个字符)。下面会通过调试(观察字符数组str)来讲解这一细节


注意观察变化

我们num的值是4 ,读到的实际上是字符a,字符b,字符c,和一个'\\0',所以说num :最⼤读取字符数(包含结尾的 \\0 ,实际最多读取 num-1 个字符)
5.3 fprintf 和 fscanf函数
这组函数,我们可以类比printf函数和scanf函数来学习
首先是
1.fprintf函数
int fprintf ( FILE * stream, const char * format, … );

我们发现它们参数非常相似,只是fprintf 多出了一个参数FILE*stream
功能: fprintf 是将格式化数据写⼊指定⽂件流的函数,可以输出到任意 输出流(如磁盘⽂件、标准输出、标准错误等),⽽不仅限于控制台。
返回值: • 成功时,返回写⼊的字符总数(⾮负值)。(不太关注)
• 失败时,先设置对应流的错误指⽰器,再返回负值,可以通过 ferror() 来检测。
通俗一点说,它的功能可以理解为把不同类型的数据都转换为字符型数据输出
下面是代码例子

可以通过仿照printf函数的书写形式来书写fprintf函数,如上图
写入文件的结果为

都转换为字符串了,这就是fprintf函数的作用
2. fscanf函数
该函数的作用其实可以理解为fprintf函数的反方向操作,它的功能是从指定⽂件流中读取格式化数据的函数。它类似于 scanf ,但可以指定输⼊源 (如⽂件、标准输⼊等),⽽⾮仅限于控制台输⼊。适⽤于从⽂件解析结构化数据(如整数、浮点 数、字符串等)。
通俗点说,就是从字符串中提取不同类型的数据。
返回值: • 成功时,函数返回成功填充到参数列表中的项数。该值可能与预期项数⼀致,也可能因以下原因少 于预期(甚⾄为零):
◦ 格式和数据匹配失败;
◦ 读取发⽣错误;
◦ 到达⽂件末尾(EOF)。
• 如果在成功读取任何数据之前发⽣:
◦ 发⽣读取错误,会在对应流上设置错误指⽰符,则返回EOF 。
◦ 到达⽂件末尾,会在对应流上设置⽂件结束指⽰符,则返回EOF 。
也可以类比scanf函数来学习
int fscanf ( FILE * stream, const char * format, … );

可以发现它们参数非常相似
代码演示


数据由字符串,转换成了我们指定的数据类型。
5.4 fwrite 和 fread
前面几组是关于文本文件的函数,而这组函数则是关于 二进制文件的函数
1.fwrite
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
功能:函数⽤于将数据块写⼊ stream 指向的⽂件流中,是以2进制的形式写⼊的。
ptr :指向要写⼊的数据块的指针。
• size :要写⼊的每个数据项的⼤⼩(以字节为单位)
。 • count :要写⼊的数据项的数量。
• stream :指向FILE 类型结构体的指针,指定了要写⼊数据的⽂件流
注意事项
在使⽤ fwrite() 之前,需要确保⽂件已经以⼆进制可写⽅式打开。
• fwrite() 通常⽤于⼆进制数据的写⼊,如果写⼊⽂本数据,请谨慎处理换⾏符和编码等问题。
代码使用

结果为(即在内存中存储的形式)分别为 1 2 3 4 5,表明写入成功
注意如果没有成功写入count个数据,则可能发生错误
所以要检查返回值

2.fread
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
功能:函数⽤于从 stream 指向的⽂件流中读取数据块,并将其存储到 ptr 指向的内存缓冲区中。
可以理解为fwrite的逆过程,从stream指向的文件流中读数据块放到ptr指向的内容中
返回值:返回实际读取的数据块数量
使⽤注意事项:
在使⽤ fread() ptr 指向的内存区域必须⾜够⼤,
以便存储指定数量和⼤⼩的数据块之前,需要确保⽂件已经以⼆进制可读⽅式打开
如果 fread() 成功读取了指定数量的数据块,则返回值等于 count ;如果读取数量少于 count ,则可能已经到达⽂件结尾或者发⽣了错误。
在⼆进制⽂件读取时, fread() 是常⽤的函数
使用:

注意如果没有读到count个数据,则可能遇到文件末尾或读取错误
所以要检查返回值

5.5 sprintf和sscanf函数
1. sprintf
int sprintf ( char * str, const char * format, … );
将格式化数据写⼊字符数组(字符串)
输出⽬标不是控制台或⽂件, ⽽是⽤⼾指定的内存缓冲区。常⽤于动态⽣成字符串、拼接数据或转换数据格式。简⽽⾔之就是将格 式化的数据转换成⼀个字符串。
返回值:
• 成功时:返回写⼊ buffer 的字符数(不包括结尾的空字符 \\0 )。
• 失败时:返回负值。

注意打印时以字符串形式打印

2. sscanf
int sscanf ( const char * str, const char * format, …);
功能:从字符串中读取格式化数据。它与scanf 类似,但输⼊源是内存中的字符串⽽⾮控制台或⽂ 件。常⽤于解析字符串中的结构化数据(如提取数字、分割⽂本等)。
成功时:返回成功解析并赋值的参数数量(⾮负值)
• 失败或未匹配任何数据:若输⼊结束或解析失败,返回 EOF (通常是 -1 )
使用,与sprintf的使用相反


5.6 对⽐⼀组函数: scanf/fscanf/sscanf
printf/fprintf/sprintf

6. ⽂件的随机读写
6.1 fseek
int fseek ( FILE * stream, long int offset, int origin );
功能:根据⽂件指针的位置和偏移量来定位⽂件指针(⽂件内容的光标)。
stream :指向 FILE 类型结构体的指针,指定了要定位⽂件指针的⽂件流。
• offset :偏移量,这个偏移量要根据第三个参数来确定取值,可以是正数(向⽂件末尾⽅向移 动)、负数(向⽂件开头⽅向移动)或 0(不移动)。
• origin :偏移的起始点(基准位置),这个参数有三个取值:
◦ SEEK_SET :⽂件开始位置 ◦
SEEK_CUR :⽂件指针当前的位置 ◦
SEEK_END :⽂件末尾位置
成功:如果⽂件位置指针被成功移动, fseek 返回 0。
• 失败:如果发⽣错误(例如试图移动到⼀个不存在的负偏移位置),则返回⾮零值(通常是 -1)
使用


代码解释,当光标指向b时,如果origin设置为当前位置,那么只需要向右偏以两个位置就可以找到d。同理其他两个起始位置,请读者自己思考
这里面讲到了ftell函数 ,其实就是告诉我们当前光标相对于起始位置的偏移量
6.2 ftell
long int ftell ( FILE * stream );
返回⽂件指针相对于起始位置的偏移量.简单介绍,上面有应用实例
6.2 rewind
void rewind ( FILE * stream );
功能:让⽂件指针的位置回到⽂件的起始位置
参数: stream :指向⼀个已打开⽂件的⽂件指针。
使⽤注意事项: • 清除错误标志: ◦ rewind 不仅移动⽂件指针,还会清除⽂件的错误标志和⽂件结束标志。
◦ 这意味着即使在读取⽂件时遇到了错误或到达了⽂件末尾,调⽤ rewind 后,⽂件状态会被 重置,可以重新开始操作。
• 与fseek 的区别:
◦ fseek(fp, 0, SEEK_SET) 只移动⽂件指针,不清除错误标志。
◦ rewind(fp) 移动⽂件指针并清除错误标志。
7.⽂件缓冲区
ANSI C 标准采⽤“缓冲⽂件系统” 处理数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为程 序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲 区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊ 到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲 区的⼤⼩根据C编译系统决定的。

7.1 fflush
int fflush ( FILE * stream );
根据上面缓冲区的知识可知,数据要先堆满缓冲区才会存储到硬盘中,
而fllush函数可以提前把数据输入到硬盘中,即刷新缓冲区,且程序关闭时也会刷新缓冲区
效果和flush相同。
注意事项: 1. 仅对输出流或更新流(最后⼀次操作为输出)有明确刷新⾏为
2. 输⼊流的刷新⾏为不可移植(如清空输⼊缓冲区是⾮标准特性)
3. 程序正常终⽌( exit )或调⽤ fclose 时会⾃动刷新,但程序崩溃时缓冲区数据可能丢失
#include <stdio.h>
#include <windows.h>
//VS2022 WIN11环境测试
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\\n");
Sleep(10000);
printf("刷新缓冲区\\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
可以通过调试观察到fllush函数的效果
这里就不演示了
8.更新⽂件
在⽂件的打开模式中有三种⽅式值得注意,分别是:"r+","w+","a+",分别是什么意思?

由于是可读和可写都可以实现,所以为了保证数据不丢失,我们在写完数据后,要及时fllush把数据存入文件,如何在继续读文件
关键要点:
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;
}
至此完结,耗费很长的时间制作,希望能对朋友们起到帮助,谢谢观看!




