【文件】C语言文件操作及其使用总结篇【初学者保姆级别福利】

【文件】C语言文件操作及其使用总结篇【初学者保姆级别福利】

一篇博客学好动态内存的管理和使用
这篇博客干货满满,建议收藏再看哦!!
求个赞求个赞求个赞求个赞 谢谢🙏

先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力 看完之后别忘记关注我哦!️️️
本篇基本上涵盖了初学阶段动态内存的所有知识点,喜欢的伙伴一定要看完哦!

强烈建议本篇收藏后食用~ 干货满满!

看完这篇博客,相信伙伴们可以基本上掌握以下内容:
1.为什么使用文件
2.什么是文件
3.文件的打开和关闭
4.文件的顺序读写
5.文件的随机读写
6.文件读取结束的判定
7.文件缓冲区

前言

1.为什么要使用文件
在以前的C语言学习当中,我们了解到,我们在运行程序的时候所定义的数据,是保存在内存中的,有可能是栈区上,有可能是堆区上,也有可能是在其它地方上,但是,这些地方,都是属于内存上的。在内存上的数据,是计算机在程序运行的时候所开辟出来的,因此,在程序结束的时候,内存上的空间都会被返还给内存,因此我们是做不到数据的持久化保存的。
这时,我们就需要用到文件。使用文件,我们就可以将数据直接存放在电脑的硬盘上,做到了数据持久化。

什么是文件?
磁盘上的文件是文件。但是在程序设计中,我们一般所指的文件有两种:程序文件、数据文件
关于这部分知识相对于没有那么的重要,博主在这里就不展开细讲,想了解的小伙伴可以给我私信留言哦。

文件的打开和关闭

文件指针

首先,我们要知道,每一个被使用的文件都在内存中开辟了一个相应的文件信息去,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是要保存在一个结构体当中的。但是这个结构体是不用我们自己声明的,系统已经帮我们声明好了,已经被取好名了,为FILE

另外,每次我们打开文件的时候,系统都会自己生成一个FILE结构的变量,里面有关信息系统会自己录入,我们作为使用者暂时不必关心细节。

OK。接下来,我们想要找到这个系统给我们准备好的结构体并使用它,我们就要通过文件指针找到这个文件。

FILE*pf;//文件指针变量

文件的打开和关闭

有了pf这个指针,我们在操作文件之前,就要通过pf来打开文件,在使用结束后一定要记得关闭文件

想要打开和关闭文件,我们要用到两个函数:
fopen()和fclose()
我们先来看这两个函数的原型:

int main()
{
	//打开文件
	FILE* fopen(const char* filename, const char* mode);
	//关闭文件
	int fclose(FILE * stream);
	return 0;
}

在fopen()函数中,filename指的是文件名,这个很好理解,fclose()函数中stream指的是,流。关于流的概念,我会在后面继续讲解,这里指的就是我们用FILE*类型定义的函数指针。
mode,指的是文件的打开方式,为char*类型。
文件的打开方式有如下几种

文件使用方式含义如果指定文件不存在
“r”(只读)为了输入数据,打开一个已经存在的文本文件err
“w”(只写)为了输出数据,打开一个文本文件建立一个新的文件
“a”(追加)往文本文件尾添加数据建立一个新的文件
“rb”(只读)为了输入数据,打开一个二进制文件err
“wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
“ab”(追加)向一个二进制文件尾添加数据err
“r+”(读写)为了读和写,打开一个新的文件err
“w+”(读写)为了读和写,建立一个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
“rb+”(读写)为了读和写打开一个二进制文件err
“wb+”(读写)为了读和写,创建一个新的二进制文件建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

例子:

int main()
{
	FILE*pf1=fopen("test.dat", "w");//这种是相对路径,是要放到程序那个指定文件夹里面才能被找到
	FILE* pf2 = fopen("D:\\test.dat", "r");//有位置的叫绝对路径,是可以找到的
	                                      //注意\如果只有一个,是会被理解成转义字符的
	                                      //所以\都要变成两个
	if (pf1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件

	//关闭文件
	fclose(pf1);
	pf1 = NULL;
	return 0;
}

在这里,我想给各位小伙伴讲一下使用使用文件的一些好习惯和细节:问下大家有注意到fopen()后面这一段代码吗
if (pf1 == NULL) { perror("fopen"); return 1; }
这一段代码的作用,就是防止文件打开失败,而fopen()在文件打开失败的时候,是会返回空指针的,如果我们不加以检验就对pf进行使用,注意:此时pf是个空指针,是会出问题的,所以,我们需要给pf验空。
另外,在我们fclose()关闭文件之后,必须要将pf置为空指针,防止后面对pf进行非法的解引用操作。

文件的顺序读写

基本函数的介绍

文件的顺序读写,我们必须知道一些必要的文件操作函数

功能函数名适用于
字符输入函数fgetc所有输入流
字符输出函数fputc所有输出流
文本行输入函数fgets所有输入流
文本行输出函数fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread文件
二进制输出fwrite文件

例子

int main()
{
	FILE* pf = fopen("test.dat", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fputc('b', pf);//往pf所指向的文件里面写入一个b
	fputc('i', pf);
	fputc('t', pf);
	//按顺序写的
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

我们打开后台文件夹,就可以找到test.txt这个文件了,我们可以看到bit是按照顺序录入进了这个文件当中。
在这里插入图片描述

流的概念

流是一个高度抽象的概念
程序-屏幕/硬盘/优盘/光盘…
各种硬件读写方式也不同
为了让写程序的人不用那么麻烦
所以在程序和硬件之间加一个流的概念

C程序只要运行起来,就默认打开了三个流
stdin-标准输入流-键盘
stdout-标准输出流-屏幕
stderr-标准错误流-屏幕
以上三个流都是FILE*类型的

因此我们可以得出,文件是一种流,屏幕键盘也都是流,而流一般用stream表示。
例子:

//想用fputc给屏幕打印数据
int main()
{
	fputc('b', stdout);
	fputc('i', stdout);
	fputc('t', stdout);
	return 0;
	//屏幕也是个流,文件也是个流
	//我们写数据可以往文件里面写,当然也可以向屏幕上写
}

在这里插入图片描述

一些文件操作函数使用的例子

例子1:

int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
	}
	//读文件
	int ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

分析:

这里的文件是用"r",也就是读取的方式打开的,我们通过fgetc()函数一次读取一个文件里面的字符,并存进ret后打印。因此,如果我们一开始要现在指定的文件夹里面先写入一些字符,我们F5运行起来之后,我们就可以看到这些打印的字符了。
在这里插入图片描述
> 运行后:
在这里插入图片描述

例子2:

//读键盘
//从标准输入流里面读取信息
int main()
{
	int ret = fgetc(stdin);//从标准输入流读取信息
	printf("%c", ret);
	ret = fgetc(stdin);
	printf("%c", ret);
	ret = fgetc(stdin);
	printf("%c", ret);
	return 0;
}

分析:
标准输入流(键盘)里面输入信息之后,用printf()打印出来。
在这里插入图片描述
例子3:

int main()
{
	FILE* pf = fopen("test.dat", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件-按照行来写
	fputs("abc\n",pf);
	fputs("qwertyuiop", pf);
	//这种都叫以文本的形式写进去
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

经过上面的学习,这些代码都比较好理解,这里就不赘述了。
运行之后:

在这里插入图片描述
例子4:

int main()
{
	FILE* pf = fopen("test.dat", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char arr[10] = { 0 };
	fgets(arr, 4, pf);
	printf("%s", arr);
	fgets(arr, 4, pf);
	printf("%s", arr);
	//这里的4,事实上其实只会读三个,因为要留位置给\0
	//后面接着读是不会从第二行开始读的,要把内容读完才行
	return 0;
}

注意: arr表示从文件录入的数据存入arr中,4表示录入4个字符,pf指向那个文件
但是,此时的录入的4个字符,只有3个是有效的,因为要留位置给'\0'
后面接着读是不会从第二行开始读的,要把内容读完才行

运行:
在这里插入图片描述
在这里插入图片描述

fprintf、fscanf、sprintf和sscanf函数的使用

fprintf()函数和fscanf()函数

fprintf()原型:int fprintf ( FILE * stream, const char * format, ... );
fscanf()原型:int fscanf ( FILE * stream, const char * format, ... );
通过对比我们发现,这两个函数分别比printf,scanf只多了一个参数FILE*stream,这个参数,也就是一个流。
那么我们通过两个例子,就可以很好的理解这两个函数。

例子1:

struct S
{
	char arr[10];
	int num;
	float sc;
};
int main()
{
	struct S s = { "abcdef",10,5.5f };
	//现在想把它放到文件里面
	//对格式化的数据进行写文件
	FILE* pf = fopen("test.dat", "w");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fprintf(pf, "%s %d %f", s.arr, s.num, s.sc);
    fclose(pf);
    pf=NULL;

很容易理解,我们想要进行格式化输出的时候,只需要在fprintf()的第一个参数上传我们要操作的流,后面的参数和printf()的使用完全一致。同理,我们也可以很好的理解fscanf()这个函数。

例子2:
现在我们想要把刚才放进test.dat里面的数据还原到一个结构体中来。

struct S
{
	char arr[10];
	int num;
	float sc;
};
//能否读出来,还原成一个结构体?
int main()
{
	struct S s = { 0 };
	//现在想把它放到文件里面
	//对格式化的数据进行写文件
	FILE* pf = fopen("test.dat", "r");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	fscanf(pf,"%s %d %f", s.arr, &(s.num), &(s.sc));
	//打印以下看看结果
	printf("%s %d %f\n", s.arr, s.num, s.sc);
	fclose(pf);
	pf = NULL;
	return 0;
}

思考:
通过以上两个例子,我们就可以很好的明白fprintf(),fscanf()这两个函数的使用。此时,我们要思考:我们前面说了,键盘,屏幕也都是流,那么,我们将fprint()fscanf()的第一个参数改成标准的输出输入流,那么它们的作用,就是相当于printf()scanf()啊。
事实确实是如此:
fprintf(stdout,...,...)printf(...,...)等价。
fscanf(stdin,...,...)scanf(...,...)等价。

sprintf()函数和sscanf()函数

其实这两个函数的运用并不属于文件这一部分的内容,但是我们讲到fprintf(),fscanf(),printf,scanf()这些函数,那我们也把这最后两个名字相似的函数讲了,给伙伴们区分一下。

scanf()针对标准输入的格式化的输入语句-stdin
fscanf()针对所有输入流的格式化的输入语句
sscanf()从一个字符串中读取一个格式化的数据

printf()针对标准输出的格式化输出语句-stdout
fprintf()针对所有输出流的格式化输出语句
sprintf()把格式化的数据,转成字符串

例子:

//sprintf/sscanf
//把格式化的数据写到一个字符串里面
struct S
{
	char arr[10];
	int age;
	float f;
};
int main()
{
	struct S s = { "hello",20,5.5f };
	//能否转化为一个字符串呢
	char buf[100] = { 0 };
	sprintf(buf, "%s %d %f", s.arr, s.age, s.f);
	printf("%s\n", buf);
	//能否从buf字符串中还原出一个结构体
	struct S tmp = { 0 };
	sscanf(buf, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.f));
	printf("%s %d %f\n", tmp.arr, tmp.age, tmp.f);
	//两次printf的结果应该是一样的
	return 0;
}

文件的二进制读写

两个函数:fwrite()函数和fread()函数

fwrite():
原型: size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
第一个参数表示被写数据(可以是个数组)的地址,第二个参数表示每一个数据的大小(如果是数组,就是每个元素的大小),第三个参数表示数据个数(如果不是数组,就是1,如果是数组,就是数组元素个数,第四个参数表示流。
fread():
原型:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
参数含义和fwrite()相同。

顾名思义就是用二进制的形式读写,是我们看不懂的,我们通过两个例子就可以很好的理解这两个函数。

例子:

//二进制的读写
struct S
{
	char arr[10];
	int num;
	float sc;
};
//fwrite
//size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
int main()
{
	struct S s = { "abcde",10,5.5f };
	//二进制的形式写
	FILE* pf = fopen("test.dat", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fwrite(&s,sizeof(s),1,pf);
	//后台打开文件,我们是看不懂的,因为是二进制形式搞进去的
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
我们是看不懂后台输入文件的含义的,因为我们是以二进制的形式输入的,要想读懂,我们必须通过fread()函数,以二进制的形式读。

//我们要用fread才能看懂
//fread
struct S
{
	char arr[10];
	int num;
	float sc;
};
//fread
//size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
int main()
{
	struct S s = {0};
	//二进制的形式读
	FILE* pf = fopen("test.dat", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读
	//参数和fwrite一样
	fread(&s, sizeof(struct S), 1, pf);
//都出来之后我们打印出来看看
	printf("%s %d %f\n", s.arr, s.num, s.sc);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
通过fread(),我们就成功读出fwrite()刚刚写进去的内容了。

文件的随机读写

想要掌握文件的随机读写,我们必须掌握的其中一个函数就是fseek()函数

fseek():
这个函数的作用就是用来调整文件指针位置的。
比如说,后台的test.dat里面有一串字符串abcdefg,但是我读的时候不想从头开始读,那么我们就可以通过fseek()函数来调整文件指针的位置,让它从a的位置挪开。
原型:int fseek ( FILE * stream, long int offset, int origin );
第一个参数表示针对的流,第二个参数表示调整后的文件指针相对于原始位置的偏移量,第三个参数表示文件指针的原始位置。
在此其中,第三个参数中我们常用的三个量为:
SEEK_CUR;//current position of file pointer指针当前位置
SEEK_END;//end of file文件末尾
SEEK_SET;//beginning of file文件开头

ftell():
这个函数返回一个整型,告诉我们当前文件指针相对于起始位置的偏移量。
rewind():
这个函数可以让文件指针回到起始位置
那么上面这些函数怎么用呢,我们看一个例子就明白了

例子:

int main() {
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL) {
		perror("fopen");
	}
	char ch = 0;
	//先打开后台在里面写入abcdefg
	//现在里面已经有abcdefg了
	ch=fgetc(pf);
	printf("%c\n", ch);//此处打印的是a
	//现在a已经读完了,文件指针指向的是b,那么怎么跳过b,c直接读d呢?
	fseek(pf, 2, SEEK_CUR);//用fseek()调整一下指针位置,跳过两个字符

	ch = fgetc(pf);
	printf("%c\n", ch);//此处打印的是d

	int ret = ftell(pf);
	printf("%d\n", ret);//此处打印的是4

	rewind(pf);
	ret = ftell(pf);
	printf("%d\n", ret);//此处打印的是0,因为pf已经被rewind了
	
	fclose(pf);
	return 0;
}

文件读取结束的判定

文件读取结束的判定
被错误使用的feof:在很多文章中和教学中经常说feof可以判断结束,其实是不准确的
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件是否结束
feof的作用是用于文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束,如果feof返回值为真,表示正常读取结束;如果为假表示文件读取失败而结束读取。

文本文件读取是否结束,是通过fgetc函数的返回值是否是EOF来判定的
fgets函数正常读取结束的时候会返回空指针,正常读取的时候,返回放字符串的空间起始地址
fread函数在读取的时候,返回的是实际读取到的完整元素的个数
如果发现读取到的完整的元素的个数小于指定的元素个数,这就是最后一次读取了。

例子:
写一个函数拷贝一个文件:

//写代码把test.txt文件拷贝一份,生成test2.txt文件
int main()
{
	FILE* pfread = fopen("test.txt", "r");
	if (pfread == NULL)
	{
		return 1;
	}
	FILE* pfwrite = fopen("test2.txt", "w");
	if (pfwrite == NULL)
	{
		fclose(pfread);
		pfread = NULL;
		return 1;
	}
	//读写文件
	int ch = 0;
	while ((ch = fgetc(pfread) != EOF))
	{
		//写文件
		fputc(ch, pfwrite);
	}
	//判断读取是因为什么结束的
	if (feof(pfread))
	{
		printf("遇到文件结束标志,文件正常结束\n");
	}
	else if (ferror(pfread))
	{
		printf("文件读取失败结束\n");
	}
	//关闭文件
	fclose(pfread);
	pfread = NULL;
	fclose(pfwrite);
	pfwrite = NULL;
	return 0;
}

文件缓冲区

文件缓冲区
程序数据区(内存)<>硬盘
在这其间存在输出缓冲区和输入缓冲区

例子:
这个例子不是本章重点,只是让伙伴们理解一下文件缓冲区的概念,小伙伴可以复制代码按照指示运行一下就可以理解了,这里不再赘述。

#include<windows.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区
	printf("睡眠10s-已经在写数据了,打开test.txt文件,发现文件没有内容\n");
	Sleep(10000);
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(硬盘)
	//注:fflush在高版本vs上不能使用了
	printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
	Sleep(10000);
	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;
	return 0;
}

尾声

能看到这里的小伙伴,如果你已经完全理解了这篇博客的内容,相信你对文件的理解已经提高了一个层次了。如果感觉这篇博客对你有帮助的话,别忘了你的赞,收藏和关注哦

posted @ 2022-01-22 12:05  背包Yu  阅读(2)  评论(0编辑  收藏  举报  来源