文章目录
前言
路漫漫其修远兮,吾将上下而求索。
一、为什么要使用文件
在博主前一篇的文章中实现了通讯录的功能(PS:下一篇,如果没看到那么便还在创作中……),当通讯录运行起来的时候,可以为向通讯录之中添加联系人、删除联系人、查找联系人……,此时”联系人“的相关数据是存放在内存中的,而倘若没有将程序中的数据写入文件,那么当此程序退出的时候,在程序中输入的数据自然也就不在了,再次运行此程序的时候需要重新输入数据;显然,没有文件的通讯录失去了其”灵魂“;
在实现通讯率功能的时候,我们想将在程序中输入”联系人“的数据保存下来,只有我们自己选择删除数据的时候,数据才会不在,即当退出程序的时候,输入的”联系人“的信息仍然存在;这便会涉及数据持久化的问题;
让数据持久化的方式有:将数据存入磁盘文件、数据库等方式;
此处使用文件便可以将数据存放到磁盘文件之中,从而做到数据的持久化;
注:数据库只是封装了一层,其最终底层的数据还是会保存到某些文件中去的;
二、什么是文件
磁盘(硬盘)上的文件是文件
但是在程序设计之中,我们谈的文件一般有两种:程序文件、数据文件(从文件功能的角度来分类)
(一)、程序文件
程序文件包括源文件(后缀为 .c)、目标文件(windows环境下后缀为 .obj)、可执行程序(windows 环境下后缀为 .exe)
(二)、数据文件
数据文件中的内容不一定是程序,而是程序中运行时读写的数据;比如程序运行需要从文本读取数据的文件、或者输出的文件;
例如我们写了一个test.c 文件,当要运行test.c 文件的时候便会将数据写入B文件中,或者从A文件中读取数据;而此时,数据需要将程序中的数据写入文件中去或者从文件中获取数据到程序中,那么此时的A文件与B文件便是数据文件;如下图所示:
将程序test.c 中的数据写入B文件中,实际上是将存储在内存中的数据输出到文件之中;而程序test.c将A文件中的数据读到程序test.c 中,实质也是将文件A中的数据读到内存之中;
实际在前面学习的过程中,我们也接触到过输入输出,只不过是将键盘上的数据输入到内存中,或者将内存中的数据输出到屏幕上;
(三)、文件名
一个文件要有唯一的标识,以便用户识别和引用;
但是在生活中,你可能会发现,在不同路径下可以存在相同名称的文件,故而想要使得一个文件的文件名唯一,那必然是需要加上文件路径的;
即文件标识:文件路径 + 文件主干名 + 文件后缀
例如 : c:\code\test.txt
而为了方便起见,又将文件标识(文件标识:文件路径 + 文件主干名 + 文件后缀)称之为文件名;
三、二进制文件与文本文件
根据文件的组织形式,数据文件被分为文本文件或者二进制文件;
- 文本文件:内存中的数据是以二进制的形式存储的,如若再将此数据输出到外存之前,将二进制的数据转换成ASCII字符的形式 输出的文件。
- 二进制文件:内存中的数据以二进制的形式存储的,不加以转换直接将此二进制的数据输出到外存;
注:字符一律都是以ASCII的形式进行存储的,而数值型的数据既可以用ASCII的形式存储又可以使用二进制的形式进行存储;
四、文件的打开和关闭
已知存在一个文件,倘若我们想使用该文件,那么我们改正么做呢?
想必你肯定听过将大象装进冰箱需要三步;即第一步,打开冰箱门;第二步,将大象装进冰箱;第三步,关闭冰箱门;
同理,使用文件,也有三步,第一步,打开文件;第二步,操作文件;第三步,关闭文件;
(一)、文件指针
缓冲文件系统中,关键概念就是“文件类型指针”,简称“文件指针”;
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件的状态以及文件当前的位置等);这些信息保存在一个结构体变量之中;而该机构体类型是由系统声明的,取名为FILE
平时我们在写代码的时候,引用的头文件也是文件;
下段代码便是 VS编译器中,stdlio.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类型的结构体变量,并自动填充其中的信息;
你有可能会有疑问,文件信息中也是有数据的,想要存放必定是要占用内存空间的,那么文件信息区究竟是存放在内存中是如何被维护的呢?
- 像前面学习的数组,本质我们只要知道该数组的起始地址,元素类型,元素个数便就可以访问并使用该数组存储在空间中的数据;同理,想要维护文件信息区,我门只要知其地址便好了,而文件信息区的类型为FILE ,故而其地址的类型为FILE*,即用FILE*的指针对文件信息区进行维护即可;
FILE* pf ;// 文件指针变量
定义的pf 是一个指向文件信息区的指针变量,而我们通过文件信息区便又可以访问该文件,即通过文件指针变量我们便能找到该指针变量对应的文件;
如上图所示,你若想要打开位于磁盘上的文件A,那么既可以直接在磁盘上打开该文件;
倘若你想在程序中打开在硬盘中的文件,那么便会通过文件指针变量找到该文件;
(二)、文件的打开和关闭
从上文知识中我们大体能感觉到倘若要在程序中打开文件就得利用FILE*的指针,那么可能便会猜想使用库函数打开文件定然也会与FILE*的指针变量有关;
我们再回顾一下,想要操作文件有三步:打开文件、操作文件、关闭文件;
ASCIN 规定使用fopen 来打开文件,使用fclose 来关闭文件;
fopen:
- fopen
- 功能:打开文件
- 引用头文件 <stdio.h>
- FILE* fopen( cnost char* filename ,const char* mode);
- 参数都是字符串,故而均要使用双引号;第一个参数为 filename, 为文件名,文件名:文件路径+文件主干名+文件后缀;倘若此文件就在给程序的根目录下,便不需要写文件路径,即只需要写文件主干名+文件后缀 (要注意使用转义字符!);否则,便要将文件名写全;第二个参数为指令,即以什么样的形式打开此文件;
- 返回类型为FILE* ,当打开此文件成功,便会返回FILE*的指针变量,即操作系统为此文件创建的文件信息区的地址;倘若失败,则会返回NULL;
- 由于打开文件也会失败,故而在操作文件之前还要对返回的指针进行判空;
注:fopen 的第二个参数 const char* mode;
文件使用方式 | 含义 | 如果该指定文件不存在 |
"r"(只读) | 为了将文本中的数据输入到内存中(输入数据),打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了将内存中的数据输出到文本二文件中(输出数据),而打开一个文本文件 | 创建一个新的文件 |
"a"(追加) | 将内存中的数据追加到文本文件的尾端,而打开一个文本文件 | 创建一个新的文件 |
"rb" (二进制读) | 为将二进制文件中的二进制数据输入到内存中,而打开一个二进制文件 | 出错 |
"wb" (二进制写) | 为了将内存中的二进制数据输出到二进制文件中,而打开的一个二进制文件 | 创建一个新的文件 |
"ab" (二进制追加) | 将内存中的二进制数据追加到二进制文件的尾端,而打开的一个二进制文件 | 创建一个新的文件 |
"r+"(读写) | 为了将文本中的数据输入到内存中(输入数据),将内存中的数据输出到文本二文件中(输出数据),而打开一个文本文件 | 出错 |
"w+"(读写) | 为了将文本中的数据输入到内存中(输入数据),将内存中的数据输出到文本二文件中(输出数据),而建立一个新的文本文件 | 创建一个新的文件 |
"a+"(读写) | 为了在此文件的尾端进行读和写,而打开一个文本文件 | 创建一个新的文件 |
"rb+"(读写) | 为了将文本中的二进制数据输入到内存中(输入数据),将内存中的二进制数据输出到文本二文件中(输出数据),而打开一个二进制文件 | 出错 |
"wb+" (读写) | 为了将文本中的二进制数据输入到内存中(输入数据),将内存中的二进制数据输出到文本二文件中(输出数据),而建立一个新的二进制文件 | 创建一个新的文件 |
"ab+" (读写) | 为了在此文件的尾端进行读和写,而打开一个二进制文件 | 创建一个新的文件 |
如若你用读的形式打开文件便就不能写,同样的,你若用写的形式打开便就只能写而不能去读;
注:以写、追加的形式打开文件,如若未找到该文件便会自己创建一个文件;而倘若以读的形式打开文件,如若没有该文件便会报错;
例1:以读的形式打开文件而在根目录底下未有此文件:
例2:以读的形式打开在桌面上的文件
打开文件的图解流程:
fclose
- fclose
- 功能:关闭文件
- 引用头文件 : <stdio.h>
- int fclose ( FILE* stream);
- 参数类型为FILE* stream, 指向文件信息区的指针;
- 返回类型为int ,关闭文件成功便会返回0,倘若失败则会返回EOF;
打开文件与关闭文件的代码如下:
注:在关闭文件之后要将该指针置空, 避免野指针;
文件在打开之后,不再使用时需要将文件关闭,原因在于:
- 1、文件也是资源,当你打开一个文件并且使用之后,最后反而并没有关闭此文件,那么此文件中的资源便相当于释放了;(文件是资源,一个程序打开的文件个数是有限的)
- 2、如若在文件中写数据,而没有关闭文件,便有可能回丢失数据;
五、文件的顺序读写
前言:
相关函数:
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输入流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输入流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输入流 |
二进制输入函数 | fwrite | 文件 |
二进制输出函数 | fread | 文件 |
什么是流?
- FILE* 类型的指针便称为"流";针对文件、键盘、显示器(屏幕)、网络、U盘、软盘、光盘、打印机等外部设备上的数据的读写都是通过 '流' 来进行的;
例如,我们在使用scanf 与 printf 会接触到标准输出流以及标准输入流,其具体作用由下图所示:
什么是标准流?
标准流有三个,分别为:标准输入流(stdin),标准输出流(stdout),标准错误流(stderr);
- stdin——标准输入流(standard stream): 用于读取普通输入的流。在大多数环境中为从键盘输入。scanf 与 getchar 等函数会在此流(stdin)中读取字符,即从键盘上获取数据;
- stdout——标准输出流(standard output stream): 用于写入普通输入的流;在大多数环境中为输出至显示器(屏幕)上;printf 、puts 与 putcar 等函数会向这个流中写入字符,即将内存中的数据写到显示器上;
- stderr——标准错误流(standard error stream); 用于写出错误的流;在大多数环境下为输出至显示器(屏幕)上;
而标准流 stdin、stdout 、stderr 都是指向文件信息区的FILE*类型的指针;任何一个C程序运行起来均会默认将这三个流打开,故而在使用用printf、puts、putchar、scanf、getchar 等,不需要像操作文件那样,在对文件进行操作的时候还得打开文件(比如你若想从键盘上获取数据或者将数据打印到屏幕上去,并没有要求你利用库函数来打开键盘或者打开屏幕,取而代之的是只要你使用相应输入、输出的函数指明所要传递的数据即可即可);
注:此处讨论的输入、输出均是以 内存 作为主体,而其面临的对象非常多样,可以是键盘、屏幕(显示器)、硬盘、软盘、U盘、光盘等;而流,就像一个“数据缓冲区”;
为什么存在流?
- 不同的外部设备其结构不同,那么写入的数据的方式便会有所差异;而,在没有流的情况下,程序员想要将数据写入到不同的外部设备之中,就要去了解对应设备的细节,如此的话,你可以想象,程序员不仅要学习程序相关的知识,还要将这些外部设备的细节了解清楚,那么程序员的学习负担便非常之大,为了解决这一问题,于是乎便设计了"流",即在读写数据的中间封装了一层;
- “流”便像水流一样(将数据的流动想象为水流),你在将数据写入外部设备的时候,并不是利用自己的程序将数据直接写到外部设备,而是将自己的数据写到“流”之中,然后流里面的数据再由C语言底层实现,写入外部设备之中;故而程序员只要关注此流即可,其他的便就不用去关注,即"流"造福于程序员,便利了程序员编写代码;
(一)、fputc
- fputc
- 功能:将内存中的数据输出到文件之中
- 适用于所有输出流
- 所要引用的头文件: <stdio.h>
- int fputc( int character , FILE* stream);
- 其第一个参数的类型为int, 以表示所要写入文件之中字符的ASCII码值;第二个参数类型为FILE*,即为指向文件信息区的一个指针;
- 返回类型为int ,操作成功便会返回输出字符的ASCII码值,否则便会返回EOF;
操作如下:
在test.txt 文件中查看数据的输出效果如下:
注:函数fputc 是将内存中的数据写入文件中,故而在使用fopen 函数的时候应该以写的形式 即利用指令"w" 打开;
由于fputc 适用于所有输出流,同样,我们可以利用fputc 将字符输出到标准输出流--> 屏幕:
(二)、fgetc
- fgetc
- 功能:在流中获取字符,流 即为指向文件信息区的指针;将文件中的数据输入到内存之中;
- 适用于所有的输入流
- 包含的头文件: <stdio.h>
- int fgtec(FILE* stream);
- 其参数类型为 FILE* ,即指向该文件文件信息区的指针
- 返回类型为 int ,在文件中获取成功输入内存之中,便会返回该字符的ASCII码值;倘若失败则会返回EOF;
此函数的使用如下图所示:(因为在上例中使用 函数fputc在文件中放入了a~z 的字符 )
现test.txt 文件中的内容如下:
注:因为函数fgetc 是将文件中的数据读入到内存中,故而在使用函数 fopen 打开文件要以读的形式打开,即利用指令 "r"
由于fgetc 适用于所有输入流,那么便可以实现利用fgetc 从标注输入流(键盘)中获取字符,使用如下图:
(三)、fputs
- fputs
- 功能:将字符串写入到文件之中
- 适用于所有输出流
- 所要包含的头文件: <stdio.h>
- int fputs ( const char * string , FILE* stream);
- 其第一个参数的类型为char * ,代表着为字符串,而const 加以修饰则说明该字符串只能被使用而不可被修改;第二个参数的类型为 FILE* ,意为指向文件信息区的指针;
- 其返回类型为int , 如若操作成功便会返回一个非负值,否则便会返回EOF;
使用如下:
注:每一次以"w"的形式打开文件,便会将文件中的原数据给清除以放入新数据;
打开根目录下的 test.txt 文件如下图:
由于fputs 适用于所有输出流,那么便也可以使用 fputs 将文本行输出到屏幕上,使用如下图所示:
(四)、fgets
- fgets
- 功能:将文件中的字符串读入放入到内存之中;
- 适用于所有输入流
- 所要引用的头文件 : <stdio.h>
- char* fgets (char* str , int num , FILE* stream);
- 其第一个参数的类型为 char* ,表示在此程序中用来存放在文件中读取的字符串的字符数组的地址;第二个元素的类型为 int, 表示要在文件中读取的字符的最大个数(包括'\0');其第三个参数的类型为FILE*,即为指向该文件的文件信息区的指针;
- 返回类型为char*, 倘若操作成功则会返回此数组的地址,否则则会返回NULL;
此时文件test.txt 中的内容为下图所示:
使用函数fgets 如下:
由于fgets 适用于所有输入流,那么便可以实现利用fgetc 从标注输入流(键盘)中获取字符,使用如下图:
(五)、fprintf
- fprintf
- 功能:将内存中格式化的数据输出到流中,即将内存中的数据输出到文件中;
- 适用于所有的输出流
- 所要引用的头文件: <stdio.h>
- int fprintf(FILE* stream, const char* format,...);
- 对比我们非常熟悉的printf --> int printf( const char* format,...); ,显然如何使用fprintf 便就非常清楚了;
- 其返回类型为int ,如若操作成功便会返回所输出的字符的个数,否则返回负值;
使用如下图:
在该程序下打开test.txt 文件:
由于fprintf 使用于所有的输出流,那么便也可以使用 fprintf 将格式化数据输出到屏幕上,使用如下图所示:
(六)、fscanf
- fscanf
- 功能:将文件中格式化的数据输入内存中
- 适用于所有输入流
- 所引用的头文件 : <stdio.h>
- int fscanf( FILE* stream , const char* format,...);
- 其返回类型为int ,如若操作成功,便会返回成功从文件中读取项目的变量个数,否则返回EOF;
文件test.txt 中存放的数据如下图:
使用的代码如下:
#include<stdio.h>
struct Peo
{
char name[10];
int age;
}s;
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//操作文件
int ret = fscanf(pf, "%s %d", s.name, &s.age);
printf("%s %d\n", s.name, s.age);
printf("%d\n", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码运行结果如下:
由于fscanf 适用于所有输入流,那么便可以实现利用fscanf 从标注输入流(键盘)中获取格式化数据,使用如下图:
接下来的两个函数 fwrite 与 fread 只能适用于文件,并且在打开文件只能以二进制的形式打开,即利用指令"wb","rb"; b -> binary 二进制 ;
二进制写入与文本的写入有什么区别?
- 一般情况下,以将数据以文本的形式写入文件,此文件中的数据我们能看懂;而以二进制的形式将数据写入文件,有可能我们便看不懂了;
- 文件的大小会有差异;以二进制的形式将数据写入文件,其文件的大小相较于以文本的形式将同样的数据写入文件中会小一些(当然,这不是绝对的,只是在大多数情况下是这样的);
(七)、fwrite
- fwrite
- 功能:将存放在内存中数据以二进制的形式写入流之中
- 只能适用于文件
- 所要引用的头文件: <stdio.h>
- size_t fwrite (const void * ptr , size_t size , size_t count, FILE* stream);
- 其第一个元素的类型为 const void* ,意为所要输出的在内存中存储的数据;第二个参数的类型为size_t,即 unsigned int ,代表这一个元素的大小,单位是字节;第三个参数的类型为size_t ,意为所要输出的元素个数;第四个参数的类型为FILE*,代表着指向文件信息区的指针,即流;
- 返回类型为 size_t,如若操作成功便会返回成功输出的元素个数,否则返回0;
使用的例子如下图所示:
打开 test.txt 文件来看一下:
为什么我们看不懂这些字符?
因为在test.txt 中存储的是二进制的数据,但是我们查看文本中的内容时是以文本的形式打开该文件;
以二进制的形式来打开此文件:
(八)、fread
- fread
- 功能:将从流中读取的二进制数据存放到内存之中
- 只适用于文件
- 所要引用的头文件 : <stdio.h>
- size_t fread (const void* ptr , size_t size , size_t count , FILE* stream);
- 其第一个参数的类型为 void* ,以表示在内存中存放此二进制数据空间的起始地址;第二个参数的类型为 size_t ,意味着所要读取的一个元素的大小;第三个参数的类型为 size_t, 代表着所要读取的元素个数;第四个参数的类型为 FILE*, 代表着指向文件信息区的指针,即流;
- 返回类型为size_t,如若操作成功便会返回成功读取的元素的个数,否则返回0;
此时文件test.txt 中存储的是数据如下图:
使用案例如下图所示:
六、文件的随机读写
看了上文,你会发现,当我们利用fgetc 读取文件中给的字符时,程序每一此启动,fgetc 均只能读到第一个字符;多写几个 fgetc 才能读到后面的字符;
倘若文件test.txt 中的数据如下:
使用一次fgetc 读取文件test.txt 中的字符:
第一次运行:
第二次运行:
为什么每次运行的结果一样呢?
- 因为默认文件打开的时候,文件指针是指向此文件中数据最开始的位置;每次启动程序,文件指针均会回到起始位置,故而每次运行程序fgetc 所得到的字符都一样:
利用多个fgetc 读取文件test.txt 中的字符:
为什么多次使用fgetc 能往后获取字符呢?
- 因为当利用fgetc 获取到一个字符的时候,文件指针便会指向下一个字符的位置……,fgetc 是按照顺序一个一个往后读字符的;
倘若你想要不按照顺序读取字符,下面的函数便可以实现此功能;
(一)、fseek
- fseek
- 功能: 根据文件指针的位置和偏移量来定位文件指针
- 只适用于文件
- 所要引用的头文件 :<stdio.h>
- int fseek( FILE* stream , long int offset ,int origin);
- 第一个参数的类型为FILE* ,代表着流;第二个参数的类型为long int ,意为偏移量,即将文件指针移动到距离当前文件指针偏移多少个字符的位置上;第三个参数的类型为int ,代表着当前文件指针的位置;
- 返回类型为int ,如若操作成功便会返回0,否则返回非0值;
当前文件 test.txt 中的数据如下图:
利用fseek 与 fgetc 读取偏移起始位置2、4的字符,代码与输出结果如下:
(二)、ftell
- ftell
- 功能:获取当前文件指针距离起始位置的偏移量
- 所要引用的头文件 :<stdio.h>
- long int ftell (FILE* stream);
- 参数类型为FILE* ,代表着流;
- 返回类型为 long int ,意为返回当前文件指针距离起始位置的偏移量;
(三)、rewind
- rewind
- 功能:让当前文件指针回到文件的起始位置处
- 所要引用的头文件: <stdio.h>
- void rewind (FILE* stream);
- 参数为流;
- 无返回类型;
使用例子如下:
七、文件读取结束的判定
当文件读取结束的时候,可能是因为遇到错误而停下,也有可能是因为遇到文件结尾而结束;
文件结束的标志:
- 文本是否读取结束,判断返回值是否为EOF(fgetc, fscanf),或者NULL(fgets)
- 二进制文件的读取结束的判断,要去判断其返回值是否小于实际要读取的个数 eg. fread 判断返回值是否要小于实际要读的个数;(倘若小于,那么此次读取时此文件的最后一次读取);
如何判断呢?
此处便会利用到两个函数: ferror 与 feof
ferror
- ferror
- 功能:判断文件的结束是不是因为遇到错误而结束
- 所要引用的头文件: <stdio.h>
- int ferror (FILE* stream);
- 其参数是流
- 返回类型为int, 意味着如果该文件是因为错误而结束的,那么便会返回一个非0值,否则便会返回0;
feof
- feof
- 功能:判断该文件的结束是不是因为遇到该文件的结尾而结束的
- 所要引用的头文件: <stdio.h>
- int feof (FILE* stream);
- 其参数为流
- 其返回类型为int ,倘若该文件是因为遇到文件结尾结束的便会返回一个非0值,否则返回0;
注:ferror 与 feof 通常是一起使用的
所要读取的文件test.txt 中的内容:
使用例子代码如下:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//将数据写入文件
char ch[15] = "chongqing";
size_t sz = strlen(ch) + 1;
fwrite(ch, 1, sz, pf);
//关闭文件
fclose(pf);
pf=NULL;
//在文件中读取数据
//打开文件
pf = fopen("test.txt", "rb");
size_t ret_code = fread(ch, 1, sz, pf);
//判断文件读取结束的原因
if (ret_code == sz)
{
puts("Array read successfully ,contents:");
printf("%s\n", ch);
}
else
{
if (ferror(pf))
printf("Array reading test.txt : unexpected end of file\n");
else if (feof(pf))
printf("End of file reached successfully\n");
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码运行结果如下:
八、文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用地文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,当此缓冲区被装满的时候操作系统会将这些数据一起送到磁盘上;如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,此内存缓冲区被放满之后,操作系统才会将缓冲区中的数据送到程序数据区;其中,缓冲区的大小根据C编译系统决定的;
数据的输入输出并未都是直接操作的,而是先让如数据缓冲区之中;
为什么存在缓冲区?
- 缓冲区的存在是为了提高操作系统的效率而设计的;
什么是系统调用?
- 所谓系统调用就是操作系统提供的接口;(操作系统会帮我们做一些事)
缓冲区的存在为什么提高了操作系统的效率?
- 我们在读、写数据的时候会让操作系统调用此函数接口来帮我们做一些事的话,那么倘若我们频繁地读、写数据便会频繁地打断操作系统,数据缓冲区的存在会让数据在缓冲区中存满了数据然后让操作系统一次性地将数据输入或者输出,从而避免频繁地打断操作系统;
缓冲区的大小取决于编译器;对于不同的编译器其缓冲区地设定不同;
缓冲区还分为无缓冲区、行缓冲区、全缓冲区……(当然,缓冲区的大小也是可以被设置地,有一些库函数专门用来设置缓冲区的大小)
证明缓冲区确实存在:
思考:我们利用输入函数(例如:fputc),再让操作系统延迟操作,我们趁这段延迟的时间打开文件,看文件中是否有数据;倘若没有,则证明有缓冲区;反之则无;
所要使用的相关函数:
- Sleep --> Sleep函数可以使计算机程序(进程,任务或线程)进入休眠,使其在一段时间内处于非活动状态; 参数的单位是毫秒;
- fflush --> int fflush ( FILE * stream ); 刷新流,会将缓冲区的数据刷新到内存、外存中;
注:会利用到fflush 的原因是 fclose 关闭文件时会刷新缓冲区;
代码如下:
#include<stdio.h>
#include<windows.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("perror");
return 1;
}
//操作文件
fputc('a', pf);
printf("睡眠10s,已经开始写数据了\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);
Sleep(10000);
printf("再睡眠10s \n");
Sleep(10000);
//关闭文件
fclose(pf);//fclose 关闭文件也会刷新缓冲区
pf = NULL;
return 0;
}
第一次10s 休眠:
第二次10s 休眠:
从上述代码中,可以证明缓冲区的存在,但是在不同的编译器上此文件缓冲区的设置是有所差异的;
在此处可以得到一个结论,因为有缓冲区的存在,需要做刷新缓冲区或者在文件操作时关闭文件才能确保数据能完全地输入或者输出;这点便可以加深之前的理解,为什么在打开文件操作完文件之后要记得关闭文件而避免数据的丢失;
流和缓冲区之间有什么关系?
- 流 是一种设计思想;
- 当数据需要从输入流中读取到内存中时,可以将数据转移到缓冲区中进行暂存。 当数据需要从外存写入到输出流中时,可以先将数据存放在缓冲区,当缓冲区满了之后,再一次性写入到输出流。 这样可以避免频繁地打断操作系统;
总结
1、有关输入的库函数:
- 适用于所有输入流的函数:fgetc、 fgets、fscanf
- 只适用于文件输入流的函数: fwrite
2、有关输出的库函数:
- 适用于所有输出流的函数:fputc、fputs、fprintf
- 只适用于文件输出流的函数: fread
3、想要判断文件读取结束的原因是什么会利用到两个函数: ferror 与 feof
4、流,FILE* 类型的指针便称为"流";针对文件、键盘、显示器(屏幕)、网络、U盘、软盘、光盘、打印机等外部设备上的数据的读写都是通过 '流' 来进行的;
5、数据的输入输出并未都是直接操作的,而是先让如数据缓冲区之中;缓冲区的存在是为了提高操作系统的效率