文件操作
1.为什么使用文件
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
2.什么是文件
磁盘(硬盘)上的文件是文件。
但是在程序设计中]我们一般谈的文件有两种: 程序文件、数据文件(从文件功能的角度来分类的)。
2.1程序文件
程序文件包括源程序文件(后缀为c),标文件 (windows环境后缀为bj),可执行程序(windows环境后缀为.exe)
2.2数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3部分:文件路径+文件名主干+文件后缀例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名
3.二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在文件中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式
输出,则在磁盘上只占4个字节。
如图:
测试代码:
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");// 我们创建了一个test文件 格式是txt
// wb的w就是写 b就是二进制
fwrite(&a, 4, 1, pf);// 二进制的形式写到文件中
// 我想把a这个四个字节的数据 1次性的的写到pf所指向的文件里
fclose(pf);
pf = NULL;
return 0;
}
这个text文件直接打开是打不开的 要用下图的方式
打开过后我们会发现
这个其实就是我们刚刚创建a的二进制在内存中直接保存在了text文件当中
我们可以发现在内存中以小端方式存储的a,没有任何处理 直接以保存在了text文件中 那么这个text文件就被叫做二进制文件
4.文件的打开和关闭
4.1 流和标准流
4.1.1流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
有了流这个概念之后,程序员只需要做到怎么向流 写/读 数据就行了
至于C语言程序数据如何输出和输入 到各个外部设备上 就交给流操作就行
但是为什么我们平常在写代码的时候我们并没有感受到流的存在呢
这就需要我们去知道标准流的知识了
4.1.2标准流
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:
- stdin 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据·
- stdout- 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。
- stderr- 标准错误流,大多数环境中输出到显示器界面
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE * 通常称为文件指针。
C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
4.2文件指针
缓冲文件系统中,关键的概念是"文件类型指针”,简称”文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。
该结构体类型是由系统声明的,取名 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;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件
4.3 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSI C 规定使用 fopen 函数来打开文件,fclose 来关闭文件。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream )
实例代码:
int main()
{
FILE* pf = fopen("test.txt", "r");// r是只读 如果打不开的话fopen函数就会返回空指针
if (pf == NULL)
{
perror("fopen");// fopen: No such file or directory
return 1;
}
// 读文件
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
如果打开了文件那就什么都不会发生
如果fopen(“test.txt”, “r”)中的r换成了w
那么打开文件后 会把原先文件里的内容给清空
5.文件的顺序读写
5.1 顺序读写函数介绍
我们来了解一下这四组函数
5.1.1 fputc 和 fgetc 函数
第一组:
fputc函数就是写字节到一个流里边 这个流会把内容写到文件中
int main()
{
// 打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 写文件
//fputc('a', pf);
//fputc('b', pf);
//fputc('c', pf);
char ch = 0;
// 向文件内写入26个字母
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);// 每次只向文件内写入一个字母
}
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们再看看 fgetc函数
int fgetc (FILE* stream);
为什么fgetc函数返回是整型类型呢 返回的不是文件内的字符吗?
我们来看一段fgetc函数使用的代码:
int main()
{
// 打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读文件
int ch = fgetc(pf);//pf 一开始指向的是文件内容的首地址
printf("%c\n", ch); //a
ch = fgetc(pf);
printf("%c\n", ch);//b // 这个时候指向的是下一个字符
ch = fgetc(pf);
printf("%c\n", ch);//c
// 但是如果文件中内容很多 字符很多 这样去读取很慢 效率低
// 那么我们就采用循环解决问题
while ((ch = fgetc(pf)) != EOF) // fgetc函数读取到文件末尾或者读取失败会返回EOF
{
printf("%c ", ch);
}
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.2 fputs 和 fgets函数
fputs
int fputs(const char* str, FILE* stream);
把一个str指针指向的字符写到 文件的流中 知道遇到\0
我们来看fputs函数使用的实例:
// fputs函数
int main()
{
// 打开文件
FILE* pf = fopen("text.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 写文件
fputs("hello world\n", pf);// 如果不加上\0的话 hello bit 将会打印再d的后面 也就是都打印在一行
fputs("hello bit", pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
那我们将字符串写进去了 我们怎么读呢?
这个时候就要用到fgets函数
char* fgets(char* str, int num, FILE* stream);
意思就是从stream这个流里面读num - 1个数据 我们读的数据会被放到str里面
为什么是num - 1个呢
因为要给\0留一个空间
我们来看fgets函数的使用实例:
// fgets函数
int main()
{
// 打开文件
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读文件
char arr[10] = { 0 };
fgets(arr, 10, pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们可以通过监视发现 确实是只读取了10 - 1个数据 也就是9个数据
最后一个位置留给了\0
还有一种情况我们来看一下:
// fgets函数
int main()
{
// 打开文件
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读文件
char arr[20] = { 0 };
fgets(arr, 20, pf); // 如果一行没有20个给它读取,那么他就只会读取一行的所有数据 如果有\n也会读取
// 一行太长是没有关系的 因为没有读取到换行符 fgets会把一行数据拆成多次读取
printf("%s", arr);// 如果还有下一行 那么就不用+\n因为 文件内的数据自带\n
// 如果我们想读取多行的数据 那就读取多次就行了 多次采取循环读取
while (fgets(arr, 20, pf) != NULL)// fgets函数如果读取到文件末尾 和 读取失败的话 那么就会返回空指针
{
printf("%s", arr);
}
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.3 fprintf函数 和 fscanf函数
fprintf函数:
int fprintf(FILE* Stream, const char* format, ...);
printf函数:
int printf(const char* format, ...);
我们可以发现 fprintf多了一个文件流的参数
来看一段使用fprintf函数的代码
//fprintf函数的使用
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "张三", 20, 66.6f };
// 把结构体S的变量s中的数据放到文件中
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 写文件 fprintf把指定形式的数据以文本的形式写进文件
fprintf(pf, "%s %d %f", s.name, s.age, s.score);
// 如果要写多个数据进去 就循环 和之前一样 这里不写
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fscanf函数:
int fscanf(FILE* stream, const char* format,...);
scanf函数:
int scanf(const char* format,...);
区别就是scanf是从键盘 也就是从我们读取输入数据, 写入到指定地址
fscanf是从文件中 读取 数据 写入到指定地址
我们来看一段使用fscanf函数使用的代码:
//上段代码我们通过fprintf函数实现了结构体S的变量s中的数据放到文件中
// 那我们现在通过fscanf函数来实现 把文件的数据存入到结构体变量中
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { 0 };
// 想从文件test.txt文件中读取数据存放到结构体变量s中
FILE* pf = fopen("test.txt", "r");//以读(r)的形式打开
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读文件
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
// 把pf指向的文件中的内容 写入指向的地址 name本身是数组名 不需要加上&
// 打印s中的数据
printf("%s %d %f", s.name, s.age, s.score);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
总结:
前面我们学的这些函数都有适用范围 都是所有的输入流和输出流
因此我们只需要在编写代码的时候将文件流改成标准输出流
那么它也能实现一些其他的功能
比如:
// 打印s中的数据
printf("%s %d %f\n", s.name, s.age, s.score);
fprintf(stdout, "%s %d %f\n", s.name, s.age, s.score); // 也能实现打印在屏幕上面
// 把输出流改成了标准输出流
比如:
int main()
{
fputc('a', stdout);
return 0;
}
5.1.4 fread 和 fwrite 函数
前面我们写入文件的数据 都是以文本的形式写进去的 所以我们打开文件是可以直接看懂文件里的数据的
我们先来看 fwrite函数:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
我们发现该函数有四个参数
那分别是用来干什么的呢 通过查询我们得知
ptr是你传入的地址 从哪里开始写入
size是传入地址后 你要写入的类型的大小
count是个数 你要写几个
stream是文件流 你把ptr后写入的内容放入文件中
我们来看fwrite函数使用的代码:
// fwrite函数的使用
int main()
{
int arr[] = { 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);
// 把从arr地址开始 的sz个大小为sizeof(arr[0]) 的数据 写入pf指向的文件中
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这个文件使用二进制编译器打开才能看到的
不然的话正常打开我们是看不懂里面的数据的
我们能从外部写入二进制数据到文件中
自然也能从文件中读取数据出来
这个时候我们就需要学习fread函数了
fread函数
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
这里我们需要注意一下fread返回的值size_t
这里的返回值返回的是真正读取到的个数
假如文件中有 count个数据 那就返回count
假如 文件中不足count个数据 那就返回真实读取的个数
比如:
文件中有3个数据 count = 5
那就返回3
如果读取不到数据 那就返回 0
我们来看一段fread使用的代码:
// fread 函数的使用
int main()
{
int arr[5] = { 0 };
FILE* pf = fopen("test.txt", "rb");// rb表示读取二进制数据
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读数据
//fread(arr, sizeof(arr[0]), 5, pf);
把从pf指向的文件内 读取5个大小为sizeof(arr[0])的二进制数据 写入到arr地址中
检验是否读取文件中的数据并写入arr数组中
//for (int i = 0; i < 5; i++)
//{
// printf("%d ", arr[i]);//1 2 3 4 5
//}
// 假如我们并不知道 文件中有多少个数据 但是我们又想把文件中的数据全部读取出来, 我们可以用循环
int i = 0;
while (fread(arr + i, sizeof(int), 1, pf))
{
printf("%d ", arr[i]);// 1 2 3 4 5
i++;
}
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.2 对比一组函数
scanf/fscanf/sscanf
printf/fprintf/sprintf
5.2.1scanf/fscanf/sscanf
scanf——从标准输入流上读取格式化的数据
fscanf——从指定的输入流上读取格式化的数据
sscanf——在字符串中读取格式化的数据
int sscanf(char* str, const char* format,...);
5.2.2printf/fprintf/sprintf
printf——把数据以格式化的形式打印在标准输出流上
fprintf——把数据以格式化的形式打印在指定的输出流上
sprintf——把格式化的数据转化成字符串
前面两个函数我们都了解了 我们现在俩了解一下sprintf函数
fprintf函数:
int fprintf(FILE* Stream, const char* format, ...);
printf函数:
int printf(const char* format, ...);
sprinf函数:
int sprintf(char* str, const char* format);
其实就是把格式化的数据转化成字符串了
我们来看一段使用sprintf函数和sscanf函数的代码
// sprintf函数和sscanf函数的使用
struct S
{
char name[20];
int age;
float score;
};
int main()
{
char buf[200] = { 0 };
struct S s = { "张三", 20, 66.6f };// 我们想把这个格式化的数据给转化成字符串
sprintf(buf, "%s %d %f", s.name, s.age, s.score);
// 将格式化的数据转化成字符串
printf("1.以字符串的形式打印:%s\n", buf);
struct S t = { 0 };
sscanf(buf, "%s %d %f", s.name, &(s.age), &(s.score));
// 从字符串中读取格式化的数据
printf("2.以格式化的形式打印:%s %d %f\n", s.name, s.age, s.score);
return 0;
}
6.文件的随机读写
6.1 fseek
根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)
int fseek(FILE* stream, long int offset, int origin);
long int offset 这个形参就是偏移量
int origin 这个是起始位置
我们来看一段使用fseek函数的代码:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读文件
int ch = fgetc(pf); // 走完这个代码后 pf指向b
printf("%c\n", ch);// a
fseek(pf, 4, SEEK_CUR);// SEEK_CUR 表示文件指针当前位置
// 从文件指针当前位置向后偏移四个量 让指针指向当前位置
// 当然想让指针从指向b到指向f 写法不止这一种
fseek(pf, 5, SEEK_SET);// SEEK_SET表示文件的起始位置
fseek(pf, -4, SEEK_END);// SEEK_END表示文件末尾位置
// 上面三种都可以
ch = fgetc(pf);
printf("%c\n", ch);// f
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
6.2 ftell
返回文件指针相对于起始位置的偏移量
long int ftell(FILE* stream);
我们来看一个代码:
// ftell函数的使用
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读文件
int ch = fgetc(pf);
printf("%c\n", ch);// a
fseek(pf, 4, SEEK_CUR);// SEEK_CUR 表示文件指针当前位置
printf("%d\n", ftell(pf));// 5 计算当前指针和起始地址的偏移量
// 我们可以通过ftell来计算文件的长度
fseek(pf, 0, SEEK_END);// 这个时候pf指向文件末尾
printf("%d\n", ftell(pf)); // 9 说明文件数据长度是9
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
6.3 rewind
让文件指针的位置回到文件的起始位置
void rewind(FILE* stream);
使用例子:
// rewind函数的使用
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 读文件
int ch = fgetc(pf); // 走完这个代码后 pf指向b
printf("%c\n", ch);// a
fseek(pf, -4, SEEK_END);// SEEK_END表示文件末尾位置
ch = fgetc(pf);
printf("%c\n", ch);// f
rewind(pf);// 让pf指向文件的起始位置
ch = fgetc(pf);
printf("%c\n", ch);// a
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
7.文件读取结束的判定
7.1 被错误使用的 feof 函数
牢记: 在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束
feof 的作用是:当文件读取结束的时候,判断读取结束的原因是否是:遇到文件尾结束。
从上面的图片我们可以知道 feof函数就是判断此时文件结束,是否时因为遇到了文件末尾才结束的
1.对于文本文件读取是否结束,
我们可以判断返回值是否为 EOF( fgetc),或者 NULL ( fgets )例如:
- fgetc 判断是否为EOF
- fgets判断返回值是否为 NULL
我们来看一个例子:
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);
}
// 前面的fgetc在读取数据失败或者遇到文件末尾的时候 都会返回EOF
// 那我们判断是什么原因导致读取结束
if (feof(pf))
{
printf("遇到文件末尾,读取结束\n");
}
else if(ferror(pf))
{
perror("fgetc");// 由于我们的代码是正确的 所以无法走到这里
}
return 0;
我们再看一个:
// 我们写一个错误的情况
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 写入数据 (前面我们是以r的方式打开文件的 但是我们却向文件写入数据 那这就会报错)
int ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
// 前面的fputc在写入数据失败或者遇到文件末尾的时候
// 那我们判断是什么原因导致文件读取结束
if (feof(pf))
{
printf("遇到文件末尾,读取结束\n");
}
else if (ferror(pf))
{
perror("fputc");// fputc: Bad file descriptor
}
return 0;
}
2.对于二进制文件的读取结束判断,
我们可以判断返回值是否小于实际要读的个数
例如:
fread判断返回值是否小于实际要读的个数。
我们来看看代码实例:
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
if (ret_code == SIZE)
{
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n)
{
printf("%f ", b[n]);
}
putchar('\n');
}
else
{ // error handling
if (feof(fp))
{
printf("Error reading test.bin: unexpected end of file\n");
}
else if (ferror(fp))
{
perror("Error reading test.bin");
}
}
fclose(fp);
return 0;
}
8.文件缓冲区
ANSIC 标准采用缓冲文件系统处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据(程序变量等)。
缓冲区的大小根据C编译系统决定的【有可能无缓冲】
人话就是 :
写入文件:
- 我们把数据写入文件中的时候 ,是先写入内存中一个叫做文件缓冲区的空间
- 直至把所有要传入文件的数据放到文件缓冲区之后
- 再然后写入文件
读文件:
- 读文件也是先把文件中的数据放到 文件缓冲区
- 直至把所有要拿出来的数据放到文件缓冲区
- 再放到其他空间中
我们来看一个例子来验证缓冲区的存在:
# include<stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
// 这个时候我们去打开test文件是什么都没有的
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
// 因为fflush函数 此时我们打开test是abcdef
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
系统**处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据(程序变量等)。
缓冲区的大小根据C编译系统决定的【有可能无缓冲】
[外链图片转存中…(img-ijOgW5RL-1713979089202)]
[外链图片转存中…(img-YsNrPw0U-1713979089202)]
人话就是 :
写入文件:
- 我们把数据写入文件中的时候 ,是先写入内存中一个叫做文件缓冲区的空间
- 直至把所有要传入文件的数据放到文件缓冲区之后
- 再然后写入文件
读文件:
- 读文件也是先把文件中的数据放到 文件缓冲区
- 直至把所有要拿出来的数据放到文件缓冲区
- 再放到其他空间中
我们来看一个例子来验证缓冲区的存在:
# include<stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
// 这个时候我们去打开test文件是什么都没有的
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
// 因为fflush函数 此时我们打开test是abcdef
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。