C语言修炼——什么是流?什么是文件?什么是文件操作?

目录

一、为什么使用文件?

如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使⽤⽂件。

二、什么是文件?

磁盘(硬盘)上的⽂件是⽂件。

但是在程序设计中,我们⼀般谈的⽂件有两种:程序⽂件、数据⽂件(从⽂件功能的⻆度来分类

的)。

2.1 程序文件

程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows环境后缀为.exe)。

2.2 数据文件

⽂件的内容不⼀定是程序,⽽是程序运⾏时读写的数据,⽐如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件。

本文讨论的是数据⽂件。

在以前各章所处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运⾏结果显⽰到显⽰器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处理的就是磁盘上的⽂件。

2.3 文件名

⼀个⽂件要有⼀个唯⼀的⽂件标识,以便⽤⼾识别和引⽤。

⽂件标识包含3部分:⽂件路径+⽂件名主⼲+⽂件后缀

例如: C:\Windows\Test\test.txt

其中C:\Windows\Test\就是文件路径test就是文件名主干.txt就是文件名后缀

为了⽅便起⻅,⽂件标识常被称为⽂件名。

需要注意:

  1. 文件名中有一些不能被包含的字符,例如这些字符:\,/,:,*,?,",<,>,|
  2. 文件路径指的是从盘符到该文件所经历的路径中各符号名的集合。
  3. 后缀名决定了文件的默认打开方式(例如以.txt打开的文件是文本文档,以.bin打开的文件是二进制文件)。
  4. 文件名中不一定包含文件后缀(例如有些文件可能被隐藏了后缀,像这样C:\Windows\Test\test)。

三、二进制文件和文本文件

根据数据的组织形式,数据⽂件被称为⽂本⽂件或者⼆进制⽂件。

数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件。

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是⽂本⽂件。

⼀个数据在⽂件中是怎么存储的呢?

字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节('1','0','0','0','0'每个字符⼀个字节),⽽⼆进制形式输出,则在磁盘上只占4个字节(一个 int 的大小)。

测试代码:

c 复制代码
//代码1
#include <stdio.h>
int main()
{
	int a = 10000;
	FILE * pf = fopen("test.txt", "w");
	fprintf(pf, "%d", a);//ASCII码的形式写到文件中
	fclose(pf);
	pf = NULL;
	return 0;
}

结果1:

c 复制代码
//代码2
#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;
}

在VS上打开二进制文件:

运行程序后在解决资源管理项添加现有项,选择对应文件的打开方式。

结果2:

可以看到在结果1中,test.txt文件中存储的是5个字符,所以需要占用5个字节的空间。而整型10000的二进制形式是00000000 00000000 00100111 00010000,化作十六进制是0x00002710,存储在test.txt中只需要4个字节(上图是小端字节序)。

四、文件的打开和关闭

铺垫了这么多,最关键的还是需要学会如何从文件读取数据与如何输出数据到文件。

4.1 流和标准流

4.1.1 流

我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出所需要的操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河,通过流将数据运送到需要的地方。

C程序针对⽂件、键盘与屏幕画面等的数据输⼊输出操作都是通过流操作的。⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。

4.1.2 标准流

那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?

那是因为C语⾔程序在启动的时候,默认打开了3个流:

  • stdin------标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
  • stdout------标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。
  • stderr------标准错误流,⼤多数环境中输出到显⽰器界⾯。

这是默认打开了这三个流,我们使⽤scanfprintf等函数就可以直接进⾏输⼊输出操作的。

stdinstdoutstderr三个流的类型是:FILE*,通常称为⽂件指针。

C语⾔中,就是通过FILE*的⽂件指针来维护流的各种操作的。

4.2 文件指针

缓冲⽂件系统中,关键的概念是"⽂件类型指针",简称"⽂件指针"。

每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名FILE

例如,VS2013编译环境提供的stdio.h头⽂件中有以下的⽂件类型申明:

c 复制代码
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*的指针变量:

c 复制代码
FILE* pf;//⽂件指针变量

定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个⽂件的⽂件信息区(⼀个结构体变量)。通过该⽂件信息区(结构体变量)中的信息就能够访问该⽂件。也就是说,通过⽂件指针变量能够间接找到与它关联的⽂件。
⽐如:

4.3 文件的打开和关闭

⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件。

在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。

ANSI C规定使⽤fopen函数来打开⽂件,fclose来关闭⽂件。

c 复制代码
//fopen原型
FILE* fopen(const char* filename, const char* mode);
//fclose原型
int fclose(FILE* stream);

mode表⽰⽂件的打开模式,下⾯都是⽂件的打开模式:

运用实例:

c 复制代码
/* fopen fclose example */
#include <stdio.h>
int main()
{
	FILE* pFile;//创建FILE*类型指针pFile
	pFile = fopen("test.txt", "w");//以只写的模式打开文件"test.txt",pFile指向开辟后的文件信息区
	//文件操作
	if (pFile != NULL)//判断pFile是否为空指针,确保接受到了文件信息区的地址再去使用
	{
		fputs("fopen example", pFile);//输出字符串到文件流中
		fclose(pFile);//关闭文件
	}
	return 0;
}

运行结果:

五、文件的顺序读写

5.1 顺序读写函数介绍

上⾯说的适⽤于
所有输⼊流 ⼀般指适⽤于标准输⼊流其他输⼊流 (如⽂件输⼊流);
所有输出流 ⼀般指适⽤于标准输出流其他输出流(如⽂件输出流)。

a. fgetc


fgetc是一个从输入流中读取数据的输入函数,返回值是指定流中位置指示器当前指向的字符,返回后位置指示器再次后移一位。
fgetc只有一个参数FILE* stream,即指定一个输入流,从该输入流中读取字符。

对于返回值,为什么fgetc要选择int而不是char呢?

其实是因为读取文件遇到文件尾或者读取失败时,fgetc函数会返回EOF(值为-1),所以为了兼顾fgetc选择int作为返回类型,既可以返回字符又可以返回EOF

使用示例:

项目文件中已经有一个test.txt文件,文件情况如上。

我们现在需要将test.txt文件内的字符数据通过输入流输入到test.c文件里的字符数组ch[20]中。

这里需要先提醒一个小细节

fgetc在文件中一个一个读取字符时,会有一条竖线,叫作光标。如图,这就是我们在fgetc介绍时提及的位置指示器的直观显现。当我们第一次调用fgetc读取文件时,光标是在文件第一个字符f前,当函数fgetc读取完第一个字符就会返回,同时光标会后移一位,到了fo之间,下一次调用fgetc时光标就从这一位置开始读取字符。

c 复制代码
#include <stdio.h>
int main()
{
	FILE* pfin = fopen("test.txt", "r");
	if (!pfin)//pfin==NULL则进入if语句
	{
		//fopen打开文件错误,报错并return退出程序
		perror("fopen");
		return 1;
	}
	char ch[20] = { 0 };
	int i = 0;
	while ((ch[i] = fgetc(pfin)) != EOF)//当fgetc的返回值为EOF,即遇到文件末尾时,退出循环
	{
		printf("%c", ch[i]);//打印字符
		i++;
	}
	fclose(pfin);//文件打开使用完必须关闭
	pfin = NULL;//pfin指针如果不用应该设置为空指针

	return 0;
}

b. fputc


fputc是向输出流中写入字符的输出函数,每写入一个字符,位置指示器后移一位。
fputc有两个参数,分别是int characterFILE* streamcharacter是我们需要输出的字符,stream是指定的输出流。

使用示例:

现在程序中有一字符串"Hello David!\n"需要输出到test.txt文件中,我们用fputc来完成。

c 复制代码
#include <stdio.h>
#include<string.h>
int main()
{
	FILE* pfout = fopen("test.txt", "w");
	if (!pfout)//pfout为空则报错
	{
		perror("fopen");
		return 1;
	}
	char str[20] = "Hello David!\n";
	for (int i = 0; i < strlen(str); i++)
	{
		fputc(str[i], pfout);//将字符数组str中的元素依次输出到文件中
	}
	fclose(pfout);//文件打开使用完后必须关闭
	pfout = NULL;//pfout指针不使用需要及时置为NULL

	return 0;
}

可以看到,我们利用字符输出函数fputc成功的把str数组中的所有字符都输出到了test.txt文件中,就连\n都输出到了文件中(上图中的第二行)。

c. fgets


fgets是一个从输入流中读取字符串的文本行输入函数。

需要注意fgets有3个参数,char* strFILE* stream没什么好讲,即接收输入流数据的字符数组的地址与指定的输入流。关键是numnum是我们指定输入字符个数的参数,但这里有一个细节就是真正从文件输入到数组的字符个数实际上是num-1,第num个字符永远是\0

同时,如果出现num的大小大于第一行的字符数且存在第二行的情况下,fgets会把第一行都读取完,并把\n也读取了,最后一位仍是设置为\0,然后光标到了第二行,函数fgets返回。

总结:
fgets读取结束的条件是读取到了num-1个字符或遇到文件尾,同时,如果读取时光标到了下一行也会停止读取。

使用示例:

fputc中写好的test.txt文件为例。

现在需要用fgets读取test.txt文件,将其中的文本信息放入另一个数组ch[20]中。

c 复制代码
#include <stdio.h>
#include<string.h>
int main()
{
	FILE* pfin = fopen("test.txt", "r");
	if (!pfin)//pfin为空则报错
	{
		perror("fopen");
		return 1;
	}
	char ch[20] = { 0 };
	fgets(ch, 20, pfin);//读取到文件末尾或者读取了19个字符或者到了下一行停止
	printf("%s", ch);
	fclose(pfin);
	pfin = NULL;//pfout指针不使用需要及时置为NULL

	return 0;
}


Hello David!\n存入ch[20]数组中,且fgets读取结束自动在末尾添加\0

d. fputs


fputs是向输出流中写入字符串的文本行输出函数。
fputs有2个参数,分别是const char* strFILE* streamstream是指定的输出流。str是需要写入输出流的字符串的地址。对于字符串的输出有一个细节需要特别关注,那就是fputs的输出遇到\0停止且字符串中的\0并不会输出。

使用示例:

我们用fputs重写一下fputc中的代码,如下:

c 复制代码
#include <stdio.h>
#include<string.h>
int main()
{
	FILE* pfout = fopen("test.txt", "w");
	if (!pfout)//pfout为空则报错
	{
		perror("fopen");
		return 1;
	}
	char str[20] = "Hello David!\n";
	for (int i = 0; i < strlen(str); i++)
	{
		fputc(str[i], pfout);//将字符数组str中的元素依次输出到文件中
	}
	fclose(pfout);//文件打开使用完后必须关闭
	pfout = NULL;//pfout指针不使用需要及时置为NULL

	return 0;
}

修改:

c 复制代码
#include <stdio.h>
#include<string.h>
int main()
{
	FILE* pfout = fopen("test.txt", "w");
	if (!pfout)//pfout为空则报错
	{
		perror("fopen");
		return 1;
	}
	char str[20] = "Hello David!\n";
	fputs(str, pfout);
	fclose(pfout);//文件打开使用完后必须关闭
	pfout = NULL;//pfout指针不使用需要及时置为NULL

	return 0;
}

e. fread与fwrite


fread是从输入流中读取二进制数据的输入函数,且只适用于文件输入流。
fread有四个参数。ptr是指向一段内存块或数组的指针。size为字节数,代表了需要读取的元素的类型。count为需要读取的元素个数。stream为文件输入流。
fread返回成功读取的元素个数,读取失败返回值小于count,读取成功返回值等于count,如果sizecount等于0则返回值为0。
fread有一个细节需要注意,size决定了一次读取的字节数,count决定了读取次数/元素个数,如果count大于文件能够读取的个数,那么fread自动结束。且fread不同于fputs,读取后不会自动加\0

fwrite是向输出流中写入二进制数据的输出函数,且只适用于文件输出流。
fwrite有4个参数。prt是指向需要写入的数组的指针。size为要写的数组的元素的大小。count为需要写入的元素个数。stream为文件输出流。
fwrite返回成功写入的元素个数。如果写入失败则返回值小于count,如果写入成功则返回值等于count,如果sizecount等于0则返回值为0。
fwrite有个细节需要注意,不同的size决定了一次写入的字节数,count决定了写入的次数/元素个数,如果count设置过大,会将我们指定内存块后面未知内存地址中的数据也写入文件中。

使用示例:

现在我们需要用fwrite输出字符串"Hello David!\n"的二进制数据到二进制文件test.bin中,再用fread读取test.bin文件中的数据存入字符数组ch[20]中。

c 复制代码
#include <stdio.h>
#include<string.h>
int main()
{
	//用fwrite写入数据
	FILE* pfout = fopen("test.bin", "wb");
	if (!pfout)
	{
		perror("fopen");
		return 1;
	}
	char str[20] = "Hello David!\n";
	fwrite(str, sizeof(char), strlen(str), pfout);
	fclose(pfout);//文件打开使用完后必须关闭
	pfout = NULL;//pfout指针不使用需要及时置为NULL

	//用fread读取数据
	FILE* pfin = fopen("test.bin", "rb");
	if (!pfin)
	{
		perror("fopen");
		return 1;
	}
	char ch[20] = "XXXXXXXXXXXXXXXXXXXX";//测试count大于文件能读取的数据范围时fread是否自动结束
	fread(ch, sizeof(char), strlen(str) + 10, pfin);
	for (int i = 0; i < strlen(str); i++)
	{
		printf("%c", ch[i]);
	}
	fclose(pfin);
	pfin = NULL;
	return 0;
}


fread确实自动结束:

可以发现,文件中的数据被全部读取完后,即使读取的个数小于countfread也自动结束。

f. scanf、fscanf与sscanf


我们发现,fscanfsscanf相比起我们熟知的scanf多了一个参数,这就是不同所在。

首先我们来重新认识一下scanfscanf通过我们今天的知识,其实我们可以知道,scanf是一个从标准输入流stdin中读取指定格式的数据format的标准输入流函数。

fscanfsscanf就是在流上与scanf有所不同。fscanf是更广泛的适用于所有输入流的格式化输入函数,而sscanf不是从流而是从字符串读取格式化数据的输入函数。

使用示例:

我们现在有上述test.txt文件和字符串char sentence []="Rudolph is 12 years old"。现在我们需要使用fscanf读取文件中的信息,用sscanf读取字符串中的Rudolph12。前者我们需要把文件信息存入字符数组ch[20]中,后者我们需要分别存入字符数组str[20]与整型变量i中。

c 复制代码
#include <stdio.h>
#include<string.h>
int main()
{
	//先读取文件信息打印
	FILE* pfin = fopen("test.txt", "r");
	if (!pfin)
	{
		perror("fopen");
		return 1;
	}
	char ch[20] = { 0 };
	fscanf(pfin, "%[^\n]", ch);
	for (int i = 0; i < strlen(ch); i++)
	{
		printf("%c", ch[i]);
	}
	printf("\n");
	fclose(pfin);
	pfin = NULL;

	//再读取字符串信息打印
	char sentence[] = "Rudolph is 12 years old";
	char str[20] = { 0 };
	int i = 0;
	sscanf(sentence, "%s %*s %d", str, &i);
	printf("%s->%d\n", str, i);

	return 0;
}

需要注意的细节就是和scanf类似,fscanfsscanf都会遇到空格停止读取,所以我们需要仔细考虑format的写法。

fscanf中,我们的目的是全部读取,不跳过空格,所以这里使用了%[^\n]这个格式符,这个格式符中^是省略的意思,即省略\n其他字符全部读取。

sscanf中,我们的目的是有选择性的读取字符,所以我们使用了%*s这个格式符,其中*可以使得%s读取到的字符被直接跳过省略,不存储到任何变量中。可以看到sscanf(sentence, "%s %*s %d", str, &i)中第一个%s读取到了Rudolph后,%*s应该读取到is,但我们并没有放字符变量去接收is,是因为读取到isis直接被省略了。

g. printf、fprintf与sprintf



和上面三个输入函数scanf,fscanf,sscanf对应,printf,fprintf,sprintf也在写入数据的地方有所不同。
printf已经知道是输出到标准输出流stdout中,而fprintf适用于所有输出流可以输出到指定输出流中,而sprintf可以输出格式化的数据到字符串中。

使用示例:

现在我们需要用fprintf输出结构体变量s的信息到test.txt文件中:

c 复制代码
#include <stdio.h>
struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	struct S s = { "小明", 22, 60.0f };
	FILE* pfout = fopen("test.txt", "w");
	if (!pfout)
	{
		perror("fopen");
		return 1;
	}
	fprintf(pfout, "%s同学是%d岁,在这次考试得了%.1f分。\n", s.name, s.age, s.score);
	fclose(pfout);
	pfout = NULL;

	return 0;
}

结果:

现在我们不需要输出"%s同学是%d岁,在这次考试得了%.1f分。\n"到文件中,而是输出到字符数组中储存,用sprintf应该怎么写呢?

c 复制代码
#include <stdio.h>
struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	struct S s = { "小明", 22, 60.0f };
	char str[50] = { 0 };
	sprintf(str, "%s同学是%d岁,在这次考试得了%.1f分。\n", s.name, s.age, s.score);
	printf("%s", str);
	return 0;
}

结果:

可以发现一个小细节,sprintf输出数据到字符数组中,和fputs类似都会自动在字符串尾添加\0

六、文件的随机读写

6.1 fseek

根据⽂件内部位置指示器(或称文件指针)的位置和偏移量来移动⽂件指针(⽂件内容的光标)。

fseek有3个参数,steam是文件输入/输出流,offset是移动的距离,origin是开始移动的起始位置。
origin有3个值,分别是SEEK_SET代表起始位置,SEEK_CUR代表当前位置,SEEK_END代表末尾位置。

如果fseek成功移动光饼/位置指示器则返回0值,如果失败则返回非0值。

使用示例:

c 复制代码
/* fseek example */
#include <stdio.h>
int main()
{
	FILE* pFile = fopen("example.txt", "wb");
	fputs("This is an apple.", pFile);//写入字符串
	fseek(pFile, 9, SEEK_SET);//从SEEK_SET这个位置向后移动9
	fputs(" sam", pFile);//移动后再次写入字符串
	fclose(pFile);
	return 0;
}

结果:

这里来讲一下这段代码的细节:

首先我们向代码中写入了一段初始信息This is an apple.,然后我们通过fseek移动光标。
fseek(pFile, 9, SEEK_SET)的意思是,从SEEK_SET,即初始位置开始移动9个字节的距离。所以光标起始位置在T之前,从此处开始移动9个字节,到了an之间。

接着fputs(" sam", pFile);写入字符串,因为我们example.txt文件打开模式为"wb",而不是"ab",所以再次写入时不是追加而是覆盖,即n ap sam替换。

最终This is an apple.变成This is a sample.,程序结束。

6.2 ftell

返回⽂件指针相对于起始位置的偏移量。

ftell只有1个参数,即文件输入/输出流stream

使用示例:

我们对上述文件进行文件长度计算。

c 复制代码
/* ftell example : getting size of a file */
#include <stdio.h>
int main()
{
	long size;
	FILE* pFile = fopen("example.txt", "rb");
	if (pFile == NULL)
		perror("Error opening file");
	else
	{
		fseek(pFile, 0, SEEK_END); // non-portable光标在文件尾不移动
		size = ftell(pFile);
		fclose(pFile);
		printf("Size of myfile.txt: %ld bytes.\n", size);
	}
	return 0;
}

结果:

上述代码结合了fseekftell,先用fseek找到文件尾,再用ftell计算了此时光标距离文件头的偏移量,这样就得出了文件长度。

6.3 rewind

让⽂件指针的位置回到⽂件的起始位置。

使用示例:

c 复制代码
/* rewind example */
#include <stdio.h>
int main()
{
	int n;
	FILE* pFile;
	char buffer[27];

	//将A~Z输出到文件中
	pFile = fopen("myfile.txt", "w+");
	for (n = 'A'; n <= 'Z'; n++)
		fputc(n, pFile);
	
	//输出后文件光标的最终位置在'Z'后
	rewind(pFile);//使用rewind调整光标回到初始位置------'A'之前

	//读取文件内容存储到字符数组buffer中
	fread(buffer, 1, 26, pFile);//从rewind调整的位置开始读取数据
	fclose(pFile);

	buffer[26] = '\0';
	puts(buffer);
	
	return 0;
}


七、文件读取结束的判定

7.1 被错误使用的feof与ferror

牢记:

在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。
feof的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束。

当我们打开一个流时,这个流上有两个标记值,一个是EOF,一个是error,我们需要使用对应的feofferror去检测标记值,我们才能确定文件结束的原因究竟是读写失败还是遇到文件末尾。

  1. ⽂本⽂件 读取是否结束,判断返回值是否为EOF(值为-1),或者NULL

  2. ⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。

    • 例如:fread判断返回值是否⼩于实际要读的个数

现在我们回过头来看看前面介绍过的顺序读写函数:

c 复制代码
//如果读取错误或读到文件末尾,返回EOF;如果读取正常,返回读取到的字符的ASCII码值
int fgetc ( FILE * stream );

//如果读取错误或遇到文件末尾,返回NULL;如果读取正常,返回的是存储读取到的字符串的字符数组的地址
char * fgets ( char * str, int num, FILE * stream );

八、文件缓冲区

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

用代码展示直观过程:

c 复制代码
#include <stdio.h>
#include <windows.h>
//VS2022 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");
	Sleep(10000);
	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;
	return 0;
}

第一阶段,已经开始写数据但数据还放在输出缓冲区,没有到文件中:

第二阶段,刷新缓冲区,数据写入文件:

第三阶段,文件关闭,刷新缓冲区,缓冲区无数据,程序结束。

这⾥可以得出⼀个结论:

因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。如果不做,可能导致读写⽂件的问题。

九、拷贝文件

结合本篇的知识,我们可以实现文件拷贝的操作,下面是代码样例:

c 复制代码
#include<stdio.h>
int main()
{
	//创建data.txt并输出字符串到该文件
	FILE* pfout = fopen("data.txt", "w");
	if (!pfout)
	{
		perror("fopen");
		return 1;
	}
	fputs("I am iron man.", pfout);
	fclose(pfout);
	pfout = NULL;

	//打开data.txt文件读取数据存入字符数组
	FILE* pfin = fopen("data.txt", "r");
	if (!pfin)
	{
		perror("fopen");
		return 1;
	}
	fseek(pfin, 0, SEEK_END);
	int offset = ftell(pfin);
	rewind(pfin);
	char s[50] = { 0 };
	fgets(s, offset + 1, pfin);
	fclose(pfin);
	pfin = NULL;
	
	//创建data_copy.txt文件并拷贝字符数组内容到该文件中
	pfout = fopen("data_copy", "w");
	if (!pfout)
	{
		perror("fopen");
		return 1;
	}
	fputs(s, pfout);
	fclose(pfout);
	pfout = NULL;
	return 0;
}

效果:


完。

相关推荐
祭の16 分钟前
IDEA旗舰版编辑器器快速⼊门(笔记)
java·笔记·intellij-idea
Easonmax21 分钟前
【JavaScript】JavaScript开篇基础(6)
开发语言·javascript·ecmascript
JavaPub-rodert21 分钟前
# Python IDE的介绍和选择 --- 《跟着小王学Python》
开发语言·ide·python·编程·开发
尘佑不尘26 分钟前
kali上安装docker,并且生成centos7容器和创建apache容器后台运行
笔记·web安全·docker·容器·apache
向阳121833 分钟前
什么是 Go 语言?
开发语言·后端·golang
Shinobi_Jack35 分钟前
Go 加密算法工具方法
开发语言·golang·哈希算法
荼靡60338 分钟前
云技术基础
开发语言·perl
好奇的菜鸟39 分钟前
Go语言的零值可用性:优势与限制
开发语言·后端·golang
qtvb19871 小时前
c# 在10万条数据中判断是否存在很慢问题
开发语言·windows·c#