C语言中的文件操作那些事儿~~


男人过了二十岁就要学着沉稳,遇到问题不要慌,虽然我们看着目录可能都要咽一口口水,但是想一想都是为了前程,吃点苦又何妨呢?

什么是文件

文件种类

大家看到这个问题是不是都要一愣?
在这里插入图片描述
在大家的印象里面文件是不是都是上面这样一个个的小黄口袋?浅了。上面的都是一个又一个文件夹,整整意义上的文件按照功能可以分为程序文件和数据文件:
在这里插入图片描述
在这里插入图片描述
像这样的以.c .obj .exe的后缀的文件都叫做程序文件,点进去就可以在对应环境下面跑程序的。
数据文件,顾名思义就是我们创建出来存储数据的,我们之前在VS2019里面创建了成千上万的变量,等到今天兄弟们看完我的这篇博客基本上就可以实现存储数据到文件的操作了!

文件名

兄弟们看到一个文件要是说起文件名肯定就是老实巴交地人家显示什么就读什么吧,可不敢这么老实啊!
在这里插入图片描述
在这里插入图片描述
有的时候电脑的使用者常常被自己的一些操作所迷惑,比如这里,不妨把这么东西勾勾选选不影响我们年入百万的!!!
文件名其实由三块构成:文件路径+文件名主干+文件后缀
在这里插入图片描述
一般来说我们口头的文件名仅仅是文件名的主干部分啊!
稍作了解一下!

为什么要使用文件

有没有发现只要是为什么有诸如此类的问题,都可以用存在即合理这一句话来概括。
兄弟们想一想,我们之前是不是讲过生命周期这个概念?如果我们的程序结束并且我们还不对已经存储好的数据进行任何处理,那不管是什么变量什么常量,它的生命周期肯定就结束了,那么它的短暂的一生就这么结束了,只是为了帮助我们解决问题而存在,就连走的时候都不曾掀起一点波澜,专业点就是内存在程序结束之后都会自动还给操作系统,但是对于一些数据我们希望它能够长久地存在啊,这就需要介绍一下我们的文件了!跟普通的变量不同,我们的文件并不依靠内存而存在,我们的文件都是放在C盘D盘这种磁盘上的,只要磁盘不坏,我们的数据就会以文件的形式一直存在!这就是文件存在的价值!

文件的打开和关闭

在进行文件操作之前,我们都是需要进行打开文件的,不然程序怎么读得懂你的故作矜持呢?
在这里插入图片描述
fopen函数就可以帮助我们打开文件:
在这里插入图片描述
这里我们先讲一下这个打开方式:

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

这是阿涛学习的时候老师给我们总结的经验,使用老师讲解的知识应该不算侵犯知识产权吧……
这里给大家强调一下:如果是多个字符还好,兄弟们都应该知道了C语言是没有字符串类型的,但是 “ ” 括起来的内容就是字符串,如果像上面的 ***‘ w ’***这就麻烦了,我真怕兄弟们还是有那个思维定势啊,上去看到字符就用单引号括起来,我们这里参数的类型写的好好的 char* 类型啊,怎么可以传的是一个字符类型的变量呢?这么一思考应该就很容易理解了吧?
在这里插入图片描述
现在我们再来讲第一个变量:const char * filename这里的意思即使让我们输入一个字符串,这个字符串就是文件名。如果我们严格遵守文件名的定义,完完整整地输入那么长的文件名自然是极好的,但是如果我们图省事,也可以不输入文件路径,这样子编译器就会自动帮我们在当前工程下面创建!
老观众都应该知道阿涛喜欢讲解返回类型参数这些东西,这些东西其实就已经包含了很多信息了,就比如现在我们看到了一个从未见过的返回类型:FILE *,首先我们可以确认的是这个 fopen 函数最后会返回一个指针类型!这样吧我们先来上手打开一波文件再来细说!

	FILE* P = fopen("text.txt", "w");

在这里插入图片描述

在这里我们来选中FILE并且转到定义来看一看FILE究竟是何方神圣!

在这里插入图片描述
这里由于vs2019已经相当高级了,所以我们是没有办法完整看到这个FILE的阵容的,不过我们还是可以直观感受出来,这个神秘的FILE其实本质上就是一个结构体类型重命名得来的,其实里面包含的就是我们创建的这个文件的一些信息,当然了这些东西有些过于深入,就连我都还没有学到,我怀疑当初我的老师也没有学到所以才会让我们不用学的那么深入……哈哈,瞎说的呢!
所以说,每当我们打开一个文件的时候,系统都会自动创建一个FILE类型的(结构体类型)的变量,并且会自动填充好文件的一些信息,并且会返回一个FILE类型的指针变量便于我们程序员进行管理!
通过溯源的思想,我们是不是就可以通过FILE
类型的指针找到系统为我们创建的那个FILE类型的文件信息区,然后再通过文件信息区是不是就可以很方便地管理我们的文件了?
兄弟们还记不记得,就在上一片博客我们还说到过动态内存的相关知识,如果我们动态内存开辟失败,会返回一个空指针,这时候就不能一顿操作了,然后我们还需要手动释放动态内存并亲自把指针置为空?我只能说,学完文件操作之后你会惊讶地发现我们的文件操作和动态内存在整体的结构上面是高度相似的:同样需要判断返回的指针是否为空,同样需要自己关闭文件!

	FILE* p = fopen("text.txt", "w");
	//判断文件是否打开成功
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	//操作文件
	
	
	//关闭文件
	fclose(p);

这就是我们文件使用的大概流程了!

那么肯定有兄弟会有这样的一个疑问:为什么我们对文件进行操作的时候需要手动打开关闭,但是对于像鼠标键盘这样的外设的时候就不需要了呢?
那是因为对于任何一个C语言,只要运行起来就会默认打开三个流:
1.stdin–标准输入流–键盘
2.stdout–标准输出流–屏幕
3.stderr–标准错误流–屏幕

文件的顺序读写

在这里插入图片描述
关于文件的读写阿涛还是有一些心得的,毕竟刚开始的时候阿涛也是被这个东西搞得头疼的要死!
现在我们还是可以进行类比,当我们内存中还什么都没有的时候,这种时候我们需要键盘输入或者是从文件中读取数据,就叫做输入,如果此时我们的内存之中有了数据了,我们想要把数据展示出去,就需要输出,或者说写!有人这时候就在举手手了!阿涛,你看啊我们从键盘上面敲出去的那么多代码,这难道不是我们在“写”代码嘛?怎么能叫读呢?请注意,我们所说的读或者是写,都是对于内存来说的,此时此刻内存中空空如也,请问你想让内存给你写什么?

fgetc/fputc

在这里插入图片描述
在这里插入图片描述
现在我们应该已经轻车熟路了,打印字符用“%c”,所以我们应当善于记忆,这边后面是以c结尾,是不是可以联想成跟字符有关呢?或许使用一次读写一个字符?是的没错!我们将会在代码中给兄弟们讲解。

	FILE* p = fopen("10.21.txt","w");//这里我们的想法是把数据先写到我们的记事本里面,
	//然后我们才可以从记事本里面读取数据
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	for (int i = 0;i < 5;i++)
	{
		putc('a' + i, p);//本来我们这里第一个参数的类型应该是int的,而我们的使用的虽然是字符,
		//但是本质上也是字符的ASCII码值,第二个参数指的是文件流,这里刚才我也搞错了,我把文件名
		//给敲了上去,发现不是很对劲,所以我又去看了一下,这里应该是FILE* 的那个指针
		//我们这步就是把自a往后面的五个字符存储到我们的文件里面

下面我们来见证奇迹:
在这里插入图片描述
是不是完美实现了我们的愿景?当然了这个记事本要到你创建的项目那里面去找哦!
别着急,奇迹还没有结束!我们下面就要从这个文件里面把数据拿出来,拿到我们的内存里面!

	FILE* p = fopen("10.21.txt", "r");//这里是以读的方式打开文件,关于读写,可以看看上面的分析
	if (p == NULL)
	{
		perror("fopen:");
		return 1;//异常返回返回的是一
	}
	int ch=0;
	for (int i = 0;i < 5;i++)
	{
		ch = getc(p);//getc的参数只有一个,就是我们的文件流
		printf("%c ",ch);
	}
	fclose(p);

在这里插入图片描述
这里我们已经是把上次写的代码屏蔽掉了,所以我们现在所得到的完完全全就是我们从记事本里面读取到的数据!

fgets/gputs

在这里插入图片描述
在这里插入图片描述
如果c是表示字符,那么我们的s是不是就应该表示字符串?差不多吧……这哥俩叫做文本行输出输入流,就是一行一行操作!

	FILE* p = fopen("10.21.txt", "w");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	char a[] = {"abcd\nef"};//这里为什么要加上一个\n呢?为了测试文本行的行究竟是什么意思
	fputs(a,p);
	fclose(p);
	return 0;

在这里插入图片描述
这里可以看到我们的文件里面就呈现了我们刚才输出的数据,没有遇到\n就停下来,说明我们的fputs是找到\0为止的!
这里我们还可以发现,每当我们重新以写的方式打开一个文件的时候好像把上一个文件残留的部分给覆盖掉了,如果你不想进行覆盖可以考虑用“r+ ”进行追加!

	char s[10] = {0};//本来我想着设计一个指针的,但是出现了问题
	//程序会报错说s可能是一个NULL
	fgets(s, 5, p);//我们的想法是从文件里面把数据拿到我们的内存中,我们就用s来接收
	//那这中间的过程肯定会包括一个解引用,这时候如果是对NULL
	//进行解引用操作那不是乱来吗?所以我们退而求其次,用数组来解决!
	printf("%s",s);
	fclose(p);

在这里插入图片描述
请注意,这里我们是不是明明想要程序输出五个数据,但是我们的数据此时只给了我们四个数据,再来试一试:
在这里插入图片描述
依旧只给了我们四个,所以说这就是我们文本行输入的由来,是按照一行来办的,也就是我们找的是\n。此时此刻是不是有好奇宝宝想问那如果我们还没有读到那个\n咋办啊!
我知道你很急,但是你先别急!我来给兄弟们演示:

	fgets(s, 2, p);
	printf("%s\n", s);
	fgets(s, 3, p);
	printf("%s\n", s);

在这里插入图片描述
欸,从这一次和上一次我们输出的结果来看,我们是不是发现了一点端倪?为什么第一次我们明明想要fgets是的六个字符,但是只出来了 “abcd” 四个字符呢?为什么我们第二次选择了两次fgets又会出现这种情况呢?
我们之前说过的,fgets是文本行输入,是按照行来办的,找的是\n并非\0,但是我们是把一个字符串给放到我们的数组里面,只要是数组是不是就要以 ‘’\0结尾啊?那我们本来要读取六个字符,但是去除最后一个‘\0’,实际上我们能放的最多只有五个字符,而且如果我们已经找到了 ‘\n’,即使后面我们还有空间也不会读取下一行的数据了,直接一个‘\0’潇洒结尾就完事了!
而且兄弟们,看我们第二次敲出来的东西,是不是好像我们并没有对文件进行任何操作,它就自己从a跳到了b啊?这也就说明了fgets会有内部机制,大概就是每当我们调用完一次fgets,它内部的指针都会自动从结束处往下移动一个单位,不然你没法解释第二行是从‘b’开始的!

fscanf/fprintf

那我们的数据类型不光光是一个字符类型,不可能一直fgetc/fgets,万一我们要输的是一个整型?是一个结构体?那其中的格式又该做如何处理?
不要苦恼!前人之述备矣!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
printf和scanf都是老朋友了,大家在使用上面肯定不会陌生了,但是兄弟们不一定熟悉fprintf和fscanf,这里我们把这四者联合起来看,你会有一种后豁然开朗的感觉!好像文件的格式读写就是增加了一个文件流的参数啊!
那我们就好办了:

struct stu
{
	char name[10];
	int age;
	double fee;
};

	FILE* p = fopen("10.21.txt", "w");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	struct stu qxy = { "qxy",20,0.0 };
	fprintf(p, "%s %d %f", qxy.name, qxy.age, qxy.fee);
	fclose(p);

在这里插入图片描述
大家看着那个fprintf,是不是有一种特别熟悉的感觉?是不是就和我们的printf如出一辙?
别急,下面我们还会给大家表演一波,从文件中读取信息:

	FILE* p = fopen("10.21.txt", "r");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	struct stu cyt = {0};
	fscanf(p, "%s %d %lf", cyt.name, &cyt.age, &cyt.fee);//这里不是数组名的记住要加上&
	//scanf使用的时候如果是一个double类型的记得使用ld!
	printf("%s %d %f",cyt.name,cyt.age,cyt.fee);
	fclose(p);
	
	fclose(p);

在这里插入图片描述
此时此刻我们是不是完整地从文件中读取了有格式的数据?

fread/fwrite

之前给大家看过一张文件使用的图标,其中是不是有 “wb”和“rb” 啊,当时后面的解释是以二进制形式读写,那我们一下子就来精神了,以二进制读写这是个什么东西啊?
在这里插入图片描述
在这里插入图片描述
别看这两个函数的参数似乎有一点多,都是纸老虎,对于我们上手来使用的人来说没有一点难度:

	FILE* p = fopen("10.21.txt", "wb");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	struct stu cyt = {"qxy",20,0.0};
	fwrite(&cyt,1,sizeof(struct stu),p);
	fclose(p);
	
	fclose(p);

在这里插入图片描述
虽然我们嘴上说的是二进制我们人眼是很难看懂的,但是有些二进制其实我们也是能看得懂的哈……
那我们再来给兄弟们演示一波:

	FILE* p = fopen("10.21.txt", "wb");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	char arr[10] = { 1,2,3,4,5 };
	//struct stu cyt = {"qxy",20,0.0};
	//fwrite(&cyt,1,sizeof(struct stu),p);
	fwrite(arr, sizeof(arr), 1, p);

	fclose(p);
	}

![在这里插入图片描述](https://img-blog.csdnimg.cn/c23328c9f3484dcc87db1db11acecfd6.png

这个我不相信兄弟们能够看得出来是什么东西了吧……
人眼虽然识别不出来二进制代码的信息,但是我们的是计算机最擅长的事情就是处理二进制数据啊!
见证奇迹!

FILE* p = fopen("10.21.txt", "rb");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	char arr[10] = {0};

		fread(arr, sizeof(arr), 5, p);
		for (int i = 0;i < 5;i++)
		{
			printf("%d ",arr[i]);
		}
	

	fclose(p);

在这里插入图片描述
兄弟们来看一看,是不是正如我们所说,计算机完美识别出了二进制里面的信息?
那我们刚才还有一个结构体类型的,也顺手打印一下!
在这里插入图片描述

文件的随机读写

fseek

我们之前的读写全都是特别死板的从文件的开始往后一个一个字符的读写,但是我们程序员就是程序里面的king,我们拥有相当大的自主权,只要自己的水平足够,只要执行的操作时程序所认可的,我们就可以做到一些很神奇的事情,就比如这里的随机读写:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
就比如我现在已经自己在文件里面写好了内容了,我想要直接读取这个c,我该怎么办?

	FILE* p = fopen("10.21.txt", "r");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	fseek(p, 2, SEEK_SET);
	int ch = fgetc(p);
	printf("%c",ch);
	fclose(p);
	return 0;

在这里插入图片描述
SEEK_SET表示从文件指针开始偏移
SEEK_END表示从文件指针末尾偏移
SEEK_CUR表示从当前指针位置偏移
为什么会有当前位置一说呢?兄弟们还记不记得我们的fgetc每当我们读取完一个字符后,它都会悄悄地给我们把字符往后移动一格,是不是就有了当前位置这个说法?
兄弟们还要记住一点,fseek函数是支持正负偏移的,正表示向后,负表示向前!

ftell

在这里插入图片描述
ftell是能够告知我们,我们当前指针的位置相较于起始位置偏移了多少!

说起来好像不是很难啊,来兄弟们读一下这段代码:

	FILE* p = fopen("10.21.txt", "r");
	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	int ch = fgetc(p);
	printf("%c\n",ch);
	fseek(p, 2, SEEK_CUR);
	ch = fgetc(p);
	printf("%c\n",ch);
	printf("%d\n",ftell(p));
	fclose(p);
	return 0;

偏移量是多少?
是不是1+2=3?
在这里插入图片描述
兄弟们看好,最后我们还使用了一次fgetc,因此,我们的指针在最后时刻还是往后面偏移了一下!

rewind

有缘之人可以逆转时光回到过去 在我们的C语言里面,还真的有类似于回到过去的操作,就是我们的 rewind
在这里插入图片描述

	if (p == NULL)
	{
		perror("fopen:");
		return 1;
	}
	int ch = fgetc(p);
	printf("%c\n",ch);
	fseek(p, 2, SEEK_CUR);
	ch = fgetc(p);
	printf("%c\n",ch);
	printf("%d\n",ftell(p));
	rewind(p);
	printf("%d\n", ftell(p));
	fclose(p);
	return 0;

在这里插入图片描述
我很希望这个世界上真的反方向的钟是真的存在的,或者说我可以拥有操控时间的能力,不是为了回到过去改变过去,只是为了回到那个爱你的时空,回到你爱我的时空!
虽然现实中不存在这种超能力,好歹我们的C语言帮助我们圆了一个梦,所以我们更要好好学习啊!

文件读取结束的判断

在这里插入图片描述
兄弟们一定要注意啊,这个函数不是为了判断我们的文件是否读取结束,而是我们的文件此时此刻已经读取结束了,我们用它来判断函数结束的原因,是遇到什么错误?还是读取完了整个文件,事了拂身去,深藏功与名。
这里我们直接使用cpp网站上面的代码给兄弟们讲解:

/* feof example: byte counter */
#include <stdio.h>

int main ()
{
  FILE * pFile;
  int n = 0;
  pFile = fopen ("myfile.txt","rb");
  if (pFile==NULL) perror ("Error opening file");
  else
  {
    while (fgetc(pFile) != EOF) {//循环表示如果fgetc没有返回EOF就会继续
      ++n;
    }
    if (feof(pFile)) {//了解feof的返回值
      puts ("End-of-File reached.");
      printf ("Total number of bytes read: %d\n", n);
    }
    else puts ("End-of-File was not reached.");
    fclose (pFile);
  }
  return 0;
}

在这里插入图片描述
我们使用feof就是使用的它的返回值来进行判断:相信以兄弟们的英文水平是很容易读懂上面的这一小串英文的:如果我们读到了文件的末尾,会返回一个非零的值,否则我们就返回零!
这里我们还要注意上面有注释的那一行循环:

1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。

文件缓冲区

缓冲区这个概念我们在讲scanf和getchar的时候应该说过一嘴,我们写下的代码不是直接就呜啦啦塞进变量或者文件的,都是应该先放在缓冲去里面,这里阿涛是真的很想给兄弟们演示一下效果的,但是想要演示的话,就要使用到一个fflush这个函数刷新缓冲区:fflush(pf)刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)!
但是我的vs2019好像不支持fflush的使用,所以只能靠意念给兄弟们描述,意思就是我想要把内存中的一些数据给写进我们的文件里面,我们此时已经写好了,程序也已经跑起来了,但是我们此时打开文件,发现里面空空如也,这就是因为我们并没有刷新缓冲区的原因!
在这里插入图片描述
就像像缓冲地带一样,可以这么理解吧,兄弟们要是实在想知道具体的内容,可以在网上搜索网友们的文章!!

好了那么到这里,我们的文件操作也就告一段落了,阿涛在开始的时候就跟兄弟们保证过,只要你看完这篇文章,那么至少你初步读写个数据肯定是不成问题的,兄弟们下来还是要自己去实践哦!
希望我的这篇博客对兄弟们或多或少有些帮助!
还是那句话:百年大道,你我共勉!!!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值