文件操作详解

目录

一.为什么使用文件?

二.什么是文件?

1.程序文件

2.数据文件

3.文件名

三.二进制文件 和 文本文件?

(1)举例

(2)举例

四. 文件的打开和关闭

1.流和标准流

(1) 流

(2)标准流 

2.文件指针

3.文件的打开和关闭

(1)相对路径

(2)代码举例:绝对路径

(3)代码举例:.和..的使用,创建当面文件上一个路径

(4)知识点补充:

五. 文件的顺序读写

1.顺序读写函数介绍 

​编辑

(1)代码举例:fputc函数的使用(写)

(2)代码举例 - fgetc函数的使用(读)

(3)代码举例 - 练习

(4)代码举例 - fputs函数的使用(写)

(5)代码举例 - fgets函数的使用(读)

知识点补充:

(6)代码举例 - fprintf函数的使用(写)

(7)代码举例 - fscanf函数的使用(读)

(8)代码举例 - fwrite函数的使用(写)

(9)代码举例 - fread函数的使用(读)

六. 文件的随机读写

1.fseek

2.ftell

(1)代码举例:ftell的使用

(2)代码举例:ftell计算文件的大小

3.rewind 

七. 文件读取结束的判定

1.被错误使用的 feof

八. 文件缓冲区


一.为什么使用文件?

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

代码举例:

int main()
{
	int n = 0;
	printf("%d\n", n);
	scanf("%d", &n);//20
	printf("%d\n", n);

	return 0;
}

程序运行的时候,对n的值输入20,程序结束下次再次运行起来的时候n的值还是为0。发现上次输入的值没有保存。
因为创建的n在内存,输入的数据也是在内存,内存的数据一但程序退出,数据就还给操作系统了。所以数据放在内存中是非常不安全,不稳定的。除非程序一直不结束,你数据就一直在。
所以想把/*数据进⾏持久化的保存*/或还想得到被改了之后的值 - 就可以使用文件。

二.什么是文件?

磁盘(硬盘)上的⽂件就是⽂件。//内存和硬盘要区分开,程序运行在内存,硬盘放的数据。
但是在程序设计中,我们⼀般谈的⽂件有两种:/*程序⽂件*//*数据⽂件*/(从⽂件功能的⻆度来分类的)。

1.程序文件

程序⽂件包括源程序⽂件(后缀为.c), ⽬标⽂件(windows环境后缀为.obj), 可执⾏程序(windows环境后缀为.exe)。
解析:程序文件是自己写的代码.c文件(源代码程序),经过编译会生成目标文件,可执行程序。

2.数据文件

⽂件的内容不⼀定是程序,而是程序运⾏时读写的数据,/*比如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件*/。
解析:程序运行起来会读a文件数据,最终写到b文件里面去,a文件和b文件就是数据文件。

过去所学的代码数据的输⼊输出都是以/*终端*/为对象的,即从键盘输入数据到终端,运⾏结果显⽰到显⽰器上。
有时候我们会把信息输出到磁盘上(保存到文件里面去)-- 一但把数据存储在文件里面去,用来存储数据的文件就是数据文件。

3.文件名

⼀个⽂件要有⼀个唯⼀的/*⽂件标识*/,以便⽤户识别和引⽤。
⽂件名包含3部分:⽂件路径 + ⽂件名主⼲ + ⽂件后缀

例如: c:\code\test.txt
c:\code\ -- 文件路径
test.txt  -- ⽂件名主⼲
.txt        -- ⽂件后缀
为了⽅便起⻅,⽂件标识常被称为⽂件名。

三.二进制文件 和 文本文件?

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

二进制文件:数据在内存中/*以二进制的形式存储*/,如果不加转换的输出到外存的文件。
含义:就是数据在内存怎么存储,在外存也怎么存储,是没有变化的。
比如:用文本编译器打开看是看不懂的。

⽂本⽂件:在外存上以ASCII码的形式存储,则需要在存储前转换,/*以ASCII字符的形式存储的⽂件。*/
比如用记事本(或文本编译器)打开的文件能直接看懂里面写的信息的文件。

文本编译器:能够读懂文本文件信息,像abcdef这种,但像二进制信息读不懂,会出现大量的乱码。

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

(1)举例

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),而⼆进制形式输出,则在磁盘上只占4个字节(VS2019测试)。
为什么10000占用5个字节?//因为会把10000的5位数分别当成一个字符 - 用于文本文件。

(2)举例

代码举例 - 以二进制形式把数据写在文件里面

int main()
{
	int a = 10000;

	FILE* pf = fopen("data.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写二进制文件
	fwrite(&a, sizeof(a), 1, pf);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

总结:
1.内存存储数据的形式直接写入文件就是二进制文件,是为了持续已久的保存数据。 - 直接看,看不懂
2.把数据每一位当成字符,再以ASCLL码值转换成二进制形式写入文件就是文本文件。 - 直接能看懂
3."test.txt"就是数据文件,我们是用程序文件操作数据文件要么向文件读数据,要么向文件写数据,专门存放数据或提供数据。

四. 文件的打开和关闭

流程:
1.打开文件
2.读写文件
3.关闭文件

1.流和标准流

(1) 流

我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同。
比如:在硬盘里面写和在网络上写方式肯定不一样
比如:从硬盘上读和网络上拿数据方式也不一样。
理论上程序员要把各种各样外部设备'读'和'写'方式都搞清楚才能写代码。

为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符和数据的河。
对于程序员来说你要读数据你去'流'里面去读,你要像外部设备写数据,写到'流'里面去就可以了。
含义:流是抽象出来的概念是中间层,/*流对接不同的外部设备*/,具体怎么把流里面的数据写到文件和网络等,只需要流管理就行,不需要程序员管理。
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。

总结:
1.C程序针对⽂件、画⾯、键盘等外部设备的数据/*输⼊输出操作都是通过流操作的*/
2.⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。不用关心不同设备如何操作。
3.是用流对接外部不同设备,比如要向文件读/写数据,只要打开文件相关的流就可以了。
4.流是内存里面的一块区域,专门在程序底层设计出来的东西。 
5.做为程序员只需如何使用流,打开流,关闭流,流的底层实现如何对接外部设备不需要我们关心。

(2)标准流 

代码举例

int main()
{
	pirntf("hehe");//发现直接用pintf打印了,重来没打开什么流,就直接打印了?
	int n = 10;
	scanf("%d", &n);//直接用scanf就读了,也没有打开流,所以打开流是什么概念?

	return 0;
}

问题:为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?是打开了我们不知道。

那是因为任何一个C语言程序写好之后运行起来,默认打开了3个流:
• stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
• stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。
• stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的。默认底层打开这3个流,我们才能写数据。

stdin、stdout、stderr/* 三个流的类型是: FILE* */ ,通常称为/*⽂件指针*/。
很重要:C语⾔中,就是通过 FILE * 的⽂件指针来维护流的各种操作的,但指针文件流。

2.文件指针

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

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

不同的C编译器的FILE类型包含的内容不完全相同,但是⼤同⼩异。但是关于结构体的细节不用了解。
每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信息,使⽤者不必关⼼细节,这个结构体就是文件缓冲区。

⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便。

举例:

data.txt - 文件,假设你要对这个文件进行处理,进行读写操作,当你在读写之前要打开文件。
1.打开文件 - 打开文件这个操作就会像内存申请一块区域(文件信息区),就会创建FILE结构体类型变量。、
2.根据当面打开data.txt文件的信息来填充FILE结构体里面相关的信息。
3.同时会给一个指向这个EILE结构体指针,/*FLIE*指针*/,用这个指针能找到这个文件信息区 - 文件信息区又跟data.txt文件相关的。
4.所以后期拿到FLIE*指针变量能找到文件信息区,文件信息区维护这个文件。
5.未来想操作文件,关注*FLIE指针就行,不许要知道文件信息区的细节,也不需要知道文件信息区又跟文件如何关联的。

总结:
1.FILE确实是个结构体,这个结构体就能描述文件的相关的信息。
2.当我想描述这些信息的时候,把这些信息存起来,利用这个FILE结构体类型,会在内存创建一个区域,这块区域就被称为文件信息区。
3.当打开一个文件想使用的时候,打开文件这个操作主动的会像内存申请一块区域创建⼀个FILE结构的变量,会根据文件当前信息把结构填充起来,不用关注细节。
4.⼀般都是通过⼀个FILE*的指针来维护这个FILE结构提体变量。
5.只要打开文件就会创建一个相关的文件信息区,不需要维护,后期操作文件的时候信息区也会跟着变化。
6.通关FILE*指针管理的中间区域(文件信息区)称为流。

问题:FILE*操作文件如何获得 文件指针变量?
解决:通过打开文件方式,获得一个文件指针。

3.文件的打开和关闭

⽂件在读写之前应该先/*打开⽂件*/,在使⽤结束之后应该/*关闭⽂件*/。
在编写程序的时候,/* 在打开⽂件的同时,都会返回⼀个FILE* */ 的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。
ANSIC 规定使⽤ /* fopen 函数来打开⽂件*/, /* fclose 来关闭⽂件。*/

fopen原型:
FILE* fopen(const char* filename, const char* mode);
参数:
const char* filename:指打开文件名。
const char* mode:打开方式 - 是进行读/写,是二进制的读还是二进制写。
返回类型:
FILE*:返回一个FLIE结构体类型的指针。

fclose原型:
int fclose(FILE* stream);
参数:
FILE* stream:FILE*类型的指针。

下⾯都是⽂件的打开模式:

代码举例:操作文件

(1)相对路径

int main()
{
	//步骤:
	//打开文件,为了写
	//打开成功会返回地址,打开失败返回NULL
	//所以要检查打开成功或失败
	//.表示当前目录
	//..表示上一级路径

	FILE* pf = fopen("data.txt", "w");//直接写叫相对路径,相对于程序在的路径。
	//解析含义
	//1.使用fopen函数打开一个文件,会自动创建一个FILE结构体类型跟文件相关的文件信息区 - 流
	//2.通过在流里读写数据,同时会fopen函数会返回FILE*,来维护文件信息区 - 流


	if (pf == NULL)
	{
		perror("fppen");
		return 1;
	}
	//写文件

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

注意:代码在那个路径,创建的文件默认在那个路径 - 相对路径。

(2)代码举例:绝对路径

int main()
{
	FILE* pf = fopen("C:\\Users\\tyl\\Desktop\\fufu1.txt", "w");//这个是从跟上写的路径 - 绝对路径
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

"C:\\Users\\tyl\\Desktop" - 我的桌面路径。

(3)代码举例:.和..的使用,创建当面文件上一个路径
 

int main()
{
	FILE* pf = fopen("./../fufu1.txt", "w");//含义是当前路径,上一级路径底下打开fufu1.txt
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

这也是相对路径写法,这是一种相对于相对路径的写法。

"w"打开的方式特点:没有文件会新创建一个文件,如果文件存在并且里面有数据,代码执行过去会清理掉文件数据。

(4)知识点补充:

什么是输入输出?什么是读写?
1.自己写的程序,数据在内存上,假设你要从键盘/文件提供数据往程序内存放,这个动作是输入操作,也可以叫读操作。
2.当你想把数据写在文件/屏幕上,这时候你是把内存数据往文件或屏幕上输出,这个动作是输出操作,也可以叫写操作。

总结:当们想操作文件的时候,第一部你要打开文件,你要告诉我路径和打开方式是进行读还是写。

问题如何写?

五. 文件的顺序读写

顺序读写:写完一个紧接写下一个,一直往下写。
随机读写:写完一个,想跳着去读或跳着去写。

1.顺序读写函数介绍 

(1)代码举例:fputc函数的使用(写)

fputc原型:
功能:一次写一个字符。
int fputc(int character, FILE * stream);
参数:
int character:参数是int,因为传字符是ASCII码值。
FILE* stream:文件流
含义是在文件流写数据,写字符。

方法1:

int main()
{
	FILE* pf = fopen("fufu1.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写
	int i = 0;
	for (i = 0; i < 26; i++)
	{
		fputc('a' + i, pf);
	}

	//关闭
	fclose(pf);
	pf = NULL;

	return  0;
}

方法2:把26字母打印在屏幕上 - 用到标准输出流 (stdout)

int main()
{
	int i = 0;
	for (i = 0;i < 26;i++)
	{
		fputc('a' + i, stdout);

	}
	fputc('\n', stdout);
	fputc('a', stdout);

	return 0;
}

(2)代码举例 - fgetc函数的使用(读)

fgetc原型:
int fgetc(FILE * stream);
功能:一个读取一个字符。
参数:
FILE* stream:传的是个文件流,说明在流里面读数据
返回类型:读成功,返回字符的ASCII码值,读失败,返回EOF。

int main()
{
	FILE* pf = fopen("fufu1.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读
	int ch = 0;
	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	//关闭
	fclose(pf);
	pf = NULL;

	return 0;
}

(3)代码举例 - 练习

题目 写一个代码,将fufu1.txt文件的内容,拷贝一份生成fufu2.txt。

分析:
1.从fufu1.txt中读取数据
2.写到fufu2.txt的文件中

int main()
{
	//打开fufu1.txt进行读
	FILE* pfread = fopen("fufu1.txt", "r");
	if (pfread == NULL)
	{
		perror("fopen->fufu1.txt");
		return 1;
	}
	//打开fufu2.txt进行写
	FILE* pfwrite = fopen("fufu2.txt", "w");
	if (pfwrite == NULL)
	{
		//如果fufu2.txt打开文件失败,也要关闭fufu1.txt文件
		fclose(pfread);
		pfread = NULL;
		perror("fopen->fufu2.txt");
		return 1;
	}

	//数据读写的(拷贝)
	int ch = 0;
	while ((ch = fgetc(pfread)) != EOF)
	{
		fputc(ch,pfwrite);
	}

	//关闭文件
	fclose(pfread);
	fclose(pfwrite);
	pfread = NULL;
	pfwrite = NULL;

	return 0;
}}

文件拷贝底层原理。

(4)代码举例 - fputs函数的使用(写)

fputs原型:
int fputs(const char* str, FILE * stream);
参数:
const char* str:字符串
FILE* stream:流
含义:写一个字符串到流。

(5)代码举例 - fgets函数的使用(读)

fgets原型:
char* fgets(char* str, int num, FILE* stream);
参数:
char* str:从流里面读取,放到str指向的空间。
int num:最多读多少个,num - 1个,因为最后一个会放\0,得预留一个位置。
FILE* stream:从流里面读取。

知识点补充:

可变参数列表 - 用的非常少
在众多库函数里边只有 printf 和 scanf 这个2函数用到可变参数。

(6)代码举例 - fprintf函数的使用(写)

printf原型:
int printf(const char* format, ...);//可变参数

fprintf原型:
int fprintf(FILE* stream, const char* format, ...);
2个函数对比发现只有第一个参数不一样,只要会使用printf就会使用fprintf。

struct St
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct St s = { "zhangsan",20,90.5f };
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	//printf("%s %d %f", s.name, s.age, s.score);pirntf的使用

	fprintf(pf,"%s %d %.1f", s.name, s.age, s.score);

	//写在屏幕
	fprintf(stdout, "%s %d %.1f", s.name, s.age, s.score);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

(7)代码举例 - fscanf函数的使用(读)

scanf原型:
int scanf(const char* format, ...);//可变参数

fscanf原型:
int fscanf(FILE* stream, const char* format, ...);
2个函数对比发现只有第一个参数不一样,只要会使用scanf就会使用fscanf。

struct St
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct St s = { 0 };
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件 - 怎么放的怎么读,注意格式
	//scanf("%s %d %f",s.name, &(s.age), &(s.score));scanf的使用,scanf后面要&地址,name是数组是首元素地址。

	fscanf(pf, "%d %s %f", s.name, &(s.age), &(s.score));

	fprintf(stdout, "%d %s %.f\n", s.name, s.age, s.score);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

注意打印的时候可以.f控制长度,但读的时候不行。

总结:
1.fgetc和fputc,fgets和fputs,fscanf和fpirntf 这些函数都是以文本信息写到文件,直接能看懂的,读相反。
2.这些函数只是另一种输入输出的手段。

(8)代码举例 - fwrite函数的使用(写)

fwrite原型:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
参数:
const void* ptr:指针指向被写的数据元素,你要写那个数据,指针就指向那个地方,就是数据的起始地址。
size_t size:被写的元素的大小,单位是字节。
size_t count:一次写几个元素。
FILE* stream:写到流。

struct St
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct St s = { "zhangsan",20,95.5f };

	FILE* pf = fopen("data.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//二进制形式 写文件
	fwrite(&s, sizeof(s), 1, pf);


	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

注意:
1.字符串不管以文本形式还是以二进制形式在文本形式都是不变的。
2.对于整形浮点型他们ASCII码值不一样,导致文本形式查看,会看不懂。

(9)代码举例 - fread函数的使用(读)

fread原型:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
含义是:stream流里面的count的数据,count大小数据,读出来放到ptr指向的空间。

跟fwrite相反参数:一样,含义不一样。

struct St
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct St s = { 0 };

	FILE* pf = fopen("data.txt", "rb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//二进制形式 读文件
	fread(&s, sizeof(s), 1, pf);

	//printf("%s %d %f\n", s.name, s.age, s.score);

	//二进制形式 写在屏幕
	fwrite(&s, sizeof(s), 1, stdout);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

六. 文件的随机读写

功能:文件指针(光标)来到想来到的位置,在合理的范围内想在哪里写就在哪里写,想在哪里读就在哪里读。

1.fseek

根据⽂件指针的位置和偏移量来定位 ⽂件指针(文件内容光标)。//光标也叫文件指针。

fseek原型:
int fseek(FILE* stream, long int offset, int origin);
参数:
FILE* stream:哪个流?
long int offset:偏移量。
int origin:有3个选择。
(1)SEEK_SET - Beginning of file //从文件起始位置算偏移量。
(2)SEEK_CUR - Current position of the file pointer//文件指针当前位置。
(3)SEEK_END - End of file*//文件末尾。
第3个参数是程序员用来选择位置计算想要光标要到位置偏移量。 

代码举例:

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	int ch = fgetc(pf);
	printf("%c\n", ch);//a

    ch = fgetc(pf);
	printf("%c\n", ch);//b

	ch = fgetc(pf);
	printf("%c\n", ch);//c

	ch = fgetc(pf);
	printf("%c\n", ch);//d

	//SEEK_CUR 不想读到e,想回到起始位置a,偏移量-4,向前偏移4个字符
	fseek(pf, -4, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c\n", ch);//a

	//SEEK_END 从文件末尾计算偏移量
	fseek(pf, -6, SEEK_END);
	ch = fgetc(pf);
	printf("%c\n", ch);//a

	//SEEK_SE 从文件起始位置计算偏移量
	fseek(pf, -6, SEEK_SET);
	ch = fgetc(pf);
	printf("%c\n", ch);//a

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

总结:基于我们知道文件内容前提下。

2.ftell

功能:返回⽂件指针相对于起始位置的偏移量。含义:当你读写操作不知道文件指针(光标)在哪里了,从起始位置计算当前位置偏移量。

ftell原型:
long int ftell(FILE * stream);

(1)代码举例:ftell的使用

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	int ch = fgetc(pf);
	printf("%c\n", ch);//a

	ch = fgetc(pf);
	printf("%c\n", ch);//b

	ch = fgetc(pf);
	printf("%c\n", ch);//c

	ch = fgetc(pf);
	printf("%c\n", ch);//d

	int n = ftell(pf);
	printf("%d", n);

	fclose(pf);
	pf = NULL;

	return 0;
}

(2)代码举例:ftell计算文件的大小

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//计算
	fseek(pf, 0, SEEK_END);
	int n = ftell(pf);
	printf("%d\n", n);//6

	fclose(pf);
	pf = NULL;

	return 0;
}

补充:也可以用于来计算文件的大小,偏移量就是字节。

3.rewind 

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

rewind原型:
void rewind(FILE* stream);

代码举例:

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	int ch = fgetc(pf);
	printf("%c\n", ch);//a

	ch = fgetc(pf);
	printf("%c\n", ch);//b

	ch = fgetc(pf);
	printf("%c\n", ch);//c

	ch = fgetc(pf);
	printf("%c\n", ch);//d

	rewind(pf);
	ch = fgetc(pf);
	printf("%c\n", ch);//a

	fclose(pf);
	pf = NULL;

	return 0;
}


 

七. 文件读取结束的判定

我们在读取文件的时候,读一个写一个信息,总得判定文件读取文件结束,用的函数不一样判定的方法也不一样。

feof函数 — 在文件读取结束后,判断是否是因为遇到文件末尾而结束。

ferror函数 — 在文件读取结束后,判断是否是因为遇道错误结束。

1.被错误使用的 feof

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

(1)⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
• fgetc 判断返回值是否为 EOF .//不管读取失败结束,还是读取道末尾结束都会返回EOF。
• fgets 判断返回值是否为 NULL .//不管读取失败结束,还是读取道末尾结束都会返回NULL。

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

功能:fread判断返回值是否⼩于实际要读的个数。

fread原型:
size_t fread(void* ptr, size_t size, size_t count, FILE * stream);
参数:
FILE* stream:文件流
size_t count:实际要读的个数
size_t size:要读取的每个元素的大小,单位是字节。
void* ptr:指向的空间
含义:在文件里面读count的个数,size大小的数据放在ptr指向的空间。
返回值:真实读取到的个数。

代码举例:

(1)⽂本⽂件的例⼦:

int main(void)
{
	int c; // 注意:int,⾮char,要求处理EOF
	FILE* fp = fopen("test.txt", "r");
	if (!fp) {
		perror("File opening failed");
		return EXIT_FAILURE;//EXIT_FAILURE c语言定义好的值
	}
	//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
	while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
	{
		putchar(c);
	}
	//判断是什么原因结束的
	if (ferror(fp))//读取错误
		puts("I/O error when reading");
	else if (feof(fp))//文件末尾
		puts("End of file reached successfully");

	fclose(fp);
}

(2)⼆进制⽂件的例⼦:

enum { SIZE = 5 };

int main(void)
{
	double a[SIZE] = { 1.,2.,3.,4.,5. };//小数点后面没写默认是0
	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);
}

八. 文件缓冲区

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

注意:缓冲区不用放满也能输入输出数据过去,只要刷新缓冲区就行,fflush函数可以刷新文件指针对应的缓冲区。


代码举例:
VS2022环境下

int main()
{
	FILE* pf = fopen("test.txt", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区
	printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
	Sleep(10000);
	printf("刷新缓冲区\n");
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘),刷新pf对应的缓冲区
	//注:fflush 在⾼版本的VS上不能使⽤了
	printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
	Sleep(10000);
	
	fclose(pf);
	//注:fclose在关闭⽂件的时候,也会刷新缓冲区
	pf = NULL;

	return 0;
}

此时文件什么都没有。

​​​​​​​

刷新了缓冲区,此时程序还没结束。

为什么存在缓存冲?
是为了效率考虑,当我们把数据写到硬盘上去,这个写硬盘的动作是谁完成的?
明面上看似fputs函数完成的,事实上fupts函数底层还会调用操作系统提供的接口,然后操作系统把我们数据写到硬盘。
//硬盘的操作是由操作系统完成的。
所以说C语言这些读写函数都会再次调用操作系统提供的接口,由操作系统帮我们真实的完成数据的读写。
这些函数在执行过程中会打断操作系统,频繁的打断不合适,所以放一块缓冲区,你先把要写的数据放在缓冲区,写满了或手动刷新操作系统在帮你写一次,这样不会被频繁的打断。

这⾥可以得出⼀个结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,//需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。
如果不做,可能导致数据的丢失。因为如果发生停电,缓冲区的数据还没写到硬盘上去的动作就断电了,数据没有及时写在硬盘等于丢了。如果及时在关闭文件或刷新缓冲区,就算停电了数据也不会丢。

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值