【C语言】万字解读 — 关于文件的读写函数

前言:
对于在c语言中,我们通常来说运行之后就会销毁,那么运行结果就无法保留,但是我们同样可以让c语言和文件做一个有机关联,那就是对于文件的各种函数。

Tip:下面的函数定义和参数,都可以到cpulspuls网站上查找到,链接如下:

点击即可 → cplusplus.com

一.文件的打开

首先,对于读写有两种:顺序读写和随机读写。

这两者的区别在哪里我们可以往下去看,但在c语言和文件之间关联起来,首先要打开和关闭文件,而打开文件的方式同样是一个函数,关闭也一样。

在这里就不过多阐述打开和关闭函数,只做简单的说明,如果想有更深刻的了解可以到cpulspuls中查找其定义学习,也可以参照博主的另一篇文章详细学习文件操作,更有利于食用本篇博客:【C语言进阶】从入门到入土(文件操作篇)

对于文件的打开,我们用到的函数是fopen

FILE * fopen ( const char * filename, const char * mode );

比如说我要打开我的data文本文档进行操作就是:

FILE* pf = fopen("E:\\代码库\\test 10.10\\data.txt", "r");
//返回的指针存储起来   " "里面的是打开文件的路径   " "这里是对文件进行指定操作

附录:打开文件操作表

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

然后关闭函数就是fclose

int fclose ( FILE * stream );

对于打开文件使用后,一定要记得关闭,并且把存储文件地址的指针置为空指针,不然会很危险:

    //关闭文件用fclose函数
	fclose(pf);//关闭刚刚打开文件的指针
	
	pf = NULL;//同时这里也要将pf置为空指针

这里就简单介绍完打开和关闭文件的代码了,如果更详细还是可以去看博主的另一篇文章哦,相信接下来看下面的代码你也可以get到这两个函数的作用。


二. 顺序读写

1.fputc 和 fgetc

在顺序读写中,我们首先要理解对于内存中的数据,可以输出也就是写到文件中,用到的函数是fputc。而从文件中读取到数据也就是输入,用到的是fgetc的函数。

然后我们看一下fputc的参数和定义:

释义:将字符写入流并推进位置指示符。也就是字符写在流的内部位置指示符所指示的位置,然后由一个位置指示符自动前进。

其实意思就是这个函数是写入字符的,每次写入一个字符,然后对于指向它的指针也会移动一个位置,当下一次写入的时候,就可以写入在最后面。

参数:

int fputc ( int character, FILE * stream );

他的定义中写道,前面的int c是字符写入流,而后面的FILE * stream 就是我们熟悉的写入位置的地址了。所以我们按照前面的打开代码再加上写入:

int main()
{
	//打开文件
	FILE* pf = fopen("E:\\代码库\\data.txt", "w");
	//注意这里我的操作已经变成'w'(写)了,就是写入了
	if (pf == NULL)
	{
		perror("fopen");//防止出现文件打开错误仍运行下去
		//当文件错误,perror函数会读取返回的错误值报告错误
		return -1;
	}
	//写文件
	fputc("h", pf);
	fputc('e', pf);
	fputc('l', pf);
	fputc('l', pf);
	fputc('o', pf);

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

	return 0;

}

当我们运行起来之后控制台中没有出现东西,其实就是运行没有错误的意思,然后当我们打开我们的data.txt文件的时候,我们可以看见,写入的hello已经在里面了:

而顺序读写有以下这些函数:

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

在这里,什么是流呢?

其实流是一个高度抽象的概念,你可以把他理解为水流。比如说我们的文件,屏幕,网络,光盘软盘等一些外部设备,就可以由流去对其读写。在c语言中的printf输出于屏幕,scanf输入于键盘,而流就在这些程序之间。对于c语言来说,只要运行起来,就会打开三个流:标准输出流(stdout),标准输入流(stdin),标准错误流(stderr),这三个流也都是FILE *的。

所以对于FILE *的流,我们可以直接像是上面的函数一样使用,比如想要输出就可以直接输入在屏幕的stdout

int main()
{

	fputc('h', stdout);
	fputc('h', stdout);
	fputc('h', stdout);
    //运行结果:hhh
	return 0;
}

然后对于读取文件上的东西的时候我们就用到fgetc这个函数:

int fgetc ( FILE * stream );

fgetc,从流里面读取数据,返回值是int,也就是说,fgetc在文件中找到字符之后的返回值是int,我们可以用int接收再打印出来。就比如我们刚刚输入进去的hello打印回来:

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}

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

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

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

	ch = fgetc(pf);
	printf("%c", ch);
    //运行结果:hello
	return 0;
}

而之前我们的表格中展示写的就是fgetc适用于使用的输入流,那么标准输入流(stdin)也就可以使用了,我们可以尝试一下:

int main()
{
	int ch = fgetc(stdin);
	printf("%c", ch);

	ch = fgetc(stdin);
	printf("%c", ch);

	ch = fgetc(stdin);
	printf("%c", ch);

	ch = fgetc(stdin);
	printf("%c", ch);

	return 0;
}

这里的代码运行起来的时候,首先我们会看见光标闪烁,输入流所以我们要输入,然后在fgetc(stdin)printf()读取下打印出刚刚输入的字符,这里只写了四个读取,所以输入再多,读到的也是前四个。


2.fputs 和 fgets

但是一个一个的读写是不是太慢了,所以我们后面的函数fgetsfputs就是文本行输入输出函数。其实使用的方法和前面的字符输入输出是一样的,函数参数可能略有不同。

fputs函数:

释义:将str指向的C字符串写入流。函数从指定的地址(Str)开始复制,直到到达终止空字符(‘\0’)为止。此终止空字符不会复制到流中.

从定义中我们知道,这里写入流的是字符串,所以可以节省我们对于字符写入时只能写一个的不足。

int fputs ( const char * str, FILE * stream );

在这里的参数可以看到,前面是写入的数据也就是常量字符串,后面是打开文件后出现的指针,所以也就是说前面写想输入文件的内容,后面就是写指向打开文件的FILE *的指针就可以了。

所以我们可以尝试写一个代码跑一下:

//fputs
int main()
{
	FILE* pf = fopen("data.txt", "w");
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}
	//写文件
	//写一行数据
	fputs("hello world\n", pf);
	//再写一行数据
	fputs("hello china\n", pf);

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

结果显示:

然后我们再来看一下fgets

释义:从流中获取字符串。从流中读取字符,并将它们作为C字符串存储到str中,直到(num-1)字符被读取,或者到达换行符或文件末尾,以先发生者为准。换行符使fgetc停止读取,但函数认为它是有效字符,并包含在复制到str的字符串中。

对于fgets来说,其实就是从流中获取字符串,相当于是读取其中的数据,比如文件中的数据。

参数:

char * fgets ( char * str, int num, FILE * stream );

而对于参数来说,这里的str是什么意思呢?其实在定义中也有,说的是指向复制字符串读取的字符数组的指针,也就是一个指向读取数据的指针。我们直接用代码来说明:

这里的代码是延续上面的fputs,也就是data.txt中已经有两行数据了。

//fgets
 int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}
	
	//读文件
	//读第一行数据
	char arr[20] = { 0 };
	fgets(arr, 20, pf);//将读取pf的前20个字符的指针内容存储到arr数组
	printf("%s\n", arr);//打印arr内容

    //读第二行
	fgets(arr, 20, pf);
	printf("%s\n", arr);
    //ps:读取的时候如果读的是5个,实际上读的字符是4个,
    //    因为第5个要求读一个'\0'。
 
	//关闭文件
	fclose(pf);
	pf = NULL;
}

所以我们看到的内容会是:


3.fprintf和fscanf

那么上面的fgets和fputs只是对于字符串的,如果是其他的类型或者说是不同类型的,就需要用到fprintffscanf函数了。

我们可以查看一下fscanf-cpulspuls中的解释和参数:

释义:从流中读取数据,并根据参数格式将它们存储到附加参数所指向的位置。附加参数应指向格式字符串中的相应格式说明符指定的类型已分配的对象。

对于fscanffprintf,也就是格式化格式化输入函数 ,就算按照某一种格式写入按照某一种格式读取。

int fscanf ( FILE * stream, const char * format, ... );

这里的参数如果看不懂,我们就可以进行对比去学习,我们可以打开scanf的说明去看一下对比一下:

实际上fscanf就多了一个FILE * 的参数,也就是打开的文件的地址参数,所有当我们进行使用的时候,我们可以先像scanf一样写出来,然后再在前面加上一个FILE * 的参数就好了。

比如:

int main()
{
    int c = 0;
    scanf("%d",&c);//scanf

    return 0;
}
int main()
{
    int c = 0;
    
    FILE* pf = fopen("data.txt", "r");//文件内容:10
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}
	
	fscanf(pf, "%d", &c);//fscanf
	//从文件中读取到10这个数据
    return 0;
}

fprintf函数就是:

把格式化的数据输出到所有输出流(屏幕/文件)上

同样的我们可以先查一下fprintf - cplusplus

释义:将按格式指向的C字符串写入流。如果格式包括格式说明符(从%开始的子序列),则格式化后的附加参数将被格式化并插入到结果字符串中,替换各自的说明符。

int fprintf ( FILE * stream, const char * format, ... );

同样经过对比之后,我们发现fprintf和printf也就是差了一个FILE *的参数,所以我们同样可以先按照printf的格式写,然后再加上参数,就可以很好的使用fprintf了。

下面是fprintffscanf的例子:

//fprintf(按某一种格式写入)
struct S
{
	int n;
	double d;
};

int main()
{
	struct S s = { 100, 3.14 };

	FILE* pf = fopen("data.txt", "w");
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}
	//写文件
	fprintf(pf, "%d %lf", s.n, s.d);

	//关闭文件
	fclose(pf);
	pf = NULL;
}
//fscanf
struct S
{
	int n;
	double d;
};
int main()
{
	struct S s = {0};
	FILE* pf = fopen("data.txt", "r");
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}
	//读文件
	fscanf(pf, "%d %lf", &(s.n), &(s.d));
   
	printf("%d %lf\n", s.n, s.d);

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

4.fwrite和fread

fwrite函数:

释义:将数据块写入流。从PTR指向的内存块写入计数元素数组,每个元素的大小为字节,到流中的当前位置。

实际上就是以二进制的形式写入文件中。

参数:

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

如果参数读不懂,下面还有关于参数的解释:

大概含义就是:第一个ptr指向的就是要被写的数据。第二个是size,也就是写的一个元素的大小,单位是字节。第三个count就是元素个数,也就是写多少个数据。第四个就是我们熟悉的FILE * 的文件指针。

所以我们直接写一个代码:

//fwrite
struct S
{
	int n;
	double d;
	char name[10];
};

int main()
{
	struct S s = {100, 3.14, "zhangsan"};

	FILE* pf = fopen("data.txt", "wb");
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}
	//写文件 - 二进制的方式写
	fwrite(&s, sizeof(s), 1, pf);
	//写的是结构体s的内容,大小是sizeof(s),一次写1个结构体,写入pf
	
	//关闭文件
	fclose(pf);
	pf = NULL;
}

运行完之后我们打开记事本看见的是:

注意:因为以二进制写进去,二进制的写法和文本写法不尽相同,所以一些数据在文件中我们是看不懂的,但再以二进制的形式打印出来,我们就可以看懂了。比如使用下面的fread函数:

释义:从流中读取数据块。从流中读取计数元素数组,每个元素的大小为字节,并将它们存储在PTR指定的内存块中。

也就是上面是怎么写入的,现在就怎么读取回来。

参数:

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

参数也和上面写入一样,所以我们直接上代码吧:

//fread
struct S
{
	int n;
	double d;
	char name[10];
};

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

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

	//打印
	printf("%d %lf %s\n", s.n, s.d, s.name);
	
	//关闭文件
	fclose(pf);
	pf = NULL;
}

而刚刚在文本文件中看不懂的数据,在这里就看到了:


三.随机读写

那么对于上面的函数,以及顺序读写,都是需要从头开始读,一直读下去的,如果我需要读取的是某一段信息的话,怎么办呢,之后就到我们随机读写的主场了。

我们可以想像一下,我们在打开文件的时候,会常见一个FILE * 的指针指向第一个元素,那么如果可以指向的是我们想读取的那一段的话,我们不就可以实现读取某一段信息了?所以有了下面的函数。

随机读写中包含了几个函数,我们一起看一下:

1.fseek函数

首先我们可以拿出fseek的定义看一下:

释义:重新定位流位置指示器。对于二进制模式下打开的流,新位置是通过向原点指定的参考位置添加偏移量来定义的。

也就是说,对于这个函数,可以通过偏移量,去选择它的读取位置,达到某一部分的读写。

参数:

int fseek ( FILE * stream, long int offset, int origin );

参数中,有我们熟悉的文件指针,也有两个新的参数。其中前面的offset是偏移量,也就是我们希望从这个位置偏移到哪一个位置开始读取,这就达到了我们可以对起始读取位置的选择。然后后面的一个是源地,也就是指针所指的位置,而这里函数还给了我们三个选择的位置:

第一个是SEEK_SET,也就是文件起始位置,这就是和顺序读写一样的起始位置。

第二个是SEEK_CUR,意思是文件指针的当前位置,当我们使用fseek函数读取到文件某一个地方的时候,文件指针再下一次读取不会回到起始位置,而是就在那,所以有了我们这一个选择从哪个位置偏移。

第三个是SEEK_END,意思就是文件末尾。

如果没有理解的话,我们可以在代码中理解一下:

int main()
{
	FILE* pf = fopen( "data.txt","r");

	if (pf == NULL)
	{
		perror(fopen);
		return - 1;
	}

	//文件data内容
	//abcdef

    //读取文件
    fseek(pf, 2, SEEK_SET);//从起始位置往后两个位置读,读到的也就是c
	int ret = fgetc(pf);
	printf("%c\n", ret);

	fseek(pf, -2, SEEK_CUR);
	//读完后会直接跳到下一个位置,也就是d,然后往前两个位,读到的就是b
	ret = fgetc(pf);
	printf("%c\n", ret);
	
    //运行结果: c b
	return 0;
}

注意:在fgetc函数中,返回指定流的内部文件位置指示符当前指向的字符。然后将内部文件位置指示符提前到下一个字符。

所以上面的偏移量为-2的时候读到的是b而不是a。


2.ftell函数

对于上面的fseek函数,我们可以用来读取偏移后某一处的位置,那么有时候我们不知道读到哪了怎么办,这时候我们可以使用ftell函数:

释义:返回流的位置指示符的当前值。对于二进制流,这是文件开头的字节数。

其实这里的意思就是得到当前位置指示符的位置,然后返回一个数值,就是起始位置到当前位置的距离。也就是距离起始位置的偏移量是多少。

返回文件指针相对于起始位置的偏移量

参数:

long int ftell ( FILE * stream );

这里的参数只需要放进文件的FILE * 的指针就可以啦,返回值是long int 。

我们接着上面的代码写一下:

int main()
{
	FILE* pf = fopen("data.txt", "r");

	if (pf == NULL)
	{
		perror(fopen);
		return -1;
	}

	//文件data内容
	//abcdef

	//读取文件
	fseek(pf, 2, SEEK_SET);
	int ret = fgetc(pf);
	printf("%c\n", ret);

	fseek(pf, -2, SEEK_CUR);
	ret = fgetc(pf);
	printf("%c\n", ret); //c b

	int b = ftell(pf);
	printf("%d\n", b);//2

	return 0;
}

这里打印的是2,是我们说错了吗,其实不是,当我们上一次读取的是b之后,位置指示符已经跳到了下一个也就是c处了,所以相对于起始位置a,偏移量是2。


3.rewind函数

当我们用fseek函数读取的时候,有可能走到很远的地方,然后我们需要他回来的时候,就可以使用rewind函数:

释义:将流的位置设置为开头。将与流关联的位置指示符设置为文件的开头。

其实就是回到起始位置的意思:

让文件指针的位置回到文件的起始位置。

参数:

void rewind ( FILE * stream );

参数同样是传文件的FILE * 的指针过去,并且没有返回值,也就是说直接运行了,就等于首先了文件指针的位置回到文件的起始位置的功能了。

还是紧接上面的代码:

int main()
{
	FILE* pf = fopen("data.txt", "r");

	if (pf == NULL)
	{
		perror(fopen);
		return -1;
	}

	//文件data内容
	//abcdef

	//读取文件
	fseek(pf, 2, SEEK_SET);
	int ret = fgetc(pf);
	printf("%c\n", ret);//c

	fseek(pf, -2, SEEK_CUR);
	ret = fgetc(pf);
	printf("%c\n", ret);//b

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

	return 0;
}

所以这里就很清楚的知道说是回到了起始位置,所以打印的结果是a。

四.读取函数结束标志

如果我们一直用fgetc函数读,什么时候会停止呢,这时候我们看定义就会发现,当读完了读到的是空的时候,返回的值就是EOF了。比如下面的代码:

int main()
{
	//1. 打开文件
	FILE* pf = fopen("data.txt", "r");
	if (NULL == pf)
	{
		perror("fopen");
		return -1;
	}

    //假设data.txt里面是abcde
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c ", ch);
	}
	//打印结果:abcde
	return 0;
}

对于上面的其他函数也可以同理得到哦,可以接收函数调用的返回值,然后去判断他是否读完整个文件。比如fgets函数的读取结束再读取返回的是字符指针。还有fscanf,遇到错误或者文件末尾,也有返回EOF的情况。对于fread也有返回值是一个size_t,如果你想读5个但是返回的是比你小的数字,说明已经读完了。


五.文件结束的判定

但是!在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。还有ferror函数也是!

int feof ( FILE * stream );

对于feof函数,是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

int ferror ( FILE * stream );

对于ferror函数,是应用于文件读取结束,判断是不是遇到错误后而读取结束的。

就好像上面说的读取结束的标志,我们总结出来是这样子的:

  1. 文本文件读取是否结束,判断返回值是否为EOF (fgetc),或者NULL(fgets)。

例如:
fgetc判断是否为EOF.
fgets判断返回值是否为NULL.

  1. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。

例如:
fread判断返回值是否小于实际要读的个数。

总结来说就是:

feof的用途:是文件读取结束了,判断是不是遇到文件末尾而结束的
ferror的用途:文件读取结束,判断是不是遇到错误后而读取结束的


本篇文章就到这里,如果感觉对你有帮助,不妨点个赞。

  • 4
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

恒等于C

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

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

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

打赏作者

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

抵扣说明:

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

余额充值