目录
一、为什么要使用文件
程序运行时使用的数据是存储在内存中的,程序运行结束后内存就回收了,数据也不存在了。有时,我们想让数据长久保存,就需要使用文件,存储在磁盘上。
二、什么是文件
文件就是存放在外存上的文件。按功能分类,又分为两类:程序文件和数据文件。
(1)程序文件
在Windows系统上,程序文件包括:源程序文件(后缀.c),目标文件(后缀.obj),可执行文件(后缀.exe)。
(2)数据文件
文件内容是程序执行时使用的数据。在之前我们都是以终端作为输入输出的对象,本文讲述以数据文件作为输入输出的对象的方法。
(3)文件名
文件名就是文件的唯一标识。文件名结构:文件路径+文件名主干+文件后缀。
例如:D:code\test.txt
三、文本文件和二进制文件
数据文件按数据的组织形式,又分为文本文件和二进制文件。
程序运行时,将数据存在内存中(二进制形式),当需要将内存中的数据输出到外存上的数据文件中时,如果直接放在文件里,那么这就是二进制文件;如果先转换成ASCII码的形式,再放在文件里,那么这就是文本文件。
例如:程序运行时,使用了一个 int 类型的数据10000,在内存中以二进制形式存储。现将它输出到文件中,如果直接以二进制形式输出,则文件占4字节;如果先转换为ASCII码形式再输出,则文件占5字节。如下图所示:
运行以下代码,将 int 类型的数据1000 以二进制形式输出到 test.txt 文件,用二进制文件编辑器查看(内存中以小端方式存储):
补充:VS上,二进制文件打开方式如下:
四、文件的打开和关闭
(1)流
我们向不同的设备(屏幕、键盘、磁盘、U盘等)输入输出数据,需要进行不同的操作。为了方便程序员对各种设备进行操作,C语言抽象出了流的概念。我们只需要关心打开流,内存向流中读或写数据,至于各种设备与流之间是如何操作的,就是C语言底层解决的事情了。
(2)标准流
我们在使用scanf、printf、perror等函数向显示器/键盘输出/输入数据时,没有打开流,也没有进行其它的底层操作,照样成功地实现了内存与外部设备的数据读写,这是为什么?
这是因为,在程序启动的时候,C语言就会自动打开3个标准流:
- stdin: 标准输入流,大多环境中从键盘输入,scanf 就是从 stdin 中读数据。
- stdout: 标准输出流,大多环境是输出到显示器,printf 就是向 stdout 中写数据。
- stderr: 标准错误流,大多环境是输出到显示器,perror 就是向 stderr 中写数据。
标准I/O流的类型就是 FILE*,称为文件指针,它可以维护流的各种操作。因此,打开文件实际上就是打开流。
(3)文件指针
进行文件打开操作之后,系统会自动在内存中开辟一个相应的文件信息区,并存放文件的相关信息(包括文件名、文件状态、文件大小等)。这些信息存放在一个结构体变量里,这个结构体类型叫做 FILE,由编译器定义。不同的C编译器,定义的 FILE 结构体类型有所不同,但大同小异。如下,VS2013 在头文件 <stdio.h> 定义的 FILE:
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
我们通常是创建一个 FILE 指针,指向 FILE 结构体,从而间接访问文件信息区的文件信息,如下图所示:
(4)文件的打开和关闭
使用文件前,要打开文件;使用完后,要关闭文件。打开文件时,会返回一个 FILE 的指针,就建立了 FILE* 与文件的联系。
在 ANSI C 中规定 fopen 函数打开文件,fclose 函数关闭文件,函数原型如下:
//打开⽂件
FILE* fopen(const char* filename, const char* mode);
//关闭⽂件
int fclose(FILE* stream);
对于 fopen:
- filename 指向文件名字符串,mode 指向模式的字符串。
- 打开成功,返回文件信息区起始地址;打开失败,返回NULL。
- mode,所有打开模式如下:
文件打开模式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 输入数据,打开一个文本文件 | 出错 |
“w”(只写) | 输出数据,打开⼀个文本文件 | 建立一个新文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新文件 |
“rb”(只读) | 输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 输出数据,打开一个二进制文件 | 建立一个新文件 |
“ab”(追加) | 向二进制文件尾添加数据 | 建立一个新文件 |
“r+”(读写) | 读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 读和写,打开一个文本文件 | 建立一个新文件 |
“a+”(读写) | 向文本文件尾进行读写 | 建立一个新文件 |
“rb+”(读写) | 读和写,打开一个二进制文件 | 出错 |
“wb+”(读写) | 读和写,打开一个二进制文件 | 建立一个新文件 |
“ab+”(读写) | 向二进制文件尾进行读写 | 建立一个新文件 |
注意:
- “r”、“rb”、“r+”、“rb+”:如果指定文件不存在,会报错。
- “w”、“wb”、“w+”、“wb+”:如果指定文件不存在,会建立一个新的文件;如果指定文件存在,会覆盖原内容。
- “a”、“ab”、“a+”、“ab+”:如果指定文件不存在,会建立一个新的文件;如果指定文件存在,会在原内容末尾追加内容。
对于 fclose,文件关闭失败,返回 -1;文件关闭成功,返回 0。
示例代码及运行结果如下,文件关闭前,pf 存储的地址:
文件关闭后,pf存储的地址:
所以 fclose 后,系统只会收回内存空间的使用权,但 pf 还是指向的原来的空间,避免 pf 成为野指针,在文件关闭后,要将 pf 置 NULL。
因文件原本不存在,"w" 模式在指定路径下创建了新文件:
五、文件的顺序读写
(1)文件的顺序读写函数
函数名 | 功能 | 适用于 |
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件输入流 |
fwrite | 二进制输出 | 文件输出流 |
所有输入/输出流表示,可以是标准流(stdin、stdout、stderr),也可以是其它的流,比如文件流。
① fgetc
函数原型:
int fgetc ( FILE * stream );
参数:流的指针。
函数返回值:
- 读取成功:返回读取到的一个字符。
- 读取到文件末尾而失败:返回EOF(-1),并设置 end-of-file 标记。
- 读取错误而失败:返回EOF(-1),并设置 error 标记。
示例代码,读完整个文件:
② fputc
函数原型:
int fputc ( int character, FILE * stream );
参数:要写入的字符;流的指针。
返回值:
- 写入成功:返回写入的字符。
- 写入失败:返回 EOF(-1),并设置 error 标记。
示例代码,向文件写入字符 a~z:
③ fgets
函数原型:
char * fgets ( char * str, int num, FILE * stream );
参数:读取的字符串存入 str 指向的内存空间;读取的最大字符数量(包括终止符);流的指针。
返回值:
- 读取成功:返回 str。
- 读取到文件尾,但读取到了字符:返回 str,并设置 end-of-file 标记。
- 读取到文件尾,并没有读取到任何字符:返回 null ,并设置 error 标记。
- 发生错误:返回 null ,并设置 error 标记。
示例代码,读完整个文件:
④ fputs
函数原型:
int fputs ( const char * str, FILE * stream );
参数:str 指向要写入的字符串;流的指针。
返回值:
- 写入成功:返回一个非负值。
- 写入失败:返回 EOF(-1),并并设置 error 标记。
示例代码:
运行结果:
⑤ fprintf
函数原型:
int fprintf ( FILE * stream, const char * format, ... );
与 printf 函数对比:
int printf ( const char * format, ... );
fprintf 的参数仅仅是多了一个 stream。
返回值:
- 成功:返回写入的字符个数。
- 遇到错误:返回一个负数,并设置 error 标记。
- 遇到编码错误(多字节字符):返回一个负数,并设置 errno 标记为 EILSEQ。
示例代码:
运行结果:
⑥ fscanf
函数原型:
int fscanf ( FILE * stream, const char * format, ... );
相比 scanf 函数,参数仅仅多了一个 stream。
返回值:与 scanf 函数相似,参考【C语言】数据类型、变量、操作符、printf、scanf详解_c语言布尔型scanf-CSDN博客
示例代码及运行结果:
⑦ fwrite
函数原型:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
参数:ptr 指向被写的数组;size 是每个元素的大小,单位字节;count 是元素个数;stream 是流。
返回值:成功写入的元素个数。
示例代码:
运行结果(二进制编辑器打开):
⑧ fread
函数原型:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
参数:ptr 指向要读取到的空间;元素大小;元素个数;流。
返回值:成功读取到的元素个数。
示例代码及运行结果:
如果 fread 的返回值小于参数 count,那就意味着这是最后一次读了。
(2)函数的对比
① printf 和 scanf
printf -- 针对标准输出流(stdout)的,将数据以格式化的形式,输出到屏幕上。
scanf -- 针对标准输入流(stdin)的,从键盘上输入格式化的数据。
② fprintf 和 fscanf
fprintf -- 针对所有输出流的,格式化的输出函数。
fscanf -- 针对虽有输入流的,格式化输入函数。
③ sprintf 和 sscanf
sprintf -- 将格式化的数据转换成字符串。
sscanf -- 从字符串中提取出格式化的数据。
函数原型:
int sprintf ( char * str, const char * format, ... );
int sscanf ( const char * s, const char * format, ...);
与 printf 和 scanf 相比,多了一个指向字符串的指针参数。
示例代码及运行结果:
应用场景:序列化(将数据结构转化为字节流或字符串等)和反序列化(序列化的逆操作)。
反序列化:前端网页上的数据以字符串的形式传给后端,后端转换为结构体,就用到 sscanf。序列化:后端将结构体转为字符串展示到前端,用 sprintf。
六、文件的随机读写
(1)fseek
函数原型:
int fseek ( FILE * stream, long int offset, int origin );
功能:根据文件内容的光标位置和偏移量来定位文件的位置。
参数:流;offset 是偏移量,根据 origin 的光标位置计算;origin 是起始位置,有3个值:
- SEEK_SET:文件的起始位置。
- SEEK_CUR:光标的当前位置。
- SEEK_END:文件的末尾位置。
假如文件内容如下:
读取 a 后,不想读取 b,而是读取 d。首先读取一个 a ,光标后移 1 位。如果 origin 是 SEEK_CUR ,光标在当前位置 a 后不动,则偏移量是 2;如果 origin 是 SEEK_SET,光标移到开头,则偏移量是 3;如果 origin 是 SEEK_END,光标移到 f 后,则偏移量是 -3。
代码及运行结果如下:
(2)ftell
函数原型:
long int ftell ( FILE * stream );
功能:返回文件当前光标位置相对于文件起始位置的偏移量。
示例代码:
读取 d 后,光标移动到 d 后,相对于文件起始位置,偏移了 4 。
(3)rewind
函数原型:
void rewind ( FILE * stream );
功能:让光标位置回到文件的起始位置。
示例代码及运行结果:
七、文件读取结束的判定
(1)feof 的错误使用
feof 并不是用来判断文件是否读取结束的。
文件读取结束有两种原因:
- 遇到文件末尾结束(正常结束)。
- 文件读取失败结束。
feof 和 ferror 是在已经知道文件读取结束的前提下,文件读取结束的原因:
- feof:是否是遇到文件末尾而正常结束。
- ferror:是否是文件读取发生错误而结束。
feof 和 ferror 的原理:在文件读写的过程中,遇到文件末尾/错误,会设置文件末尾标记/错误标记。feof 就是检测文件末尾标记,而 ferror 就是检测错误标记。
(2)判断文件读取结束的方法
函数 | 文件未读取结束回值 | 文件读取结束返回值 | 判断文件读取结束方法 |
fgetc | 读取到的字符ASCII码 | EOF(-1) | 判断返回值是否为EOF |
fgets | 存储字符串的数组地址 | NULL | 判断返回值是否为NULL |
fscanf | 读取到的参数个数 | 0或EOF(-1) | 判断返回值是否小于要读的参数个数 |
fread | 读取到的元素个数 | 0 | 判断返回值是否小于要读取的个数 |
示例代码 1 及运行结果(处理文本文件):
示例代码 2 及运行结果(处理二进制文件):
八、文件缓冲区
ANSIC 标准采用 “缓冲文件系统” 处理数据文件,即系统自动地在内存中,为程序中每一个正在使用的文件,除了开辟文件信息区外,还开辟一块“文件缓冲区”。内存到磁盘的数据输出/输入,都先要送到“缓存区”,缓冲区的数据放满了,再送到目的地。如下图所示:
为什么要有缓冲区?
如果没有缓冲区,并且需要读/写很多数据,但是每读/写一点数据,就需要调用操作系统把数据从硬盘读出或者写入硬盘,这样频繁打断操作系统,就无法全局地为计算机上的其它程序服务,从而导致计算机的效率变慢。
以下 3 种情况满足一条,就会从缓冲区读出/写入数据:
- 缓冲区满。
- 手动刷新缓冲区。
- 关闭文件。
测试代码:
//VS2019 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");
//注:fclose在关闭文件的时候,也会刷新缓冲区,因此又睡眠了10秒,防止是 fclose 刷新的
Sleep(10000);
fclose(pf);
pf = NULL;
return 0;
}
一开始,用 fputs 写数据到文件 test.txt,因为缓冲区没有满,所以此时文件中并没有内容。使用 fflush 手动刷新缓存区,就会把数据从缓冲区取出写入文件中,这时文件有内容了。
结论:文件操作结束时,关闭文件非常重要,否则数据会丢失。