目录
4.1 对比一组函数:scanf/fscanf/sscanfprintf/fprintf/sprintf
1. 为什么使用文件
之前我们所有的c语言的操作都是在内存空间进行的,程序结束后内存中的数据就会消失(比如静态版本和动态版本的通讯录)。
这里涉及到了数据持久化的问题,我们一般数据持久化的方法有
a: 把数据存放在磁盘文件。
b: 存放到数据库等方式。
所以使用文件就是解决数据持久化的问题,通过使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2. 什么是文件
首先,磁盘上的文件是文件。
但在程序设计中分为程序文件和数据文件(是从文件功能角度来分析的)
2.1 程序文件
如源程序文件(vs中后缀为.c、.h)、可执行文件.exe、Windows环境下目标文件.obj等,可以运行的文件。
2.2 数据文件
即程序文件运行时需要从数据文件中读数据和写数据,如.text等。
此篇接下来讨论的就是基于数据文件。
C语言之前的讨论的是数据从键盘输入到内存,进行处理,再从内存输出到屏幕。
接下来就是讨论的是数据从数据文件读到内存,进行处理,再从内存写入内存。
2.3 文件名
文件名即一个文件唯一的文件标识,以便用户识别和引用。
文件名的组成:文件路径+文件名主干+文件后缀
c:\code\test.txt
文件标识常被称为文件名。
3. 文件的打开和关闭
3.1 文件指针
对数据文件,每次被使用时,都会在内存中开辟一个属于自己的信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
FILE* pf;文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

3.2 文件的打开和关闭
文件在读或写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose函数来关闭文件。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//第一个参数为数据文件名
//第二个参数为文件打开方式
//返回成功,返回值为FILE*类型的指针,指向该文件;返回失败会返回NULL;
//关闭文件
int fclose ( FILE * stream );
//参数即为打开文件时创建的FILE*类型指针;使用关闭文件函数后记得将指针置为NULL,以免出现野指针。
fopen函数中第二个参数如下:
| 文件使用方式 | 含义 | 如果指定文件不存在 |
| “r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
| “w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
| “a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
| “rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
| “wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
| “ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
| “r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
| “w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
| “a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
| “rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
| “wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
| “ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
更多请参考cpp网站:Reference - C++ Reference
使用实例:
int main()
{
//打开文件
//相对路径 此时打开方式是“w”,内存写入数据到文件,因为是相对路径,
//所以会在工程文件目录下创建test.txt
//FILE* pf = fopen("test.txt", "w");
//绝对路径 此时打开方式是“w”,内存写入数据到文件,因为是绝对路径,
//所以会在c:\code\中创建test.txt
FILE* pf = fopen("c:\\code\\test.txt", "w");
//因为fopen打开文件失败会返回空指针,所以使用fopen函数时,
//要对FILE*类型的返回值的进行非NULL判断
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
//关闭文件后要对FILE*指针进行置空,防止野指针出现。
fclose(pf);
pf = NULL;
return 0;
}
数据文件使用三部曲中现在我们已经会了打开文件和关闭文件,接下来就是使用文件,即对文件的读写。
4. 文件的顺序读写
文件的读写可以理解为以内存为主体,
内存从文件中读取数据,方向是从文件向内存中输入,所以称为读;
内存将自己的数据写入文件,方向是从内存向文件输出,所以称为写。

fputc 字符输出函数(写)
int fputc ( int character, FILE * stream );
//参数1为想写入文件的字符,或者其相应的ASCII码。
//参数2为打开文件时返回的FILE*指针变量
//成功会返回字符相应的ASCII码值;写入失败会返回EOF,可以用ferror查看
使用实例:

fgetc 字符输入函数(读)
int fgetc ( FILE * stream );
//参数为fopen成功后返回的FILE*指针,指向目标文件
//如果读取到文件中的文件结束标志,即EOF,表明文件已经全部读取结束,则返回EOF,
//并且可以通过feof查看
//如果读取出现错误,返回EOF,并可以在perror中查看
//如果读取成功,返回相应字符的ASCII码值。
使用实例:

fputs 文本行输出函数 (写)
int fputs ( const char * str, FILE * stream );
//参数1为要写入文件的字符串起始指针
//参数2为打开文件时成功返回的FILE*类型指针,指向要写入的文件。
//成功返回值为一个非负数,失败会返回EOF,可通过perror查看。
使用实例:
fgets 文本行输入函数 (读)
char * fgets ( char * str, int num, FILE * stream );
//参数1是从文件读取的字符串拷贝放在内存中的起始地址
//参数2是要读一行里的几个字符,其中包括了最后一个自动添加的‘\0’,即如果需要读5个字符,
//但最后实际上只读进4个,最后一个是\0.所以实际需要5个的话,num需要写6;
//若num大于一行里的字符个数,则会读完整行包括\n,然后最后添加一个\0.
//参数3是从哪个文件读取字符串的信息,是打开文件成功时返回的FILE*类型指针.
//成功则返回str指针;若在尝试读取任何字符前就读到了EOF,返回NULL,可通过feof查看;
//若读取过程中发生错误,返回NULL,可通过perror查看
使用实例:
fprintf 格式化输出函数 (写)
当需要同时写入不同类型的信息时,如将结构体的信息写入文件,就可以用到fprintf。
int fprintf ( FILE * stream, const char * format, ... );
//参数1,将格式化数据写入到FILE*类型的指针指向的文件中
//参数2,详见https://legacy.cplusplus.com/reference/cstdio/fprintf/?kw=fprintf
//成功,返回值为写入的总个数;失败的话,会返回一个负数
使用实例:

fscanf 格式化输入函数 (读)
int fscanf ( FILE * stream, const char * format, ... );
//第一个参数,表示从哪个文件读取信息,用fopne成功时返回的FILE*类型的指针
//第二个参数,参考https://legacy.cplusplus.com/reference/cstdio/fscanf/?kw=fscanf
//成功时返回值为成功读取到数据的个数,失败返回EOF.
使用实例:

fwrite 二进制输出 (写)
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
//参数1从数据哪个位置开始写入文件
//写入数据的单位大小
//写入数据的总数量
//写入到哪个文件类型的流中
//成功返回实际成功写入数据的个数,如果num 或者size有一个为0,则返回零,其他错误可以通过perror查看
使用实例:

fread 二进制输入 (读)
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
//参数1读取的信息需要存放的位置
//读取信息的类型大小
//读取信息的总个数
//从哪个文件类型的流中读信息
//成功返回读取到信息的个数,失败时不等于应该正确读到信息的个数;若返回0,说明size
//或num为0
使用实例:

对于任何一个C程序,只要运行起来就会默认打开3个流:
stdin——标准输入流==键盘
stdout——标准输出流==屏幕
stderr——标准错误流==屏幕
这三个的类型都为FILE*,因此可以用fscanf和fprintf实现scanf和printf的效果。

4.1 对比一组函数:
scanf/fscanf/sscanf
printf/fprintf/sprintf
scanf :按照一定的格式从键盘输入数据
fscanf: 按照一定的格式从文件类型的流(如文件/stdin)输入数据
sscanf: 按照一定的格式从字符串中读取并输入数据
printf:按照一定的格式把数据输出到屏幕上
fprintf: 按照一定的格式把数据输出到文件类型的流(如文件/stdout)
sprintf: 按照一定的格式将数据输出到字符串中
int sprintf ( char * str, const char * format, ... );
//参数1,写入的数据放到字符串的地址
//参数2, 写入那些格式化的数据,详见https://legacy.cplusplus.com/reference/cstdio/sprintf/?kw=sprintf
//成功返回值为成功写入的字符的数量,失败则返回值为数量少于应写入数据的个数,甚至为0;如果一个数据都没有写入则返回值为EOF。
int sscanf ( const char * s, const char * format, ...);
//参数1从哪个字符串的地址开始读数据
//参数2需要写入哪些格式化的数据
//成功返回值为成功读取到的字符的数量,失败则返回值为数量少于应读到数据的个数,甚至为0;如果一个数据都没有读到则返回值为EOF。
scanf与sprintf的使用实例:
5. 文件的随机读写
5.1 fseek
int fseek ( FILE * stream, long int offset, int origin );
//根据文件指针的位置和偏移量来定位文件指针
//https://legacy.cplusplus.com/reference/cstdio/fseek/?kw=fseek
通过规定一个起始位置的偏移量来移动文件指针的位置,已达到想读取任意位置数据的目的。
使用实例:

5.2 ftell
long int ftell ( FILE * stream );
//get current position in stream
//返回文件指针相对于起始位置的偏移量
使用实例:
5.3 rewind
void rewind ( FILE * stream );
//Set position of stream to the beginning
使用实例:
6. 文本文件和二进制文件
根据数据在数据文件的组织形式分为文本文件和二进制文件
二进制文件:即数据在内存中以二进制的形式存储,如果不加转换的输出到外存文件。
文本文件:以ASCII字符的形式存储的文件。
如整型10000 可以是以二进制形式存入,只占四个字节;
也可以是字符形式存入,每一位看成一个字符,占五个字节。

可在编译器查看区别:
7. 文件读取结束的判定
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。与ferror配合使用。
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread 判断返回值是否小于实际要读的个数。
fgetc 如果读取正常,会返回读取到的字符的ASCII值
如果读取失败,返回EOF
fgets 如果读取正常,返回的是存放读取到的数据的地址
如果读取失败,返回的NULL
fscanf 如果读取正常,返回的是格式串中指定的数据的个数
如果读取失败,返回的是小于格式串中指定的数据的个数

8. 文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,
所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。

关闭文件的时候会刷新缓冲区,将缓冲区中的数据拿出来存入文件。
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。