🥑存储在硬盘上的数据信息集合就叫做文件(file),也称为计算机文件。而且文件是以硬盘作为载体的,文件有多种类型,比如文本文件(.txt)、图片(.jpg)、压缩文件(.zip)、Excel电子表格(.xls)等等。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
★ 程序文件包括源程序文件(后缀为.c),目标文件(windows环境下后缀为.obj),可执行程序(windows环境后缀为.exe)。
★ 文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
🍍🍍🍍我们平常所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3部分:文件路径+文件名主干+文件后缀。例如:
上面就是一个文件名,紫色部分就是文件路径,红色部分是文件名主干,最后面的绿色部分(包括点)就是文件后缀。
为了方便起见,文件标识常被称为文件名。
🥝🥝如果没有文件,我们写的程序数据是存储在电脑的内存中的,如果程序退出,那内存就会回收,数据就会丢失,等再次运行程序时,是看不到上次程序的数据的。所以如果要将数据进行持久化的保存,我们就可以使用文件。
🥥根据数据的组织形式,数据文件又被分为文本文件或者二进制文件。
✿ 数据在内存中以二进制的形式存储,如果不加转换的输出到外存(硬盘)的文件中,就是二进制文件。
✿ 如果要求在外存(硬盘)上以ASCII码的形式存储,则需要在存储前进行转换。以ASCII字符的形式存储的文件就是文本文件。
那一个数据在文件中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
举一个例子,现有整数10000,如果以ASCII码的形式输出到磁盘中,则磁盘中占用5个字节(每个字符一个字节),而以二进制形式输出,则在磁盘上只占4个字节(VS2022测试)。在32位平台下,10000的两种形式的存储如下:
#include<stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//以⼆进制的形式写到⽂件中
fclose(pf);
pf = NULL;
return 0;
}
上面的代码可能有些看不懂,下面会详细讲解的。上面的代码就是创建了一个整型变量a,并赋值为10000,那内存就为这个变量开辟了4个字节的空间来存储10000。要把10000以二进制的形式写进test这个文件中。上面的代码就这么一回事。记住数据在内存中都是以二进制补码的形式来存储的。在运行这段代码后,我们再在test文件里查看存储的数据:
对于文件的操作,要遵循几个步骤:
🍑1.打开文件
🍑2.读/写文件
🍑3.关闭文件
在进行文件操作之前我们先掌握下面的一些概念。
(1)🍋流🍋
我们程序的数据需要输出到各种外部设备上,也需要从外部设备上获取数据,不同的外部设备的输入输出操作方式各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流理解为流淌着数据的水流。
我们的C语言程序有时会在终端、硬盘、网络、U盘等外部设设备上输入输出数据,那对于每种外部设备我们对其进行输入输出数据时,都要知道他的操作方式。这对我们程序员的要求就太高了。所以就C语言就抽象出了🍕流🍕的概念,流是一个高度抽象的概念。让流和这些外部设备进行交流,C语言程序只需要从流里面读/写数据即可,至于流底层上与这些外部设备是怎样进行交流的,那是C语言和操作系统要做的事情,我们不需要掌握。一个程序员只需要知道流是怎么操作的就可以了。这样就大大的简化了程序员学习编程的难度。
C程序针对文件、画面、键盘等数据的输入输出操作都是通过流进行操作的。
一般情况下,我们想向流里写入数据,或者从流中读取数据,都是要打开流,然后再进行操作。那我们现在理解了流的概念,那在C语言中,打开文件,其实就是打开流,然后再进行流的读写。
(2)🍊标准流🍊
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了下面的3个流:
▼ stdin-标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据的。
▼ stdout-标准输出流,大多数的环境中输出至显示器界面(屏幕),printf函数就是将信息输出到标准输出流中。
▼ stderr-标准错误流,大多数环境中输出到显示器界面。
就是因为默认打开了这三个流,所以我们使用scanf、printf等函数就可以直接进行输入输出的操作。stdin、stdout、stderr这三个流的类型是:FILE*,通常称为🍐文件指针🍐。
C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。每个被使用的文件都在内存中开辟了一个相应的🍥文件信息区🍥,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
例如,VS2013编译环境提供的stdio.h头文件中有以下的文件类型申明:
struct _iobuf
{
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器的FILE类型结构体包含的内容不完全相同,但是大同小异。
🏐每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心其细节。一般都是通过一个FILE*类型的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:
FILE* pf //文件指针变量
🍓1.文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
🍆2.在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
🥩3.ANSI C规定使用fopen函数来打开文件,用fclose函数来关闭文件。
这两个函数的形式如下:
//打开文件
FILE* fopen(const char* filename , const char* mode);//关闭文件
int fclose(FILE* stream);
上面fopen函数的第一个参数很好理解是文件名,传的文件名是要带有后缀的,第一个参数是char*类型的指针,所以文件名要用双引号括起来。第二个参数mode表示文件的打开模式(以什么样的形式打开),下面都是文件的打开模式:
对于上面打开模式的含义比较精简,我们可以细讲一下:
♣ “r”(只读):以只读的方式打开文件,只能读取文件内容,不能写入。
♣ “w”(只写):以只写的方式打开文件,只能写入文件内容,不能读取文件内容。
♣ “a”(追加):以追加的方式打开文件,可以在文件末尾添加内容,不能覆盖已有内容。
🍅🍅注意在以"w"(只写)的方式打开文件时,如果文件存在,且这个文件里面已有内容,那就会清空其中的内容,从起始位置开始写入数据。这也是"w"(只写)和"a"(追加)的区别。
🥝上面表格中的字母r、w、a、b在英语中分别就是read、write、append、binary的缩写。上面表格中的+表示在前一个字母含义的基础上加上缺少的那个功能,比如"r+"就表示在读的基础上还能向文件写入数据。
🥔可能大家对于输入输出的概念还不太理解,对于程序和文件之间,如果是程序想从文件中读取数据,那就叫输入。如果想通过程序向文件中写入数据,那就叫输出。
例如我们现在要读一个test文件,写如下的代码:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件…
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
还需要注意一点,如果fopen函数打开文件失败,则这个函数会返回一个空指针(NULL)。所以对于fopen函数的返回值一定要做检查。通过一个if语句来判断pf指针是否为空,如果pf指针为NULL,那perror函数就会打印出错误信息:
上面打印的信息意思就是没有这个文件或者路径。文件操作完成后,记得要关闭文件,将文件指针置为空。
上面表格中说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。
1.比如我们要写26个英文字母到test这个文件中,我们就可以使用fputc函数:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch,pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行上面的代码后,打开test文件查看:
由上图可知,确实把26个小写的英文字母写到了test文件中。需要知道的是当以只写的方式打开文件时,在这个文件的开头是有一个闪烁的光标的,每当调用一次fputc函数写进去一个字符时,光标也会依次向后挪动一个字符位置。所以为什么叫做顺序读写函数。
fputc函数的声明形式如下:
int fputc ( int character , FILE* stream );
2.上面的fputc函数是向文件中写一个字符进去,那如果我们要从文件中一个字符一个字符的读取数据(输入),就可以用fgetc函数。fgetc函数的声明如下:
int fgetc ( FILE* stream );
比如我们要通过fgetc函数读取test文件中的所有内容,我们用一个while循环即可完成:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "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;
}
程序运行结果:
需要注意与fputc相似,当以读的形式打开一个文件时,在文件的开头会有一个闪烁的光标,每调用一次fgetc函数读取一个字符后,光标也会随之往后移动一个字符位置。所以上面的while循环就能把文件中的数据全部读取出来。
3.刚才上面都是一个字符一个字符的在文件里面读或者写。那我们也可以通过字符串的形式往文件里面写或者以字符串的形式从文件里面读。那用到的两个函数分别就是下面的fputs和fgets函数:
//往文件里面写入一个字符串
int fputs ( const char* str , FILE* stream );
//从文件流steam里面读num个字符(包括着\0字符),并放到str所指向的空间里面
char* fgets ( char* str , int num , FILE* stream );
① 比如我们要往test这个文件里写入"hello world"这个字符串,就可以使用fputs这个函数:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputs("hello world", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行上面的代码后,打开test文件查看:
当然如果多次调用fputs函数,那就会紧接着在上一次写入字符串的后面继续写入:
fputs("hello world", pf);
fputs("welcome!\n", pf);
🍅注意如果要写入文件的字符串里含有转义字符的话,那在文件中也会实现其功能。就像上面,在第一次调用fputs函数将"hello world"写入test文件后,第二次再调用fputs函数又将"welcome!\n"写入到test文件中,该字符串的末尾有一个’\n’字符,那在文件中就会出现换行,如果这时再继续调用fputs函数写入字符串的话,就会从第二行开始写入。所以可以看到上面test文件中有2行。
②如果我们想从文件中读取字符串,那就可以使用fgets函数:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
char arr[15] = "xxxxxxxxxxxxxx";
fgets(arr, 10, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
假设test文件里面存着上面我们刚刚写进去的字符串"hello worldwelcome!\n"。那通过fgets函数从这个文件里面读10个字符放到arr这个字符数组里面。那么只会读到这个字符串的前9个字符到arr这个数组里面,为什么呢?不是说要读10个字符吗?原因是还有一个字符要留给’\0’。就是🍞如果你传进去的参数是num的话,那只会读到这个文件的前num-1个字符,剩下的一个字符会用来存储’\0’🍞,就是这个意思。我们可以逐语句调试起来,在监视窗口查看字符数组arr的情况:
还没有读文件内容到arr数组里时,数组存储的内容如下:
通过fgets读字符到arr数组里后,数组内容变化如下:
可以看到确实只读到了文件的前9个字符到数组arr中,还有一个字符用来存储’\0’了。
如果通过fgets从文件中读取字符时,还没读够num-1个字符就遇到了’\n’字符会怎么样呢?假设我们往test文件中写入了如下的几个字符串:
fputs("hello\n", pf);
fputs("welcome to C!\n", pf);
fputs("goodbye\n",pf);
假如我们现在要从test这个文件中读10个字符:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
char arr[10] = "xxxxxxxxx";
fgets(arr, 10, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
还是通过监视窗口来查看数组arr的存储情况:
由上图可以看到,还没有读到9个字符就遇到’\n’时,就不会再往下读取了。在读完’\n’后,再在后面存储一个’\0’就完了。这就是还没有读完num-1个字符就遇到’\n’的情况。如果我们想把这个文件里的所有数据都读出来,那么用一个while循环即可:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
char arr[4] = { 0 };
while (fgets(arr, 4, pf) != NULL)
{
printf("%s", arr);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
🍧4.我们上面用到的fgetc、fputc这两个函数是用来读写字符的,fgets、fputs这个两个函数是用来读写字符串的。那如果现在我们想在文件中读写一些带有格式的数据,就可以用fscanf和fprintf函数。我们可以对比着scanf和printf函数看看这两对函数的相似之处。比如我们想以指定的格式向文件流中写入数据,可以用fprintf函数:
#include<stdio.h>
struct S
{
char name[10];
int age;
float weight;
};
struct S s = { "张三",18,56.5 };
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %f", s.name, s.age, s.weight);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行程序后查看test文件:
还可以以指定的格式读文件中的数据,比如我们可以将刚才写进文件中的数据以指定的格式读出来放到一个结构体中,再在屏幕上打印结构体成员的数据:
#include<stdio.h>
struct S
{
char name[10];
int age;
float weight;
};
int main()
{
struct S s = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.weight));
//打印结构体成员数据
printf("%s %d %f\n", s.name, s.age, s.weight);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
5.剩下还有一组和函数,与上面讲到的顺序读写函数不同,这两个函数只能从文件流中读写,不能从其他流中读写。上面我们遇到的函数在文件中读写的数据都是文本的形式,而fread和fwrite函数是以二进制的形式在文件流中读写。这是这两个函数与上面的几组函数的不同点。
❶先来看fwrite函数:
size_t fwrite ( const void* ptr , size_t size , size_t count , FILE* stream );
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
//打开文件
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
int sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行上面的程序后查看test文件:
可以看到文件中是乱码,因为这是文本编辑器,所以看到的是乱码。说明输出数据成功。
❷再看fread函数
size_t fread ( void* ptr , size_t size , size_t count , FILE* stream );
🥑fread函数就是从指定的文件流steam中读取count个大小为size(单位字节)的数据,存放到ptr所指向的空间里去。🥑比如我们要将刚才以二进制形式写到test文件中的数据,再以二进制的形式读到一个数组中,就可以使用fread函数:
#include<stdio.h>
int main()
{
int arr[5] = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fread(arr, sizeof(arr[0]), 5, pf);
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
🥙上面学习了关于顺序读写函数在文件流中的使用,现在来对比下面的两组函数:
scanf - fscanf - sscanf
printf - fprintf - sprintf
对于上面有两组还没有学过的函数sscanf和sprintf,我们可以在c++官网上查看这两组函数的信息:
一个是将带有格式化的数据转换成字符串,一个是从字符串中以指定的格式读取数据。这就是两个函数的作用。
#include<stdio.h>
struct S
{
char name[10];
int age;
float weight;
};
int main()
{
char buf[100] = { 0 };
struct S s = { "李四",20,48.5f };
sprintf(buf, "%s %d %f", s.name, s.age, s.weight);
printf("以字符串形式打印:%s\n", buf);
struct S t = { 0 };
sscanf(buf, "%s %d %f", t.name, &(t.age), &(t.weight));
printf("按照格式打印:%s %d %f\n", t.name, t.age, t.weight);
return 0;
}
程序运行结果:
现在来总结一下上面两组函数的作用:
🍎 根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)。
int fseek ( FILE* stream , long int offset , int origin );
该函数的第一个参数就是我们要操作的文件流(文件指针),第二个参数是偏移量的意思,第三个参数是起始位置。比如现在要从test文件里读字符,文件里现在有abcdefghijk这几个字符,通过fgetc函数来读取文件中的字符,那一开始文件光标是指向文件开头的第一个字符的,每调用一次fgetc函数,文件光标就会向后移动一个字符位置。
如果使用fseek函数来指定文件光标的位置,就要知道指定字符相比较于起始位置的偏移量。对于起始位置参数的选取就有三种:
当我们第一次调用了fgetc函数读取test文件中的第一个字符a后,光标就会指向下一个字符b。如果我们想读取指定位置的字符,就可以通过fseek函数来指定文件光标的位置,然后通过fgetc来读取我们想要的字符:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
//让光标从文件的起始位置向后偏移5,指向字符'f'
fseek(pf, 5, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
当然上面还有另外两中方式来指定光标的位置:
//让光标从当前所在位置向后偏移4个字符,指向'f'
fseek(pf, 4, SEEK_CUR);
//让光标从文件末尾向前偏移6个字符,指向'f'
fseek(pf, -6, SEEK_END);
上面两种方式指定文件光标位置后,再调用fgetc函数读取字符,也可以读到字符’f’。这就是fseek函数的作用。
ftell函数会返回文件指针相对于起始位置的偏移量。
long int ftell ( FILE* stream );
比如刚才我们调用了fseek函数指定了文件光标(文件指针)位置,光标现在指向’f’字符。那现在要通过ftell函数返回文件光标相对于文件起始位置的偏移量,那就是5:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
//让光标从文件的起始位置向后偏移5,指向字符'f’
fseek(pf, 5, SEEK_SET);
printf("%d\n", ftell(pf));
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
有时我们操作文件,会让文件的光标前后移动,那如果现在想让光标回到文件的起始位置,就可以使用rewind函数。🍉🍉rewind函数的作用就是让文件指针的位置回到文件的起始位置:
void rewind ( FILE* stream );
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
//让光标从文件的起始位置向后偏移5,指向字符'f’
fseek(pf, 5, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);
rewind(pf);//让文件的光标回到文件的起始位置
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
通过上面的运行结果可知,当通过rewind函数让文件光标回到文件的起始位置后,再调用fgetc函数来读取文件中的字符,就又从文件的开头开始读取字符。
feof函数经常会被错误理解和使用。🍆牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件是否读取结束。🍆在文件读取结束后,我们要判断是什么原因导致读取结束的?
🍅1.有可能是因为遇到文件末尾才读取结束的。
🍅2.还有可能是因为读取时发生错误而导致的读取结束。
所以feof的作用是:当文件读取结束的时候,判断读取结束的原因是否是遇到文件末尾。与之对应的还有一个ferror函数:是用来判断是否是因为读取时发生错误而导致的读取结束。
在打开一个流的时候,这个流上会设置有两个标记值:
(1) 遇到文件末尾时会设置一个标记值
(2) 发生错误时会设置一个标记值
上面我们学过的顺序读写函数,他们都有返回值:
🍉1.fgetc函数当遇到文件末尾或者发生错误时,返回EOF。
🍉2.fgets函数当遇到文件末尾或者发生错误时,返回NULL。
🍉3.二进制文件的读取结束,判断返回值是否小于实际要读取的个数。例如:fread判断返回值是否小于实际要读的个数。
通过官网来查看一下feof和ferror这两个函数的返回值:
所以我们在文件读取结束的时候就可以用这两个函数来判断是什么原因导致读取结束的:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c\n", ch);
}
//判断是什么原因导致读取结束的?
if (feof(pf) != 0)
{
printf("遇到文件末尾,正常读取结束!\n");
}
else if (ferror(pf) != 0)
{
perror("fgetc");//打印错误原因
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果:
当然也有读取失败的时候,就比如在上面代码中,本来是以读的形式打开,那用fputc去写文件就会发生错误:
ANSIC标准是采用“缓冲文件系统”来处理数据文件的,所谓缓冲文件系统就是🥑指系统自动地在内存中为程序中的每一个正在使用的文件开辟一块“文件缓冲区”🥑。从内存向磁盘输出的数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果是从磁盘向计算机中读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区)中,然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小是根据C编译系统决定的。
所以缓冲区本质上就是一块内存空间,是计算机在内存中开辟的一块存储空间。用来暂时保存输入或输出的数据。
1. 缓冲区是为了让低速的输入输出设备和高速的用户程序能够协调工作,并降低输入输出设备的读写次数。由于内存的读写速度是远高于硬盘的,当我们向硬盘写入数据时,程序需要等待,不能做任何事情,就好像卡顿了一样,用户体验非常差。计算机上绝大多数应用程序都需要和硬件打交道,例如读写硬盘、向显示器输出、从键盘输入等,如果每个程序都等待硬件,那么整台计算机也将变得卡顿。
2. 缓冲区的另外一个好处是可以减少硬件设备的读写次数。我们的程序其实并不能直接读写硬盘,它需要告诉操作系统,让操作系统内核(Kernel)去调用驱动程序,只有驱动程序才能真正的操作硬件。
🥥🥥从用户程序到硬件设备需要经过好几层的转换,每一层的转换都有时间和空间上的开销,一旦用户程序需要密集的进行输入输出操作,这种开销就会变得非常大。程序的性能就会大大下降。那分配缓冲区就有很大的作用啦。每次调用读写函数时,先将数据放入缓冲区,等缓冲区满了再进行一次真正的读写操作,这样转换的次数就会大大减少。程序的性能也可以成倍的提高。
🍑举一个简单的例子:比如我们在使用打印机打印文档时,由于打印机的打印速度相对较慢,那就可以先把要打印的文档输出到打印机相应的缓冲区里,由打印机逐步的打印输出在缓冲区里的文档(而不是打印完一份文档,再等CPU输出要打印的文档,这会很浪费时间)。这时我们可以操作CPU处理别的事情。当缓冲区里的文档都打印完了,再等待CPU输出文档到缓冲区里继续打印。这样CPU与打印机之间就可以协调工作。大大的提高计算机的运行速度。
下面一段代码就可以很好地看出,当我们向文件中写入数据时,会先将数据放到缓冲区中,再将缓冲区中的数据写入内存中:
#include<stdio.h>
#include <windows.h>
//VS2022 Win11环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);//10000毫秒==10秒
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才会将输出到缓冲区的数据写到文件(磁盘)里
//注:fflush在高版本的VS上不能使⽤
printf("再睡眠10秒此时,再次打开test.txt文件,⽂件里就有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
程序运行结果:
大家可以运行一下这段程序,等待的前10秒,是看不到test文件中的数据的,刷新缓冲区后再等待10秒,再点开test文件,就能看到输出的内容啦。
这里就得到一个结论:
🏀🏀🏀因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能会导致读写文件的问题。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- huatuo0.cn 版权所有 湘ICP备2023017654号-2
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务