c语言入门---文件操作

一.为什么要使用文件

我们来想一个问题就是为什么要使用文件呢?首先我先回想一下我们之前在写代码的时候,要输入或者说记录某些值的时候是不是都是暂时的啊,这些内容或者数据都是暂时存放在内存里面的,我们把这个程序关闭的话,那么这些数据都会不见了,等我们再运行一下程序的话就会发现这些内容全部都消失了,得重新输入数据,所以要想做到能够永久性的存储我们的数据的话那就只能把这些数据放到我们的数据库里面或者文件里面,所以就有了我们今天的这一篇文章来告诉大家如何来使用我们的文件。

二.什么是文件

那么我们这里就说磁盘上的文件就是文件请添加图片描述
但是在我们的程序设计中,我们一般谈的文件有两种:程序文件,数据文件(从文件功能的角度来分类)

1.程序文件

包括源程序文件(后缀为,c),目标文件(windows环境后缀为.obj),可执行程序(windows环境的后缀为.exe)。

2.数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取的文件,或者输出内容的文件。那么本篇文章要讨论的就是我们这里的数据文件,在一千各章所处理的数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上,那么起始我们有时候会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里的处理的就是磁盘上的文件。

三.文件名

一个文件需要一个唯一的文件标识,仪表用户的识别和引用,那么我们这里的文件名就包括三个部分:文件的路径+文件名主干+文件后缀,那么这里为了方便起见,文件表示常被称为文件名。

四.文件的打开或者关闭

那么这里我们就来步入正题了,我们这里来讲讲文件的打开或者关闭,那么有小伙伴们看到这个就不经笑道,这有什么好讲的啊这不是挺简单的吗?这直接鼠标双击不就可以了嘛,啊!你这里说的是使用电脑的时候的文件打开或者关闭,那么我们这里在编写程序的时候又是如何来打开或者关闭文件的呢?那么这里我们就先来了解一个点叫文件指针。

1.文件指针

我们每使用一个文件的时候,它都会在内存当中开辟一个空间,我们通常叫这个空间为文件信息去,那么这个信息区是用来存放我们这个文件相关的信息的,比如说文件的名字,文件的状态,文件的地址等等等,然后这些不同的信息又是保存到一个结构体变量的,该结构体类型是有系统声明的取名为FILE,例如我们的vs2013编译环境条共的该结构体的声明就是这样的:

struct _iobuf {
	char* _ptr;
	int   _cnt;
	char* _base;
	int   _flag;
	int   _file;
	int   _charbuf;
	int   _bufsiz;
	char* _tmpfname;
};
typedef struct _iobuf FILE;

不同的编译器实现的FILE类型包含的内容是不完全相同的,但是大同小异,然后我们每打开一个文件我们的编译器就会根据该文件的情况来自动的创建一个该结构体的变量,并且向里面填充相关的信息,那么这里使用者就不必在乎这里的细节,所以我们就可以通过这个结构体里面的信息来操作我们的文件,但是大家有没有发现一个问题就是我们这里的结构体十分的复杂,我们普通的操作者好像不是很好的能够通过这个结构体来操作我们的文件,所以我们这里就可以创建一个该结构体变量的指针,通过该指针来维护我们的这个结构体,再用该结构体来操作我们的文件,那么这样的话我们使用起来就非常的方便了,比如说下面的这个代码:

FILE * pf;

这里就是我们创建的一个FILE *的指针变量,我们可以通过该指针来找到我们某个文件的文件信息区,再通过该文件信息区来维护修改和使用我们的文件。

2.文件的打开和关闭

那么我们知道了文件指针是什么,那么我们这里就可以来看看我们的文件的打开或者关闭,首先我们要想使用一个文件的话,是不是得先想办法打卡我们的文件,那么这里的打开文件就得用到一个函数叫做fopen那么我们这里就可以来看看我们这个函数的基本介绍:
请添加图片描述
那么我们这个函数需要两个参数,我们这里可以根据这里的英文翻译可以看到这里需要的一个参数就是我们这里的文件名,另外的一个参数就是我们这里的模式,那么我们这里首先就聊聊文件名,那么这里的文件名默认的就是我们该程序路径下的文件名,如果说你想要打开其他地方文件的话,那么我们这里就得在在这里对应输入文件名的地方输入绝对路径,比如说我们这里在桌面上创建了一个文件:
请添加图片描述
那么我们这里就可以右击这个文件,然后点击这个属性就可以出现这样的页面:
请添加图片描述
那么这里的位置就是我们这里的绝对路径,如果你想要打开其他路径的文件的话,就得把这里的绝对路径输入进去而不是单独的输入一个文件名。接下来我们就可以来看看我们这里的第二个参数,我们这里讲这个参数翻译成模式,也就是说我们这里的打开文件的形式是用很多情况的,所以我们这里就得选着一种情况来打开一个文件,那么我们这里的情况就有如下几个形式:

请添加图片描述

我们这里就主要用的是前两个模式只读和只写,那么看到这里想必大家应该能够明白我们这里参数的使用方法,那么我们再来看看这个函数的返回值:
请添加图片描述
这个函数的返回值是一个FILE*类型的指针,那么我们在使用这个函数的时候就得用一个该类型的指针来进行接收,但是我们根据这个函数的介绍我们知道,当你以某些方式打开文件,但是文件却不在的话它会返回一个空指针,那么这些打开方式就是我们上面的那个表中写出错的那些方式,那么看到这里想必大家应该知道了如何来打开我们的文件,那么我们这里就可以来通过代码来演示一下:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pa = fopen("text.txt", "r");//以读的形式来打开一个文件
	if (pa == NULL)//如果打开失败则执行下面的代码
	{
		printf("%s", strerror(errno));//打印打开失败的原因
	}
	else//如果打开成功则执行下面的代码
	{
		fclose(pa);//关闭文件
		pa = NULL;
	}
	return 0;
}

那么我们这里就将这个代码运行一下,那么我们这里并没在该路径下创建该类型的文件,所以我们这里会执行if语句下面对应的代码块,那么这里就会打印错误信息,那么我们这里就来看看这个代码运行的结果为:
请添加图片描述
那么我们这里再在该路径下面创建一个文件的话就不会出现这样的问题比如说下面的图片:
请添加图片描述
我们这里就创建好了一个文件,然后我们这里再运行一下就会发现这里并没有报错
在这里插入图片描述
那么这里大家可能会遇到一个问题就是,大家的文件可能没有把这个文件的扩展名勾选上:
在这里插入图片描述
那么这样的话就会出现一个问题,你在创建一个text.txt文件的时候,你输入的名字是text.txt但是实际上它的名字变成了text.txt.txt,所以这就导致了你看上去有这个文件,但是你在用程序打开的时候却打不开,那么这样的话你就可以点击查看再把这个文件扩展名勾上,我们这里演示的是没有文件会报错的情况,那么我们这里还有另外一种情况就是如果没有该文件它会自动创建一个名字为该文件的情况,那么这里还有注意的一点就是如果你想要打开其他路径下的文件的话,你得输入它的绝对路径,比如说这样:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pa = fopen("C:\\Users\\Administrator\\Desktop", "r");//以读的形式来打开一个文件
	if (pa == NULL)//如果打开失败则执行下面的代码
	{
		printf("%s", strerror(errno));//打印打开失败的原因
		return 1;
	}
	else//如果打开成功则执行下面的代码
	{
		fclose(pa);//关闭文件
		pa = NULL;
	}
	return 0;
}

那么这些绝对路径一般都会带有一些斜杠,但是这些斜杠和字符串连接在一起的话,就会被视为转义字符,那么这里为了解决这种情况就必须得在这些斜杠的前面加上一个斜杠来转义这些斜杠,那么我们这里应该能够明白如何打开一个文件,那么我们这里就来讲讲如何来关闭文件,那么这里用到的函数就是fclose这个函数的功能就是关闭一个文件,那么我们来看看这个函数的介绍:
请添加图片描述
那么我们可以看到这个函数需要一个FILE*类型的指针,那么我们这里就可以将上面的pa放到这个函数里面,那么这个函数要注意的一点就是我们这里虽然将这个文件关闭了,但是我们这里的指针依然还指向那个文件信息区里面,所以我们在关闭这个文件之后还得将这个指针的内容初始化为空指针,这样可以防止以后的越界访问。还有一件事就是我们这里的文件是不允许无线打开的,所以我们这里的关闭文件可以为其他文件的打开提供空间,然后如果你不关闭文件的话很可能会导致这个文件的数据丢失。

3.文件的顺序读写

那么我们这里知道了如何打开一个文件和关闭文件,那么我们这里剩下来要做的就是如何对文件里面的内容进行操作,如何把文件里面的数据提取出来打印到我们的屏幕里面,那么这里我们就要来介绍一下我们下面的几个函数:
请添加图片描述
那么我们这里的操作就有如上这么多个函数来进行操作,那么我们下面将会一个一个的介绍这些函数的使用方法,那么大家在看这个表格的时候有没有想过一个问题就是这里的流是什么意思?这里的输入输出又是指的什么?那么我们先来解决后面的这个问题,把我们在程序中写的数据传到一些外接设备中的这个操作就叫输出,那么这些外接设备可以是硬盘,屏幕,光盘,网络,u盘,软盘等等,那么与之相反,将这些外接设备中的数据输入到我们的程序当中的操作就叫输入,那么这些外接设备的存储数据的原理是与我们电脑中储存数据的原理是不一样的,那么我们这里又是如何将这个数据存到这些外接设备里面的呢?那么这里就得用到我们的流,我们都是先将数据传递给我们的流,再又流对这些数据进行处理来传给我们的外接设备并进行存储,这些外接设备所对应的流都是不一样的,比如说下面的这三个流:FILE *stdin—标准输入流(键盘) FILE *stdout——标准输出流(屏幕)
FILE *stderr——标准错误流(屏幕),这就是我们常见三个流,第一个表示的意思就是我们从键盘当中输入数据,第二意思就是将我们的数据输出到我们的屏幕上面,第三个的意思就是将我们的错误信息输出到屏幕上面,但是有些小伙伴表示我们平时在写代码的时候咋没有见过这些流啥的啊,那是因为我们目前在写代码的时候一般都只用的到我们的屏幕和键盘,这些也是流也需要打开,但是我们的任何一个c程序只要运行起来的时候都会自动打开我们的上面说的那三个流,所以这里就会察觉不到,那么我们下面在打开文件的话就得自己主动的去打开对应的流了,那么通过上面的知识我们就很好的理解下面的函数的使用。

4.fgetc

那么我们这里首先来看看这个函数的基本信息:
请添加图片描述
我们可以看到这个函数需要的是一个文件类型的指针就可以,当然你要是翻译一下的话就会发现这个指针就是我们所说的流,然后这个函数返回的类型是一个整型,这个函数的功能就是读取文件的一个字符,并且将这个字符的ascall码值作为返回值传回来,并且每使用一次这个函数,那么这个读取的顺序就是从左往右进行读取,那么每读取一个字符都会往后面挪动一位来指向下一个字符,那么这里我们就可以在我们的text.txt文件中先输入一个字符串比如说这样:请添加图片描述
然后我们再用下面的代码再来进行读取:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pa = fopen("text.txt", "r");//以读的形式来打开一个文件
	if (pa == NULL)//如果打开失败则执行下面的代码
	{
		printf("%s", strerror(errno));//打印打开失败的原因
		return 1;
	}
	else//如果打开成功则执行下面的代码
	{
		int c=fgetc(pa);
		printf("%c\n", c); 
		c = fgetc(pa);
		printf("%c\n", c);
		c = fgetc(pa);
		printf("%c\n", c);
		c = fgetc(pa);
		printf("%c\n", c);
		fclose(pa);//关闭文件
		pa = NULL;
	}
	return 0;
}

我们这里使用了四次这个函数来读取我们文件中的内容,由于我们该函数一次只能读取一个字符,所以我们这里就只会打印出四个字符出来,因为我们这里每读取一次我们这里的都会向后移动一位,所以我们这里打印的值因该为a b c d,那么我们来看看这个代码的运行结果为:
在这里插入图片描述
确实跟我们想的是一样的,那么我们这个函数的用法是向文件中输入一个字符进来,那么我们这里要是想读取这个文件中的全部内容,那该怎么去做呢?那么我们这里就可以看到这个函数的介绍中说到,当我们的文件读取到末尾的时候就会返回一个EOF,那么我们这里就可以将其放到一个循环里面,如果我们读取的结果是EOF 的话我们就可以来停止循环。我们来看看这个代码应该如何去写:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pa = fopen("text.txt", "r");//以读的形式来打开一个文件
	if (pa == NULL)//如果打开失败则执行下面的代码
	{
		printf("%s", strerror(errno));//打印打开失败的原因
		return 1;
	}
	else//如果打开成功则执行下面的代码
	{
		int ch = 0;
		while ((ch = fgetc(pa))!=EOF)
		{
			printf("%c", ch);
		}
		fclose(pa);
		pa = NULL;
	}
	return 0;
}

那么我们这里就来看看这个代码的运行结果为:
在这里插入图片描述
我们这里是将文件中的内容以一个字符一个字符的形式打印了出来,那么与之功能相反的就有另外的一个函数fputc这个函数的功能就是向文件输出一个字符,那么接下来我们就来看看这个函数的基本用法。

5.fputc

同样的道理,我们来看看这个函数的基本信息:
在这里插入图片描述
那么我们来看看这个函数的基本信息,这个函数需要两个参数,其中的一个函数的就是我们这里的流,另外一个函数就是这里的字符,那么我们这里就知道了这个函数的作用就是将一个字符输出到我们这里的流里面去,那么我们这里就可以就可以通过下面的代码来看看这个函数的使用:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pa = fopen("text.txt", "w");//以读的形式来打开一个文件
	if (pa == NULL)//如果打开失败则执行下面的代码
	{
		printf("%s", strerror(errno));//打印打开失败的原因
		return 1;
	}
	else//如果打开成功则执行下面的代码
	{
		fgetc('e', pa);
		fgetc('f', pa);
		fclose(pa);//关闭文件
		pa = NULL;
	}
	return 0;
}

我们来看看这个代码运行只会我们的文件就成了这样:
在这里插入图片描述
那么这里我们发现我们的文件中确实多了两个字符一个是e另外一个就是f,但是我们这里同时的出现了一个问题就是我们这里原来的那一长串字符也同时不见了,那么这里就是我们的一个副作用就是我们这里每以写的形式来打开一个文件的话,它都会自动的清楚这个文件之前所记录的内容,那么这里我们的就得要注意一下。

6.fgets

大家在学完上面的两个函数有没有一种感觉就是好慢啊,一个字符一个字符的输入或者输出,那么我们这里有没有一种函数能够一整个字符串的输入或者输出呢?当然有我们这里的fgets就可以做到一大段字符串的输入,那么我们这里就来看看这个函数的基本介绍:请添加图片描述
那么我们这里可以看到这个函数需要三个参数,其中一个是流另外的一个就是一个整型数字,还有一个就是我们这里的一个字符指针,那么这个函数的作用就是从文件中读取整型个字符,然后将这些字符赋值到我们这个字符指针所对应的内容里面去,那么这个字符指针一般指的就是一个字符数组,那么这里我们就可以通过下面的代码来看看这个函数的具体的用法:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pb = fopen("text.txt", "w");
	if (pb == NULL)
	{
		perror("Error opening file");
	}
	else
	{
		char i = 'a';
		for (i = 'a'; i <= 'z'; i++)
		{
			fputc(i, pb);//循环往文件里面输入字符
		}
		fclose(pb);
		pb = NULL;
	}
	FILE* pa = fopen("text.txt", "r");//以读的形式来打开一个文件
	if (pa == NULL)//如果打开失败则执行下面的代码
	{
		printf("%s", strerror(errno));//打印打开失败的原因
		return 1;
	}
	else//如果打开成功则执行下面的代码
	{
		char arr[100] = { 0 };
		fgets(arr, 10, pa);
		printf("%s", arr);
		fclose(pa);//关闭文件
		pa = NULL;
	}
	return 0;
}

那么这里我们首先通过fputc函数和循环向我们的文件中输入一个个字符也就是从字符a到z
请添加图片描述
然后我们这里就再以读的形式来打开我们的文件这里我们就一下子读取10个字符出来,我们先创建一个字符数组,然后将读取出来的字符放到我们这个数组里面去,然后再将这个数组的内容打印出来我们来看看打印出来的结果:
请添加图片描述
我们可以看到这里确实打印出来了很多的字符,但是细心的小伙伴可以看到我们这里的字符串打印的只有九个字符,这是为什么呢?因为我们这里虽然写的是10个字符,但是实际上它传过来的只有9个字符,它会将最后一个字符的内容赋值为\0。

7.fputs

同样的道理,我们这里也有这么一个函数,它的作用就是将一长段字符串输出到我们的文件里面,我们来看看这个函数的基本信息:
在这里插入图片描述
那么我们这个函数需要两个参数,一个参数就是这里的指针,那么这个指针就是对应的字符串,那么第二参数就是我们的流吗,那么这里就是对应的我们这里的文件,那么我们这里就可以通过我们下面的代码来看看这个函数是如何来进行使用的:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE* pa = fopen("text.txt", "w");//以读的形式来打开一个文件
	if (pa == NULL)//如果打开失败则执行下面的代码
	{
		printf("%s", strerror(errno));//打印打开失败的原因
		return 1;
	}
	else//如果打开成功则执行下面的代码
	{
		fputs("hijklmn", pa);
		fclose(pa);//关闭文件
		pa = NULL;
	}
	return 0;
}

那么我们这里就是将这一长段字符串输出到我们的文件里面,那么这样的话我们的文件里面的内容就是这样的:
在这里插入图片描述
那么这个函数的使用也很简单啊,不必多讲。

8.fprintf

大家看了上面的函数有没有一种感觉就是这些函数好像只能处理我们的字符串啊,那我要是像对整型,浮点型,结构体来进行输入输出那又该怎么办呢?那么这里就得用到我们的fscanf函数和fprintf函数,那么我们这里首先来讲讲fprintf函数,大家看到这个函数的时候有没有感觉这个函数我们学过的一个函数很像啊,对吧就是printf函数,那么我们先来看看printf函数的基本信息:
在这里插入图片描述

那么我们这里的fscanf函数的基本形式就是这样的:
在这里插入图片描述

那么我们这里就发现这两个函数的参数就相差一个流,那么我们这里的使用的时候就很好办了,我们用的时候就可以直接在前面加一个流就可以了,那么我们来看看下面的代码:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
	float weight;
};
int main()
{
	struct s p1 = { "zhangsan",25,50.5f };
	FILE* pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		perror("Error opening file");
		return 1;
	}
	fprintf(pf, "%s %d %f", p1.name, p1.age, p1.weight);
	fclose(pf);
	pf = NULL;
	return 0;
}

那么这里我们就创建了一个结构体,我们讲这个结构体的内容输出到我们的文件里面,那么这里我们就得用到我们的fprintf函数,这个函数第一个参数就是我们的流,也就是我们这里的文件指针,然后后面的参数就是和我们平时用的printf函数的参数类型是一模一样的,但是这里要注意的一点就是我们这里的%s和%d这些输入的位置之间得用空格将其隔开,这样的话我们之后读取数据的时候也就能够更好的来识别,我们来看看这个代码运行之后我们的文件的内容变成了什么样:
请添加图片描述
我们发现确实是讲我们这里的结构体的内容复制到了我们的函数里面。

9.fscanf

那么与上面相反的功能的函数就是我们这里的fscanf它的作用就是以格式化的形式讲我们文件里面的内容读取出来,那么我们接着我们上面的代码来讲,我们上面的代码在文件中输入了一些数据,那么我们这里就可以通过这个函数将这些数据提取出来,那么我们首先看看我们的scanf函数的基本信息:
在这里插入图片描述
我们再来看看fscanf函数的基本的信息:
在这里插入图片描述
那么这里我们也可以看到我们这两个函数的参数相差也是一个流,那么这样我们就很好办了,我们就上面的代码进行修改,我们就文件中的数据读取出来,放到一个结构体变量里面去,然后再将其打印出来,那么我们的代码就如下图所示:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
	float weight;
};
int main()
{
	struct s p1 = { 0 };
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("Error opening file");
		return 1;
	}
	fscanf(pf, "%s %d %f", p1.name, &(p1.age), &(p1.weight));
	printf("%s %d %f", p1.name, p1.age, p1.weight);
	fclose(pf);
	pf = NULL;
	return 0;
}

那么这里要注意的一点就是我们这里的scanf函数在使用的时候得输入参数对应的地址,所以我们这里得在这些参数前面加一个对应的取地址符号,然后我们这里的数组之所以没有加取地址符号是因为,这里的数组名对应的本来就是一个地址,不用加,那么我们这里的代码打印出来的结果就是这样的:
在这里插入图片描述
我们可以看到这里确实是将我们这里的数据打印到了屏幕上面。那么看到这里大家有没有发现一件事就是我们这里的流就很奇怪,因为我们这里是将数据输入到我们的文件里面,所以我们这里的流写的就是文件指针,那么我们这里能不能将这里的流进行一下修改呢?我不将它输入到文件里面,我想将他输入到屏幕上面,而且我们还想使用该函数来进行输出,那么这里我们是不是就只用将我们这里的文件指针改成我们之前说的那个标准屏幕输出流来进行输出,那么他会不会输出到我们的屏幕上面呢?那么我们的代码如下:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
	float weight;
};
int main()
{
	struct s p1 = { 0 };
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("Error opening file");
		return 1;
	}
	fscanf(pf, "%s %d %f", p1.name, &(p1.age), &(p1.weight));
	fprintf(stdout,"%s %d %f", p1.name, p1.age, p1.weight);
	fclose(pf);
	pf = NULL;
	return 0;
}

那么我们这里就可以不用我们的printf函数来进行输出,我们这里可以使用fprintf来进行输出,但是我们这里就得将前面的流改成stdout,那么我们将代码运行起来就可以发现我们这里确实能将我们这里的数据打印到我们的屏幕上面:
在这里插入图片描述

10.fwrite

那么我们上面的函数都是争对的所有的输入或者输出流,那么我们这里就来看看下面的这两个函数,这两个函数他的输入和输出就只能是文件,那么我们首先来看看fwirte这个函数的基本信息
请添加图片描述
那么我们这里可以看到我们这个函数需要4个参数,第一个参数是指针,第二个参数表示的意思就是大小,第三个参数表示的意思就是数目,第四个参数的意思就是流,那么这里我们还可以看看下面对这些参数的一些介绍:
请添加图片描述
那么我们这里就可以看到我们这里的指针指向的就是我们这里的要输出的内容来自于哪里,而我们这里的size就是一个元素有多大,count就是有多少个元素,那么我们看到了这里我们就可以来看看这个函数的具体使用,那么我们这里就还是将上面的那个结构体以二进制的形式输入到我们的文件里面去,那么我们的代码如下:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
	float weight;
};
int main()
{
	struct s p1 = { "zhangsan",25,50.5f };
	FILE* pf = fopen("text.txt", "wb");
	if (pf == NULL)
	{
		perror("Error opening file");
		return 1;
	}
	fwrite(&p1,sizeof(struct s), 1, pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

那么我们这里就是以二进制的形式将这些内容输出到我们的文件文件里面,那么这样的话我们就可以来运行一下我们的这个程序,我们再来打开我们的文件看一下就会发现我们这里文件的内容似乎看不太懂:
请添加图片描述
那么这里就是以二进制的形式来将这些信息存储到我们的文件里面的,所以我们我们这里看不太懂,但是这里有小伙伴们就感到很疑惑了,我们既然看不懂的话,那写进去干嘛啊对吧,那么我们这里就要说啊,虽然我们人看不懂,但是我们的机器可是看的懂的,我们这里就可以用fread来读取我们这里看不懂的二进制文件。

11.fread

我们首先来看看我们这里的fread函数的基本信息:
请添加图片描述
那么我们这里就可以看到我们这个函数所需要的参数就跟我们之前的fwrite的参数是一模一样的,那么我们这里就可以照葫芦画瓢将我们这个文件里面的数据读取到我们的的这个程序里面来,那么我们的代码如下:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
	float weight;
};
int main()
{
	struct s p1 = { "zhangsan",25,50.5f };
	FILE* pf = fopen("text.txt", "rb");
	if (pf == NULL)
	{
		perror("Error opening file");
		return 1;
	}
	fread(&p1, sizeof(struct s), 1, pf);
	printf("%s %d %f", p1.name, p1.age, p1.weight);
	fclose(pf);
	pf = NULL;
	return 0;
}

我们可以看看这个代码的运行情况如下:
请添加图片描述
那么这里想必大家应该能够理解这些函数的使用那么我们接着往下看。

五.对比一组函数

那么我们这里就来看看下面的这些函数的区别:

scanf / fscanf  / sscanf
printf/ fprintf /sprintf

那么我们这里就来看看我们这些函数的区别首先就是我们这里的scanf和printf这里的作用就是这样的:scanf是针对标准的输入的格式化输入语句,printf是针对标准的输出的格式化输出语句。fscanf是针对所有输入流的格式化的输入语句,fprintf是针对所有输出流的格式化的输出语句,那么我们这里的sscanf和sprintf又是什么意思呢?那么我们这里就可以先看看其中一个函数的基本信息:
请添加图片描述
那么我们这里将这个黑色的字体翻译一下就知道这个函数的作用就是从一个字符串中读取数据,那么我们这里再来看看另外一个函数的基本信息:
请添加图片描述
那么我们这里就可以将这个黑字翻译一下就可以看到这个函数的作用就是将数据转换成字符串,那么我们这里是不是就可以大致的明白了我们这两个函数的作用,那么我们再来看看这两个函数所需要的参数,我们发现这个参数跟我们的scanf函数和printf函数差不多,就是在前面多了一个指针,那么我们这里就可以先来尝试一下我们这里的sprintf函数,这个函数的作用就是将我们的数据转换成字符串,那么我们这里的操作就如下面的代码所示:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
	float weight;
}; 
int main()
{
	struct s p1 = { "zhangsan",25,50.5f };
	char buf[100] = { 0 };
	sprintf(buf,"%s %d %f", p1.name, p1.age, p1.weight);
	printf("%s", buf);
	return 0;
}

那么这里我们就是将这些结构体中的数据改成了我们这里的字符串,并且放到我们的buf这个数组里面,然后我们再以字符串的形式打印这个字符数组里面的内容,我们来看看这个代码运行的结果如下:
请添加图片描述
那么我们这里确实可以将其进行转换,那么与之相反我们这里就可以改一下,我们能不能将这个字符串转换成我们这里的数据呢?那么我们这里就可以用到我们这里的sscanf我们的代码如下:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
	float weight;
};
int main()
{
	struct s p1 = { "zhangsan",25,50.5f };
	struct s p2 = { 0 };
	char buf[100] = { 0 };
	//把s中的格式化的数据转换成字符串放到buf中
	sprintf(buf, "%s %d %f", p1.name, p1.age, p1.weight);
	printf("%s", buf);
	//把字符串buf中获取一个格式化的数据到tem中
	printf("\n");
	sscanf(buf, "%s %d %f", p2.name, &(p2.age), &(p2.weight));
	printf("%s %d %f", p2.name, p2.age, p2.weight);
	return 0;
}

那么这里我们就可以看到这个打印之后的结结果却是可以看到我们这里实现了两种转换:
请添加图片描述
这里还要提一句的就是我们这两个函数是不针对流的,那么有小伙伴们就要问了这个函数有什么用啊?那么这里大家就想象一个场景就是我们网络上面的一些输入数据,比如说学生登记,你输入的一些数据,啥年龄,体重,姓名之类的这些信息,我们在前端接收的时候可能会将这些信息组合成一个长的字符串但是我们在后端的时候得将这个长的字符串进行转换,分给一个结构体里面的各个成员来进行处理,那么这样的话是不是就得将这些东西经过我们这个函数来进行处理啊,同样的道理处理完之后是不是得输出到我们前端去啊,但是我们前端的显示是用字符串的, 你是不是又得将其转化为字符串来显示啊,那么这就是我们这个两个函数的作用所在。

六.文件的随机读取

大家看了上面的几个函数的介绍以及使用方法想必大家应该能够发现一个问题就是我们上面讲的那几个函数他们在读取文件的时候好像都只能按照顺序来进行读取,只能从文件中的第一个字符开始读取,不能说从文件中的第8个字符开始读怎么怎么样的,那么为了解决这么一个问题我们就引用了这么三个函数来解决我们这个随机读取的问题。

1.fseek

首先我们来看看这个函数的基本的信息:
请添加图片描述
那么根据这个函数下面的介绍我们可以发现我们这个函数的作用就是将文件指针定位到我们想要的位置上面去,根据我们上面的学习我们发现在程序运行起来的时候我们的文件指针是指向我们文件中的第一个字符的,每指向完一次fgetc我们的文件指针就会往后移动一位,指向第二个字符,那么我们这里的fseek函数的作用就是直接将我们的这里fseek跳转到我们指定的位置去,那么我们这里就可以来看看这里的这个函数所需要的参数,第一个参数就是这里的流,第二个参数就是就是所谓的偏移量,第三个参数就是起始位置,那么这里的偏移量就是指的是以你给的起始位置开始向右的偏移量,那么这里的偏移量既可以是正数也可以是负数,那么这里的正数就是向右,这里的负数就是向左,那么这个起始位置我们该怎么来给呢?那么我们c语言就给了这么三个起始位置:
在这里插入图片描述
那么我们这里就可以根据这三个位置和你给的偏移量来确定我们想要的文件指针的位置,那么这里我们就可以看看下面代码的操作,首先我们将文件的内容改成这样:

在这里插入图片描述
然后我们只想打印这里的d和f那么我们该如何来做呢?那么这里就很简单嘛,我们这里就直接用fseek函数来移动我们的这里的文件指针,一开始我们的文件指针指向的是第一个字符,那么我们这里就以开始位置为起始位置,然后使其偏移量为3这样的话就指向的是我们这里的d,然后我们想要打印这里的f的话我们这里就可以以文件结尾的位置为起始位置,然后使其偏移量为-1,这样的话我们的文件指针此时指向的就是我们这里的f,这里大家要记住的一件事情就是我们这里文件结尾的位置是我们这个文件的最后一个字符的后面的一个位置,那么这里我们的代码实现就如下:

#include<stdio.h>
int main()
{
	FILE*pa=fopen("text.txt", "r");
	if (pa == NULL)
	{
		perror("Error opening file");
	}
	fseek(pa, 3, SEEK_SET);
	char a = 0;
	a = fgetc(pa);
	printf("%c", a);
	fseek(pa, -1, SEEK_END);
	a = fgetc(pa);
	printf("%c", a);
	fclose(pa);
	pa = NULL;
	return 0;
}

我们来看看这个代码运行的结果:
在这里插入图片描述
那么我们这里就可以看到我们这里却是是将d和f打印了出来。

2.ftell

那么这个函数的使用就和我们上面的函数的使用一一相关,我们这个函数的功能就是求得当前文件指针相对于起始位置的偏移量,那么我们这里就来看看这个函数的基本信息:
在这里插入图片描述
那么我们这里就来看看这个函数的基本信息,我们发现这个函数需要的参数就是一个流,然后返回值就是这个偏移量的值,那么我们这里就可以看看下面的代码:

#include<stdio.h>
int main()
{
	FILE* pa = fopen("text.txt", "r");
	if (pa == NULL)
	{
		perror("Error opening file");
	}
	fseek(pa, 3, SEEK_SET);
	char a = 0;
	long b = 0;
	b = ftell(pa);
	printf("当前文件指针的便宜量为: %d\n", b);
	a = fgetc(pa);
	printf("%c\n", a);
	fseek(pa, -1, SEEK_END);
	b = ftell(pa);
	printf("当前文件指针的便宜量为: %d\n", b);
	a = fgetc(pa);
	printf("%c\n", a);
	fclose(pa);
	pa = NULL;
	return 0;
}

那么我们将这里的代码运行一下来看看:
在这里插入图片描述
那么这里大家可以去数一下就可以发现这里的结果却是是对的。

3.rewind

那么这个函数的功能也很简单就是当你不知道你的函数指针跑哪去的时候你就可以通过该函数来使得你的文件指针回到起始位置,也就是文件的开头处,那么这里我们来看看这个函数的基本信息:
请添加图片描述
那么这里的参数就是一个流,我们再来看看这个函数的使用方法:

#include<stdio.h>
int main()
{
	FILE* pa = fopen("text.txt", "r");
	if (pa == NULL)
	{
		perror("Error opening file");
	}
	fseek(pa, 3, SEEK_SET);
	char a = 0;
	long b = 0;
	b = ftell(pa);
	printf("当前文件指针的便宜量为: %d\n", b);
	a = fgetc(pa);
	printf("%c\n", a);
	fseek(pa, -1, SEEK_END);
	b = ftell(pa);
	printf("当前文件指针的便宜量为: %d\n", b);
	a = fgetc(pa);
	printf("%c\n", a);
	rewind(pa);
	b = ftell(pa);
	printf("重置位置之后的偏移量为:%d", b);
	fclose(pa);
	pa = NULL;
	return 0;
}

我们来看看这段代码的运行结果为:
在这里插入图片描述

七.文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)那么我们这里就可以来看看下面的代码:

#include <stdio.h>
int main()
{
 int a = 10000;
 FILE* pf = fopen("text.txt", "wb");
 fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
 fclose(pf);
 pf = NULL;
 return 0; }

那么我们这里就是将这个10000以二进制的形式写进了我们的文件里面,那么我们这里来看看我们的文件中变成了什么样:
在这里插入图片描述
我们发现我们这个完全看不懂这个文件里面装了啥,但是我们这里不要怕我们可以通过vs这个编译器来讲我们这个进行翻译一下,我们先在源文件这里添加一下现有项:步骤是右击源文件,讲鼠标滑动到添加这时候就可以看到现有项这个选项:
在这里插入图片描述
这时候就会跳出来我们这个程序当前的文件夹中,那么我们再把这个我们创建的text.txt文件添加进去
在这里插入图片描述
然后我们就会发现我们这里的双击是打不开这个文件的,那么我们这里就得右击这个文件,然后点击这个打卡方式,然后向下滑就可以找到我们这里的一个打开方式:
在这里插入图片描述
有一个以二进制编译器的形式打开,那么我们打开之后就成了这样:
在这里插入图片描述
那么这里的 10 27 00 00可能大家就不是那么的熟悉,那么我们这里就可以讲这个16进制转换成2进制就可以发现这里就是10000的二进制但是是以小端来储存的。

八.文件读取结束的判定

1.被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是因为读取失败而导致的结束,还是遇到文件尾结束而导致的结束。

  1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
    例如:
    fgetc 判断是否为 EOF .
    fgets 判断返回值是否为 NULL .
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:
    fread判断返回值是否小于实际要读的个数。那么这个就可以这么理解我们的fread函数在使用的时候他其中有一个参数,这个参数就是你要读取数据的个数,那么这里的函数的返回值就是他已经读取的元素的个数,那么我们这里就可以将他的返回值和我们要求他读取的值来进行比较,以此来判断是否是因为读取到结尾而结束的。

那么我们这里就可以来看看下面的代码:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
	int c; // 注意:int,非char,要求处理EOF
	FILE* fp = fopen("text.txt", "r");
	if (!fp) {
		perror("File opening failed");
		return EXIT_FAILURE;
	}
	//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);
}

那么我们这里就有一个循环,那么这个循环结束的标志就是当fgetc的返回值为EOF的时候,而我们知道当fgetc读取结束的时候返回的值就是EOF那么我们这里就想知道的一个问题就是他到底是因为读取过程中出现了错误而返回的EOF还是因为读取到文件的末尾而返回的EOF,那么这里就可以用我们下面的代码来进行判断,那么这里的函数ferror函数就是这个作用,他的返回值如果为真的话就说明是读取的过程中出现了问题而导致的问题,如果为假则说明不是,那么下面的feof则是用来判断是否是因为读到文件结束而导致了读取失败,如果是的那么他的返回值就是真,就会执行下面的代码,看到这里想必大家能够明白这个原理,那么上面的代码是我们正对文本文件来做的判断,那要是二进制的文件呢?那么我们这里就可以看看我们下面的代码:

#include <stdio.h>
enum { SIZE = 5 };
int main(void) 
{
	double a[SIZE] = { 1.,2.,3.,4.,5. };
	FILE* fp = fopen("text.txt", "wb"); // 必须用二进制模式
	fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
	fclose(fp);
	double b[SIZE];
	fp = fopen("text.txt", "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);
}

那么我们这里就可以来理解理解这个代码,我们首先是创建了一个double类型的数组这个数组里面有5个元素,然后我们以二进制的形式将这些数据全部写到我们的文件夹里面,然后我们再以读的形式将其取出来,那么这里我们fread将其取出来的时候我们得用一个无符号的整型来接收这个函数的返回值,然后在使用之前我们就可以来进行一下判断,如果他的值和我们当初写进去的值一样的话,那么就表示我们这里的读取没有问题我们就可以直接将其一个一个的打印出来,如果不一样那么我们这里就可以来分析一下问题的所在。

九.文件缓冲区

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

在这里插入图片描述
那么这里我们就可以用下面的代码来验证我们的文件缓冲区的存在:

#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
点击此处获取代码

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
C语言入门到精通》是一本针对C语言学习的教程。本书以系统完整的方式介绍了C语言的基本概念、语法和应用,并通过大量的示例和练习帮助读者快速掌握C语言编程技巧。 书籍的第一部分主要介绍了C语言的基础知识。首先,它介绍了C语言的发展历程以及与其他编程语言的区别。然后,书中详细解释了C语言的数据类型、运算符、流程控制语句和函数等基本概念,帮助读者建立起对C语言编程的基础理论知识。 第二部分是关于C语言的高级应用和技巧。本书详细介绍了C语言的数组、指针、结构体和文件操作等高级特性。读者可以通过学习这些知识,掌握更加灵活、高效的编程方法,提高自己的程序设计能力。 此外,本书还包含了一些常用的C语言库函数和相关工具的介绍,如字符串处理函数、内存管理函数和调试工具等。这些内容帮助读者更好地理解和使用C语言的相关资源,提高自己的编程效率。 《C语言入门到精通》不仅提供了理论知识的讲解,还提供了大量的实战编程示例和练习题。这些示例和练习题既可以帮助读者巩固所学知识,又可以提供编程实践的机会,培养读者的编程思维和解决问题的能力。 总的来说,这本书适合初学者作为C语言学习的入门教材,也适合有一定编程基础的读者作为C语言进阶学习的参考书。无论是新手还是专业人士,通过学习这本书,都可以逐步掌握C语言的基本概念和高级应用,从入门到精通。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

叶超凡

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

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

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

打赏作者

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

抵扣说明:

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

余额充值