C语言文件操作


前言

提示:

深入了解C语言文件操作及其相关函数实现


提示:以下是本篇文章正文内容:

一、文件是什么?

磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境
后缀为.exe)。
在这里插入图片描述

数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
在这里插入图片描述
本篇博客主要讨论数据文件;

二、为什么要使用文件

使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。

三、怎么使用文件?

文件名

在我们了解怎么操作文件的时候,我们也应该了解什么是文件名:

一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。

文件的打开和关闭

文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
eg:(VS2019)
在这里插入图片描述
C语言是通过这样一个结构体来维护我们的文件的,我们可以把这个结构体看作是一个文件信息区,相当于这个结构体里面保存着被打开的文件的位置信息、大小信息、名字信息等,我们作为使用者不必关心如何去填充这个结构体,因为这些操作都是由编译器替我们去完成的,同时也无需关注该结构体的大小,不同编译器对此设计不一样,大小也会有所差异;我们只需知道,我们能够通过该结构或该结构体指针找到找到我们对应的想要操作的文件即可;也就是我们可以这么简单理解(该结构体或该结构指针就是我们想要操作的文件,每当我们想要对文件进行操作时,就是想对该结构体或结构体指针进行操作,通过该结构体或结构体指针我们能精准的对被操作文件进行操作!);
在这里插入图片描述

文件打开

C语言的文件打开是由fopen函数来实现的;
fopen是个什么函数?
没关系,我们来深入了解一下:
在这里插入图片描述
参数
filename:文件名,我们需要告诉fopen我们想要操作在那个位置,文件名是什么的文件,说人话就是,需要精确告诉fopen我们需要操作的文件;
mode:告诉编译器你需要以什么方式打开文件;
文件打开方式:
在这里插入图片描述
其中用红色笔迹写的表示打开文件的基本操作,b、+, 与其组合表示复合操作
返回值: FILE*文件指针,我们可以通过该指针去操作该文件,(可以简单理解为该指针就是我们操作的文件);
当然从上面的打开方式我们可以看处,文件也会有打开失败的时候,故C语言也对此做出了判断,当文件打开失败是就会返回NULL;否则就会返回文件指针;因此我们在对文件指针进行操作时,一定要先对该指针进行判NULL;
eg:
在这里插入图片描述

文件关闭

C语言文件的关闭也是用函数来实现的;——fclose
fclose简介:
在这里插入图片描述
参数介绍: stream 流,在这里可以简单理解为文件指针
什么是流?
对与输入输出的数据我们可以这样看待:
流是个抽象的概念,是对输入输出设备的抽象
该设备可以是文件,可以是网络,也可以是外设
流是具有方向性的,至于是输入流还是输出流则是一个相对的概念,一般以程序为参考,如果数据的流向是程序至设备,我们成为输出流,反之我们称为输入流。当我们的程序需要数据的时候就会开启输入流,反之就会开启输出流;这是数据就会像水流一样流向输入输出设备;而fclose就像在这水流之间安装了一个开关控制着“水流”的流动;
在文件操作里面我们可以这么理解:
在这里插入图片描述
返回值 :int 类型,成功关闭文件则会返回0,关闭失败就会返回EOF;

文件打开与关闭的实操

在这里插入图片描述

int main()
{
	FILE* pf = fopen("test.txt","w");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fclose(pf);
	return 0;
}

现在我们要在这个目录下建立一个文件:
在这里插入图片描述
我们可以看到成功建立了一个test.txt文件
我们以这样的方式建立的文件叫做相对路径的方式建立的文件,
什么是相对路径
就是在当前源文件所在目录下面建立一个文件;
我们还可以以绝对路径的方式建立文件:
比如我们在C:\Code\test.txt
我们在C盘Code文件下建立一个test.txt文件:
在这里插入图片描述
我们可以发现现在这个文件夹里卖什么也没有;
现在我们就在这里件一个文件:
测试代码:

int main()
{
	FILE* pf = fopen("C:\\Code\\test.txt","w");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fclose(pf);
	return 0;
}

在这里插入图片描述
我们可以发现该文件的确被创建了;

文件的顺序读写

现在文件创建好了,我们就要往文件里面写东西了:
其中写东西,也是通过函数来实现的;
在这里插入图片描述

fputc

fputc简介:
在这里插入图片描述
参数:
character:输出字符的阿斯克码值,该int值在fputc内部会被转换为无符号字符在输出;
stream:输出的对象、输出道那里去(文件流或文件)
返回值: 成功写入的话返回写入字符阿斯克码值;失败的话返回EOF
eg:
在这里插入图片描述
现在文件里卖啥也没有;
测试代码

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Code\\test.txt","w");
	if (!pf)//对文件指针进行判NULL
	{
		perror("fopen");
		return 1;
	}
	//将大写字母写入文件;
	for (int i = 0; i < 26; i++)
		fputc('A'+i,pf);
	//关闭文件
	fclose(pf);
	return 0;
}

运行结果:
在这里插入图片描述
26个大写字母的确被写入文件;

fgetc

fgetc简介:
在这里插入图片描述
参数: 文件指针(也就是文件流)
**返回值:**成功读取字符则返回字符的阿斯克码值,读取失败的话返回EOF;
eg:读取26个大写字母,并输出道屏幕
测试代码:

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Code\\test.txt", "r");
	if (!pf)//对文件指针进行判NULL
	{
		perror("fopen");
		return 1;
	}
	//将大写字母写入文件;
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c ",ch);
	}
	//关闭文件
	fclose(pf);
	return 0;
}

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

fputs

上面fgetc、fputc是针对单个字符的,现在我们来介绍专门针对字符串的;

fputs简介:
在这里插入图片描述
参数:
str:需要输出的字符串
stream:需要输出到哪里去?
返回值: 成功输出则返回非负值,若失败则返回EOF
eg:

测试代码:

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Code\\test.txt", "w");
	if (!pf)//对文件指针进行判NULL
	{
		perror("fopen");
		return 1;
	}
	//将字符串写入文件;
	int ch = 0;
	const char* p = "I love China!";
	fputs(p,pf);
	//关闭文件
	fclose(pf);
	return 0;
}

输出结果:
在这里插入图片描述

fgets

fgets简介:
在这里插入图片描述
参数:
str:用于存放读取到的字符串;
num:一次性读取多少个字符,实际上只会读取num-1个字符,最后一个字符默认是\0
stream:从哪里读取;
返回值:
如果成功读取则返回str指针,如果读取失败返回NULL
fgets运行机制:当fgets遇到\n或读到文件末尾时会停止读取,不管此时num读没读满,同时该\n也会被读取走,同时在字符串末尾自动添加\0
eg:

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Code\\test.txt", "r");
	if (!pf)//对文件指针进行判NULL
	{
		perror("fopen");
		return 1;
	}
	int ch = 0;
	//const char* p = "I love China!";
	char arr[20];
	fgets(arr,20,pf);
	printf(arr);
	//关闭文件
	fclose(pf);
	return 0;
}

fprintf

fprintf简介:
在这里插入图片描述
在这里插入图片描述
我们可以发现fprintf与printf只有第一个参数不一样;
printf主要针对标准输出流的格式化输出函数;
fprintf’是针对所有文件输出流的格式化输出函数;
上文我们说过万物皆可为文件,当然包括显示器,你把显示器当作一个文件来看待,一切都说的通了,printf是专门针对标准输出流而设计的函数;
那么按照上面的逻辑,如果我们想要从显示器(标准输出流)里面读取数据的话,我们是要先打开显示器的,但是我们似乎没有打开,但是我们还是能够使用啊!;
实际上,这些不需要我们手动打开,编译器会自动帮我们打开三个流:
1、标准输入流(stdin):键盘
2、标准输出流(stdout):显示器
3、标准错误流(stderr)
当然我们可以手动关闭标准输入流,这样我们就不能从键盘获取数据了:
在这里插入图片描述

通过该程序我们可以看到在我们关闭标准输入流之后,编译器不会等待我们的输入,直接就结束了程序;
同理我们可以关闭标准输出流,这样我们也就不能打印数据了:
在这里插入图片描述
这样的话编译器就报错了;

言归正传:
fprintf
与printf就第一个参数不一样,其它规则都是一样的,现在我们就可以把文件想象成一块显示器,然后进行输出即可:
eg:

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Code\\test.txt", "w");
	if (!pf)//对文件指针进行判NULL
	{
		perror("fopen");
		return 1;
	}
	for (int i = 1; i <= 9; i++)//在文件里面输出9x9乘法表
	{
		for (int j = 1; j <= i; j++)
		{
			fprintf(pf,"%-2d*%2d=%2d  ",j,i,j*i);
		}
		fputc('\n',pf);
	}
	//关闭文件
	fclose(pf);
	return 0;
}

在这里插入图片描述

fscanf

在这里插入图片描述
在这里插入图片描述
我们可以发现scanf与fcanf只有一个参数的差别;
事实上scanf是fscanf的一个子集,fscanf适用于所有输入流,scanf只适用于标准输入流(显示器)
当然出去第一个参数的哈别fscanf的使用规则与scanf一样;
eg:
在这里插入图片描述

fwrite

fwrite简介:
在这里插入图片描述
参数:
ptr:需要写入的数据块指针;
size:该ptr指针指向的类型的大小;
count:一次性写入多少个该类型的数据块;
srream:写到哪里去;
返回值:
在这里插入图片描述

eg:
在这里插入图片描述
我们可以看到成功写入了,但是并不像上面的测试样例一样,我们肉眼看到懂,实际上前文说了,fwrite是以二进制的形式写入数据,所以写进文件里面的都是二进制数据,我们是看不懂得,但是计算机看的懂!!;

fread

fread函数简介:
在这里插入图片描述
参数:
str:指向用于存放从stream读取出的数据的类型
size:读取的类型的大小;
count:一次性读取多少;
stream:从哪里读取;
返回值:
在这里插入图片描述
eg:
在这里插入图片描述

scanf/fscanf/sscanf区别

首先scanf、fscanf、sscanf都是格式化输入函数,用法也都是一样的,但是它们最大的区别就是在于从哪里读取数据:

scanf
在这里插入图片描述
标准输入流读取数据;
fscanf
在这里插入图片描述
任何流里面读取数据(包括标准输入流,如果fscanf第一个参数改为stdin,fscanf==scanf)
sscanf
在这里插入图片描述
sscanf:简单使用:
在这里插入图片描述

字符串里面读取数据
总的来说,就是数据的来源不一样,但是总体的读取数据的方式是一样的;

printf/fprintf/sprintf

首先printf/fprintf/sprintf功能都是一样的,都是格式化输出函数,最大的区别在于输出的对象不一样:

printf
在这里插入图片描述
将数据输入到标准输出流(显示器)
fprintf
在这里插入图片描述
将数据输出到任何流(如果第一个参数为stdout,fprintf==printf)
sprintf
在这里插入图片描述
将数据写入字符串
sprintf :eg
在这里插入图片描述

文件随机读写

我们知道文件读写一般都是从文件开头开始读写的,这样会给我们的操作造成较大的限制,我们能不能随机从某个位置开始读写呢?就是我想从那开始读写就从哪里开始读写;(有点指哪打那的意思!~)
当然可以!
为了实现这一些列操作,C语言给我们提供了常用3个随机读写函数;

fseek

fseek函数简介:
在这里插入图片描述
参数:
stream:需要操作的流
offset:偏移量
origin:从哪里开始偏移(具体如下)
在这里插入图片描述
返回值:
如果成功偏移则返回0;
如果失败了则返回非0;
eg:

int main()
{
	FILE* pf = fopen("test.txt","w+");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fputs("abcdef",pf);//将abcdef写入test.txt文件里面去;
	//然后我们利用fgetc一个一个读出来;
	int ch = 0;
	rewind(pf);//让指针回到起始位置,也就是重新指向a的位置,因为上一句fgets写完字符串后指针就指向文件末尾了
	ch = fgetc(pf);//正常来说我们会从文件开头开始读取,读取到的也就是a
	printf("%c\n",ch);//输出a
	//但是现在我们直接读取d;那么我们就可以利用fseek函数来帮助我们完成
	fseek(pf,3,SEEK_SET);//将指针从文件开头开始偏移3个偏移量,指针就指到了d的位置,下次读取的时候就会读取d了
	ch = fgetc(pf);
	printf("%c\n",ch);//输出d
	fseek(pf,-3,SEEK_END);//从文件末尾开始偏移-3个偏移量,这是指针也能指向d的位置,我们再次读取也能读到d
	ch = fgetc(pf);
	printf("%c\n",ch);//输出d
	fseek(pf,-1,SEEK_CUR);//读取完d过后指针指向了e的位置,我们用SEEK_CUR能找到当前位置指针,然后像前偏移1个单位,右回到了d
	ch = fgetc(pf);
	printf("%c\n",ch);//输出d
	fclose(pf);
	return 0;
}

在这里插入图片描述

ftell

当我们不知道文件里面的指针指向那个位置的时候,我们可以利用ftell函数,让它来告诉我们现在指针相对于起始位置的偏移量

ftell简介:
在这里插入图片描述
参数:
stream:返回那个流里面的指针的偏移量
返回值:
成功的话,返回当前位置相对于起始位置的偏移量;
失败的话,返回-1L(long int类型的-1);
eg:

int main()
{
	FILE* pf = fopen("test.txt", "w+");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fputs("abcdef", pf);//将abcdef写入test.txt文件里面去;
	//然后我们利用fgetc一个一个读出来;
	long ret=ftell(pf);//由于上一次fgets写入字符串过后,指针就指向了文件末尾,也就是f的后一位,所以此时
	                  //相对于起始位置的偏移量就是6;
	printf("%ld\n",ret);//输出6
	fclose(pf);
	return 0;
}

在这里插入图片描述

rewind

当文件很复杂,指针被我们偏移来偏移去,不知道在哪里的时候,我们除了,利用ftell( )来告诉我们偏移量之外,我们还可以通过rewind( )函数直接让指针回到起始位置,从头开始读写;!!!

rewind函数简介:
在这里插入图片描述
参数:
stream:告诉该函数要操作那个流里面的指针;
返回值:

eg:

int main()
{
	FILE* pf = fopen("test.txt", "w+");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fputs("abcdef", pf);//将abcdef写入test.txt文件里面去;
	rewind(pf);//让指针从新回到开始位置;
	int ch = 0;
	char arr[7] = { 0 };
	fgets(arr,7,pf);//从头开始读取,那么最后就会将整个字符串读取出来
	printf(arr);//输出abcdef
	fclose(pf);
	return 0;
}

在这里插入图片描述

文件读取结束的判定

被错误使用的feof函数

我们在使用文件操作函数都时候,要始终牢记,feof并不能用来判断文件是否已经读取结束;

文件读取结束的标志有:
fgetc:读取结束返回EOF
fgets:读取结束返回NULL;
fscanf:读取结束返回EOF;
fread:读取结束返回小于需要读取的数目;

而feof()函数是作用于文件已经读取结束!!!再次注意是已经读取结束,就是说我feof函数已经知道你读取结束了,我们来判断你是不是以读到文件末尾结束的,是用来判断你是以什么类型的方式来完成读取结束的;
类似的含有ferror()判断是不是由于输入输出问题而导致文件读取结束的!;

feof简介:
在这里插入图片描述
参数:
stream:需要检查的流
返回值:
如果是因为读到文件末尾而导致文件结束的则返回非0值,表示真;
否则返回0值表示假;
eg:

int main()
{
	FILE* pf = fopen("test.txt", "w+");
	if (!pf)
	{
		perror("fopen");
		return 1;
	}
	fputs("abcdef", pf);//将abcdef写入test.txt文件里面去;
	rewind(pf);//让指针从新回到开始位置;
	char arr[7] = { 0 };
	fgets(arr,3,pf);
	if (feof(pf))//指针此时指向c的位置,没有指向文件末尾,所以不是读取到文件末尾而导致文件读取结束
		printf("是因为读到文件末尾而导致文件读取结束!\n");
	else
		printf("不是因为读到文件末尾而导致文件读取结束!\n");
	printf(arr);
	fgets(arr,7,pf);
	if (feof(pf))//文件指针指向文件末尾,是因为文件读取到末尾而结束
		printf("\n是因为读到文件末尾而导致文件读取结束!\n");
	else
		printf("不是因为读到文件末尾而导致文件读取结束!\n");
	printf(arr);
	fclose(pf);
	return 0;
}

在这里插入图片描述

文件缓冲区

我们在内存和文件之间操作数据,数据最终会被输出到文件或者数据最终会被输入到内存;这些步骤不是一步就完成的,在内存和文件之间是存在一种“介质”一样的东西;这种“介质”被称为缓冲区
在这里插入图片描述
缓冲区存在于内存里面,是内存的一部分;

为什么会有缓冲区?

1、举个简单例子,就好比张三是老师,你是学生;你遇到了一个问题就去问张三,遇到一个问题就去问张三,而且其中时间间隔还很短,频率很高,这样来说,张三是不是就被你一个人给霸占了,张三也就没有机会去帮助其它同学解决问题和处理自己的工作了,这样效率很低,于是张三就对你说,你每隔30问我一次,这样是不是张三就得到了“解放”,也有时间和精力去解决其它问题;这里的缓冲区也是一样,操作系统不可能一直为你的文件输出服务,它会让你把需要输出的全“扔在”缓冲区,待缓冲区满了,在一次性送进文件里面;当然也并不一定非要缓冲区满了才推送,当缓冲区读到\n或者你利用fflush强制刷新缓冲区也是会让操作系统来帮你完成推送的,这样很大程度上解放了操作系统,让操作系统能处理更多的事情;
2、直接从文件读取数据的速度比较慢,我内存需要数据,但是我的供给跟不上啊,于是我整个程序就会停下来等你,很是浪费时间,干脆我做个缓冲区,像将数据读到缓冲区,然后我直接从缓冲区读取数据;由于直接从缓冲区读取数据,比直接从文件读取速度要快,更能从节约时间成本;

验证缓冲的存在:

#include <stdio.h>
#include <windows.h>
//VS2019 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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
(完)

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 28
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南猿北者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值