C语言笔记C语言文件操作全解

Posted 林先生-1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言笔记C语言文件操作全解相关的知识,希望对你有一定的参考价值。

【C语言笔记】C语言文件操作全解

一、文件的打开和关闭

我们在编写或运行C语言程序时总会产生或多或少的数据,如果我们想让这些数据能够长久的使用,就可以将它们保存到文件中。
在程序设计中,我们所谈到的文件一般分为两种:程序文件和数据文件,而我们今天所要讲的文件操作主要针对的是数据文件。

1、文件指针

在讲文件操作之前,必须先清楚的一个概念就是“文件指针”。
顾名思义,“文件指针”就是一个“指向文件的指针”
关于文件指针的来历,其实是这样的:

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.

而我们所说的“文件指针”其实指向的也并非文件本身,而是文件所的对应的文件信息区的结构体变量。
所以我们在操作文件的时候其实是通过文件指针找到了一个文件信息区变量,在通过这个变量取得文件的地址,在访问文件,实际属于间接间接访问了:

2、“流”的概念

在程序设计中,“流”是一个高度抽象的概念,它的出现主要是为了解决效率问题。设想一下我们程序员在编写程序时候,可能用把程序所产生的数据文件存储到不同格式的文件之中,这些格式不同的文件的读写方式当然也是不同的,那么这岂不是要我们程序员要把这些读写方式全都掌握呢?这也太麻烦了吧。
所以,为了调效率,我们才抽象出了“流”的概念,“流”其实可以粗略的理解为是一个统一的通道,它把各种输入的数据统一汇集到它的通道中,然后再经过要求将数据以各种格式输入到各种格式的文件中:

所以我们程序员在编写程序的时候,只需要关心把信息写入到流中即可,置于流是怎么把数据写入各种文件中的,我们不需要关心。
所以我们之前的文件指针就可以称为是一个“流”,因为我们程序员的输入格式可能和真正要写入文件的格式是不同的。
所以在大多数的文档中对于文件指针更多的是称其为“流”,而不是直接称为文件指针,这个也是要事先说清楚的。

3、fopen和fclose函数

ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
其中打开的形式如下:

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

其中filename指的是你要打开的文件的文件名,mode是你将要以什么形式打开,这些形式有很多种,这里就不一一列举了,后面遇到再细讲。
其实就像动态内存申请一样,我们文件的打开也也有失败的时候,如果打开失败,那么fopen函数就将返回一个空指针NULL。
所以对文件打开的结果进行判断也是很重要的:

int main() 
	FILE* pf = fopen("test.txt", "w"); // "w"表示以读的形式打开
	if (NULL == pf) 
		perror("fopen");
		return -1;
	
	return 0;

关闭文件的形式如下:

int fclose ( FILE * stream );

我们只需要传入一个文件指针(流)即可。
因为文件也是一种资源,而资源总归是有限的,不可能让你只是用而不回收,所以在使用完文件后就关闭文件也是很重要的:

int main() 
	FILE* pf = fopen("test.txt", "w"); 
	if (NULL == pf) 
		perror("fopen");
		return -1;
	
	// 写文件
	// 关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

二、文件的顺序读写

打开文件后,我们就可以对文件进行读写操作了。

1、fputc和fgetc函数

fputc函数的功能是每次向文件中写入一个字符,使用这个函数我们需要传入两个参数,第一个是要写入的字符,第二个就是文件流:

比如我们想依次向文件中写入“hello”这个单词,就可以用fputc函数分5次写入:

int main() 
	FILE* pf = fopen("test.txt", "w");
	if (NULL == pf) 
		perror("fopen");
		return -1;
	
	// 写文件
	fputc('h', pf);
	fputc('e', pf);
	fputc('l', pf);
	fputc('l', pf);
	fputc('o', pf);
	// 关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

而当我们以“w”的形式打开文件时,如果文件不存在,那么程序就会自动自当前路径地下创建一个文件:

所以当我们的程序运行时,就可以发现当前路径下就自动创建了一个名为test.txt的文件:

我们打开它,就可以看到里面已经是有内容的了:

而fgetc函数正好是与fputc相反的,即为每次从文件中读出一个字符,使用这个函数我们就只需要传入一个参数文件流即可:

例如我们想把刚刚存进去的“hello”逐个字符的读出来,就可以这样做:

int main() 
	FILE* pf = fopen("test.txt", "r");
	if (NULL == pf) 
		perror("fopen");
		return -1;
	
	// 读文件
	char ch = 0;
	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);
	// 关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

而这次我们是以“r”——只读的形式打开的文件,该方式如果文件不存在的话就会直接报错:

而我们的test.txt是已经创建好了的,所以我们运行程序后,屏幕上就会出现这样的结果:

2、fputs和fgets函数

fputs函数的功能是每次向文件中写入一个字符串,使用这个函数我们需要传递两个参数,第一个是一个字符串(其实是字符串起始字符的地址),第二个就是文件流:

例如我们想象杠杠的test.txt文件中写入“hello world!”这就话,就可以这样做:

int main() 
	FILE* pf = fopen("test.txt", "w");
	if (NULL == pf) 
		perror("fopen");
		return -1;
	
	// 写文件
	fputs("hello world!", pf);
	// 关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

这里就要先说一下以“w”只读方式打开文件的一个特性了。
以此方式打开文件时,每次打开不管文件中有多少内容,都会先将原有的内容全清空,然后再重新向里面写入数据。
所以当程序执行完毕之后再打开test.txt就会发现内容变成了“hello world”,之前的“hello”就不见了:

而fgets也就是每次从文件中读出一个字符串,使用这个函数我们需要传入三个参数,第一个是读到的字符串将要被拷贝到的空间的起始地址,第二个是每次将要读取的字符的个数,第三个才是文件流:

该函数的返回值是一个char类型的指针,也就是第一个字符的地址,所以我们在调用此函数的时候需要用一个char*类型的变量来接收其返回值。
而这个函数还有一点需要注意,就是这个函数在每次读取的时候都会默认再添加一个字符串结束标志‘\\0’,而这个‘\\0’也会占用一个个数,也就是说我实际要传参传入的个数要比我们实际想要读到的字符的个数多一个。
例如我们想要将已存进test.txt文件中的“hello world!”全部读则需要传递个的个数是13而不是12:

如果传入的是12,那就会少了一个字符:

3、fwrite和fread函数

以上的函数都是以文本的形式读写文件,也可以说是以字符的形式读写文件。但读写文件的方式可以多种多样的,fwrite和fread函数就是以二进制的形式进行读写文件,也就是将内存中的数据(内存中的数据全都是以二进制的形式存储的)直接写入文件中,不做任何转换。
以二进制的形式向文件中写入信息的函数时fwrite,使用该函数时我们需要传入4个参数:

这参数好像有点儿多了,让我们一个一个的来看吧:
1、ptr: 指向源数据首地址的指针(要写的数据来自哪里)。
2、size: 要写的每一个元素的大小。
3、count: 总共要写多少个元素。
4、stream: 将要被写入数据的流。

这样好像还是难以理解,我们举个例子吧:

int main() 
	FILE* pf = fopen("test.txt", "wb"); // wb表示以二进制写入的方式打开
	if (NULL == pf) 
		perror("fopen");
		return -1;
	
	// 写文件
	char str[20] = "no overtmie";
	fwrite(str, sizeof(char), strlen(str), pf);
	// 要写的数据从str这个数组中来
	// 每个元素的大小是1个字节
	// 一共写strlen(str)个元素
	// 写入到pf所指向的文件中
	
	// 关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

写完后我们可以看看test.txt的内容:

有些人可能就会认为,这好像和直接使用文本写入也没什么区别啊。
其实这是因为这里所写入的是字符啊,我们知道字符类型的本质是ASCLL码值啊,ASCLL码在内存中就是以二进制的形式存储的。所以这里现实的彩绘和文本写入的相同。
当要是我们写入的是一个结构体的数据,那结果就不同了:

struct boy 
	int id;
	int age;
	char name[10];
;

int main() 
	FILE* pf = fopen("test.txt", "wb"); // wb表示以二进制写入的方式打开
	if (NULL == pf) 
		perror("fopen");
		return -1;
	
	// 写文件
	struct boy b =  1, 20, "zhangsan" ;
	fwrite(&b, sizeof(b), 1, pf);
	
	// 关闭文件
	fclose(pf);
	pf = NULL;
	return 0;

程序运行结束后我们再来看看test.txt中的内容就看不懂了:

但我们看不懂没关系,计算机能看懂就行了。比如fread函数就能看懂。
fread函数的功能就是以二进制的方式从文件中读出数据,使用该函数时,我们也还是需要传入4个参数:

1、ptr: 指向读出的数据将要被放到的内存的起始地址(读出的数据要放到哪里)。
2、size: 要读出的每一个元素的大小。
3、count: 总共要读出多少个元素。
4、stream: 将要被读出数据的流。
比如我们可以将之前存入文件中的“no overtime”这个字符串在从文件中读出来:

三、文件的随机读写

之前介绍的函数都只能按顺序的读写文件,但C语言给我们提供了一些函数,这些函数可以使我们的文件指针来回地移动,从而实现文件的随机读写。

1、fseek函数

fseek函数的功能是根据文件指针的位置和偏移量来定位文件指针。也就是从当前文件指针的位置开始,将文件指针移动到对应偏移量的位置。
这个函数一共有三个参数:

第一个参数stream就是文件流,第二个参数offset就是偏移量,第三个参数origin就是起始位置。
而且该函数规定了origin的取值只能由三种情况:

我们可以使用fseek函数对指针就行定位后在进行读取,比如我们现在的test.txt中有以下内容:

若是我想一开始就从第4个字符开始读取,就可以这样做:

2、ftell函数

ftell函数的功能是返回当前文件指针相对于起始位置的偏移量。例如上一个例子执行完毕后,pf相对于起始位置的偏移量应该是4,我们可以将它打印出来看看:

3、rewind函数

rewind函数的功能是将文件指针重新指向文件的起始位置,当我们操作的过多或者过复杂的时候,就可以使用这个函数来完成一个“归零”,例如我们紧接着上例,就可以将文件指针先“归零”后在读取一次,看看它打印的是不是第一个字符:

四、文件读取结束的判定

文件的大小一定是有限的,那么就一定会有文件读取结束的情况。

1、判断EOF或者NULL

判断文件读取是否结束常用的方法就是判断返回值是否是EOF或者NULL。
判断EOF是相对于fgetc函数而言的,我们可以去相关文档去看看关于fgetc函数返回值的介绍:

上面清晰地说明着,如果文件指针已经到了文件的末尾,就会返回EOF。
而判断是否为NULL是相对于fgets函数而言的,我们也可以去相关的文档中翻一翻关于这个函数返回值的介绍:

上面也说了如果读取失败或出现错误,则返回空指针。

C语言学习笔记(18)文件操作2

五.文件的随机读写

fseek


根据文件指针的位置和偏移量来定位文件指针。

#include<stdio.h>
int main()
{
	FILE* pf1 = fopen("FirstFile.txt", "w");
	if (pf1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件 
	fputs("abcdef", pf1);
	//关闭文件 
	fclose(pf1);
	pf1 = NULL;
	
	FILE* pf2 = fopen("FirstFile.txt", "r");
	if (pf2 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件 - abcdef
	int ret = fgetc(pf2);
	printf("%c\\n", ret);
	//调整文件指针
	//fseek(pf2, -1, SEEK_CUR);//a a b -  从文件指针当前位置偏移-1 
	//fseek(pf2, 2, SEEK_SET);//a c d - 从文件开头的位置a偏移2
	fseek(pf2, -3, SEEK_END);//a d e - 从文件结尾的位置向前偏移3
	ret = fgetc(pf2);
	printf("%c\\n", ret);
	ret = fgetc(pf2);
	printf("%c\\n", ret);

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

ftell

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

int main()
{
	FILE* pf1 = fopen("FirstFile.txt", "w");
	if (pf1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件 
	fputs("abcdef", pf1);
	//关闭文件 
	fclose(pf1);
	pf1 = NULL;
	
	FILE* pf2 = fopen("FirstFile.txt", "r");
	if (pf2 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件 - abcdef
	int ret = fgetc(pf2);
	printf("%c\\n", ret);//a
	ret = fgetc(pf2);
	printf("%c\\n", ret);//b
	ret = fgetc(pf2);
	printf("%c\\n", ret); //c
	//计算当前文件指针位置相对于起始位置的偏移量
	int set = ftell(pf2);
	printf("%d\\n", set);//3 - c对于起始位置偏移量为3

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

rewind

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

#include<stdio.h>
int main()
{
	FILE* pf1 = fopen("FirstFile.txt", "w");
	if (pf1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件 
	fputs("abcdef", pf1);
	//关闭文件 
	fclose(pf1);
	pf1 = NULL;
	
	FILE* pf2 = fopen("FirstFile.txt", "r");
	if (pf2 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件 - abcdef
	int ret = fgetc(pf2);
	printf("%c\\n", ret);//a
	ret = fgetc(pf2);
	printf("%c\\n", ret);//b
	ret = fgetc(pf2);
	printf("%c\\n", ret); //c
	int set = ftell(pf2);
	printf("%d\\n", set);//3
	//让文件指针回到起始位置
	rewind(pf2);
	ret = fgetc(pf2);
	printf("%c\\n", ret);//a
	set = ftell(pf2);
	printf("%d\\n", set);//1

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

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

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。

一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储, 数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节), 而二进制形式输出,则在磁盘上只占4个字节(VS2013测试) 。


以ASCII码的形式输出

以二进制形式输出


按上面方式打开可得到

用计算器验证一下,结果无误

注意:该程序是小端存储

七.文件读取结束的判定

错误的使用feof

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

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

  • fgetc判断是否为EOF
  • fgets判断返回值是否为NULL

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

  • fread判断返回值是否小于实际要读的个数

拷贝一份文件

将test1.txt文件拷贝一份生成test2.txt

int main()
{
	FILE* pfread = fopen("test1.txt", "r");
	if (pfread == NULL)
	{
		perror("fopen");
		return 1;
	}
	FILE* pfwrite = fopen("test2.txt", "w");
	if (pfwrite == NULL)
	{
		perror("fopen");
		fclose(pfread);
		pfread = NULL;
		return 1;
	}
	//读写文件
	int ch = 0;
	while ((ch = fgetc(pfread)) != EOF)//读
	{
		fputc(ch, pfwrite);//写
	}
	if (feof(pfread))//如果遇到文件结束,它会返回一个非0的值
	{
		printf("遇到文件结束标志,文件正常结束\\n");
	}
	else if (ferror(pfread))//ferror返回非0,表示文件读取失败
	{
		printf("文件读取失败\\n");
	}
	//关闭文件 
	fclose(pfread);
	pfread = NULL;
	fclose(pfwrite);
	pfwrite = NULL;
	return 0;
}


八.文件缓冲区

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

以上是关于C语言笔记C语言文件操作全解的主要内容,如果未能解决你的问题,请参考以下文章

C语言学习笔记(17)文件操作1

C语言文档阅读笔记-Basics of File Handling in C

C语言文档阅读笔记-Basics of File Handling in C

C语言文档阅读笔记-Basics of File Handling in C

C语言初阶笔记深入探索C语言操作符的奥秘(下)!!

C语言初阶笔记深入探索C语言操作符的奥秘(下)!!