怒肝1.5万字——史上最全C语言文件操作详解

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


在这里插入图片描述

一、为什么使用文件

我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来时,可以给通讯录增加、删除数据,此时的数据是存放在内存中的,当程序退出时,通讯录的数据就被销毁了,下次运行通讯录时,数据又得重新录入,如果使用这样的通讯录就会非常的坐牢。

所以这些通讯录数据我们仅仅放在内存里是不行的,大家都知道,我们电脑的C盘、D盘、E盘等等里面存放的文件,你不进行删除,它就一直在那里,那我们可以试着把通讯录的数据存入磁盘里,来实现数据的持久性。

二、什么是文件?

磁盘上的文件就是文件。
但是在程序设计中,我们一般谈的文件有两种:
1.程序文件
2.数据文件
(以文件功能来划分)

2.1程序文件

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

2.2数据文件

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

下图是数据文件与程序文件的交互简图
在这里插入图片描述

2.3文件名

一个文件要有一个唯一的文件标识,便于用户的识别与引用。
文件名包含3个部分:文件路径+文件名主干+文件后缀
栗子:E:\c-language-notes\test.txt
这里的E:\c-language-notes\叫作文件路径,是在E盘c-language-notes这个路径底下
test叫作文件主干
.txt叫做文件后缀

为了方便起见,文件标识常被称为文件名

三、文件的打开与关闭

3.1文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称文件指针

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

如上图,当你想要操作一个数据文件时,你不可避免地会经历3个操作:打开文件、读/写文件、关闭文件。只要你打开文件,系统会自动生成一个叫做文件信息区的东西,也就是会创建一个 FILE 类型结构体变量,上图以f作为示例,实际也可能是其他的,那么创建完成后,f就会和data.txt文件强关联了,f会记录data文件名、文件有多大、文件在哪个位置、文件的状态是怎样的。。。

vs2013的编译器环境提供的stdio.h的头文件中有以下的文件类型声明:

struct _iobuf
{
    
    
	char*_ptr;
	int _cnt;
	char*_base;
	int _flag;
	int _file;
	int _charbuf;
	int _bufsiz;
	char*_tmpfname;
};
typedef struct _ibuf FILE//把上述结构体重命名为FILE

不同编译器的FILE类型包含的内容不一定完全相同,但基本都是大同小异,每当打开一个文件时,系统会根据文件的情况自动创建一个FILE结果的变量,并填充其中的信息,我们使用者不必过度关心细节,按周总理说的“求同存异”即可。

一般都是通过一个FILE类型的指针来维护FILE结构的变量,这样使用起来更加方便。

FILE*pf;//文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就可以访问该文件。也就是说,通过文件指针变量能找到与它关联的文件如下图所示:在这里插入图片描述
pf是指向文件信息区的,而文件信息区又可以确切的找到与它关联的文件,这样你就可以通过pf找到所需文件并进行相关操作。

3.2文件的打开与关闭

文件在读写之前应该先打开文件,在使用后应该关闭文件
在编写程序时,打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相对于建立了指针和文件的联系。

ANSIC规定使用fopen函数来打开文件,fclose来关闭文件

FILE *fopen(const char* filename, const char* moede);//打开文件
//fopen第一个参数是文件名,第二个参数是打开模式
//比如你传一个test.txt到一个参数里,传的其实是首字母t的地址
//打开模式是这样的,你是想给这个文件写点东西还是想读取这个文件的一些东西
int fclose(FILE*stream);//关闭文件

关于打开模式如下图,比如你打开模式是r,那你的打开方式就是读,如果你的文件不存在或者没有被找到,那fopen函数就会调用失败
在这里插入图片描述

(图片来自比特就业课,这里只举r一个打开方式,其他打开方式读者可自行对照上表)

fopen打开模式打开data.txt文件会返回一个FILE*的指针,该指针是指向与data.txt文件相关联的文件信息区的起始地址,如果打开失败会返回空指针。
在这里插入图片描述
fclose关闭文件相对fopen就简单很多了,你要关闭哪个文件,我们直接传那个文件关联的文件信息区的指针即可,也就是上图的pf

打开和关闭实际操作代码示例如下:
比如我现在要打开E:\c-language-notes\test.21.10.9路径下的data.txt文件

int main()
{
    
       //打开文件
    //fopen函数
	FILE*pf=fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "r");
	//这里的"\"可能会与后面的字母构成转义字符,我们用\对\进行转义一下,让\单纯是一个\
	//fopen函数会返回一个FILE*的指针,打开失败返回空指针
	if (pf == NULL)
	{
    
    
		perror("fopen");//perror函数显示错误信息
		return -1;
	}
	//读文件。。。
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

需要注意的是,fclose关闭文件是不会把pf置为空指针的,我们需要手动操作置为空指针

四、文件的顺序读写

在这里插入图片描述
(图片来自比特就业课)
所有输入流包括:istream 类连续文本模式输入使用、ifstream磁盘文件输入、istringstream 类从内存字符串的输入

4.1字符输入输出函数

fgetc和fputc函数分别是读入一个字符和输出一个字符
在这里插入图片描述
如上图,我们写一个程序时,会产生一些数据,数据会存放在内存中,如果你想把数据写入(输出)到文件里,或者你想把文件里的信息读到内存中,叫读(输入操作)。我们用输入/输出操作就用的是fgetc与fputc

fputc函数示例:

// int fputc(int c, FILE *stream);函数声明
int main()
{
    
       
	FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "w");//以“只写w”的模式打开文件
	if (pf == NULL)
	{
    
    
		perror("fopen");//perror函数显示错误信息
		return -1;
	}
	//写文件
	fputc('b', pf);
	fputc('i', pf);
	fputc('t', pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

运行完上述代码后,相关文件自动出现fputc函数写入的三个字
在这里插入图片描述
fgetc函数示例:

//int fgetc(FILE *filename);函数声明
int main()
{
    
    
	FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "r");//以“只读r”的模式打开文件
	if (pf == NULL)
	{
    
    
		perror("fopen");//perror函数显示错误信息
		return -1;
	}
	//读文件
	int ch1=fgetc(pf);
	printf("%c\n", ch1);

	int ch2 = fgetc(pf);
	printf("%c\n", ch2);
	
	int ch3 = fgetc(pf);
	printf("%c\n", ch3);
	fclose(pf);
	pf = NULL;
	return 0;
}

假设我们现在相关文件里有3个字母abc
在这里插入图片描述
我们运行上述程序,程序自动从文件里获取三个字母abc
在这里插入图片描述

4.2文本行输入输出函数

我们再来看一看文本行输入/输出函数
fputs函数示例:

//int fputs(const char *s, FILE *stream);函数声明
//s 代表要输出的字符串的首地址,可以是字符数组名或字符指针变量名。
//stream 表示向何种流中输出,可以是标准输出流 stdout,也可以是文件流。标准输出流即屏幕输出,printf 其实也是向标准输出流中输出的。
int main()
{
    
    
	FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "w");//以“只写W”的模式打开文件
	if (pf == NULL)
	{
    
    
		perror("fopen");//perror函数显示错误信息
		return -1;
	}
	//写文件(写一行)
	fputs("hello world\n", pf);
	fputs("hello bit\n", pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

和fputc差不多,fputc是写一个字母,fputs是写一行,运行完上述程序,相关文件夹出现hello world 和hello bit
在这里插入图片描述
fgets函数示例:

//char *fgets(char *buf, int bufsize, FILE *stream);
//*buf: 字符型指针,指向用来存储所得数据的地址。
//bufsize: 整型数据,指明存储数据的大小。
//*stream: 文件结构体指针,将要读取的文件流。
int main()
{
    
    
	FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "r");//以“只读r”的模式打开文件
	if (pf == NULL)
	{
    
    
		perror("fopen");//perror函数显示错误信息
		return -1;
	}
	//读文件(读一行)
	char arr[20] = {
    
     0 };
	fgets(arr,5,pf);
	printf("%s\n", arr);//打印hell,说是最多读5个其实是读到第四个,然后第五个补\0
	fclose(pf);
	pf = NULL;
	return 0;
}

我们原先文件里有hello world 和hello bit,我们读一行中的5个字符到arr里
在这里插入图片描述
在这里插入图片描述
说是打印5个字符,其实是打印4个字符,第五个字符是自动补\0

4.3格式化输入输出函数

格式化输入输出也就是按某种格式写入或读取
fprintf函数示例:

struct S
{
    
    
	int n;
	double d;
};
int main()
{
    
        //int fprintf(FILE *filename, const char *string, . . . .);函数声明
     //看起来函数声明有点麻烦,我们再来看一下常见的printf函数声明
     //int printf( const char *format, … );
     //对比一下很容易发现也就是比printf函数多一个指针参数而已,其他的都按printf来即可
	    struct S s = {
    
     100,3.14 };
		FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "w");//以“只写w”的模式打开文件
		if (pf == NULL)
		{
    
    
			perror("fopen");//perror函数显示错误信息
			return -1;
		}
		//写文件
		fprintf(pf,"%d %lf", s.n, s.d);
		fclose(pf);
		pf = NULL;
		return 0;
}

fprintf就是printf函数多一个pf指针,其他的和printf都是一样的,上述代码运行一下,相关文件出现100,和3.140000(浮点型默认6位小数)
在这里插入图片描述
fscanf函数示例:

struct S
{
    
    
	int n;
	double d;
};
int main()
{
    
    
    //int fscanf(FILE *stream, char *format,[argument...]);函数声明
    //和fprintf一样,就是scanf函数前面多一个pf指针参数
	struct S s = {
    
     0};
	FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "r");//以“只读r”的模式打开文件
	if (pf == NULL)
	{
    
    
		perror("fopen");//perror函数显示错误信息
		return -1;
	}
	//读文件
	fscanf(pf,"%d %lf", &(s.n), &(s.d));
	printf("%d %lf\n", s.n, s.d);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
原先文件里有100 3.140000,运行程序后读取出这两个数
在这里插入图片描述

4.4二进制输入输出函数

fwrite函数声明如下
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
ptr-- 这是指向要被写入的元素数组的指针。
size-- 这是要被写入的每个元素的大小,以字节为单位。
nmemb-- 这是元素的个数,每个元素的大小为 size 字节。
stream-- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
fwrite函数示例:

//size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
//ptr-- 这是指向要被写入的元素数组的指针。
//size-- 这是要被写入的每个元素的大小,以字节为单位。
//nmemb-- 这是元素的个数,每个元素的大小为 size 字节。
//stream-- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
struct S
{
    
    
	int n;
	double d;
	char name[20];
};
int main()
{
    
    
	    struct S s = {
    
     100,3.14,"zhangsan" };
		FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "wb");//以“只写wb”的模式打开文件
		if (pf == NULL)
		{
    
    
			perror("fopen");//perror函数显示错误信息
			return -1;
		}
		//写文件-二进制方式写
		fwrite(&s, sizeof(s), 1, pf);
		//关闭文件
		fclose(pf);
		pf = NULL;
		return 0;
}

这里要注意的是,我们以二进制fwrite写入,产生的是二进制文件,所以我们以二进制的wb模式打开,文件里的内容如下
在这里插入图片描述
因为是二进制文件,我们直接看是看不懂的,但是我们知道可以二进制读,也就是下面的fread函数
函数声明:size_t fread( void *buffer, size_t size, size_t count, FILE *stream )
buffer 是读取的数据存放的内存的指针(可以是数组,也可以是新开辟的空间,buffer就是一个索引)
size 是每次读取的字节数
count 是读取次数
strean 是要读取的文件的指针
fread函数示例:

struct S
{
    
    
	int n;
	double d;
	char name[20];
};
int main()
{
    
    
	struct S s = {
    
    0};
	FILE*pf = fopen("E:\\c-language-notes\\test.21.10.9\\data.txt", "rb");//以“只写wb”的模式打开文件
	//wb
	if (pf == NULL)
	{
    
    
		perror("fopen");//perror函数显示错误信息
		return -1;
	}
	//读文件-二进制方式读
	fread(&s, sizeof(struct S), 1, pf);
	//打印
	printf("%d %lf %s\n", s.n, s.d, s.name);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
我们现在文件里是这些东西,我们运行程序读一下
在这里插入图片描述
可以读出之前写入的东西

五、文件的随机读写

5.1fseek

函数声明:int fseek(FILE *stream, long offset, int fromwhere);
函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere为基准,偏移offset个字节的位置。如果执行失败(比如offset超过文件自身大小),则不改变stream指向的位置。执行成功返回0,否则返回其他数。

fromwhere有三个值:
SEEK_CUR 文件指针当前位置
SEEK_END 文件的末尾
SEEK_SET 文件的起始
在这里插入图片描述
比如a的地址就是文件的起始,f就是文件的末尾,假如我现在fgetc读了一个,指针从a往后移动一位,指针指向b,那b所在位置就是SEEK_CUR,也就是文件指针当前位置

注意:随机读写不是乱读,而是想读哪里读哪里,比如我想读文件第三个字母,我就可以直接用随机读写读取第三个字母。我们仍以上面这个文本文档为例:
现在我要读第三个,按照常规的顺序读写,是有一个指针指向a,然后每读一个,指针往后移一位,读第三个要移动2次。fseek函数就是可以快速根据指针的位置和偏移量来定位文件指针,大白话讲就是fseek函数可以快速找到第三个字母的指针。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//fseek函数
int main()
{
    
    
	//1.打开文件
	FILE*pf=fopen("E:\\c-language-notes\\test.21.10.11\\data.txt", "r");
	if (pf == NULL)
	{
    
    
		perror("fopen");
		return -1;
	}
	//2.读文件(随机读写)
	//读c
	fseek(pf, 2, SEEK_SET);//现在我要读c,刚开始cur和set都是起始位置,用cur也可
	int ch = fgetc(pf);
	printf("%c\n", ch);
	//读b
	fseek(pf, -2, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c\n", ch);
	//3.关闭文件
	return 0;
}

关于读b,因为我们读c之后,指针会自动往后移一位,所以cur是指向d的,b关于d的偏移量是-2,所以我们用fseek(pf, -2, SEEK_CUR);读取

5.2ftell

函数声明:long int ftell(FILE*filenname);

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

#include<stdio.h>
//fseek函数
int main()
{
    
    
	//1.打开文件
	FILE*pf=fopen("E:\\c-language-notes\\test.21.10.11\\data.txt", "r");
	if (pf == NULL)
	{
    
    
		perror("fopen");
		return -1;
	}
	//2.读文件(随机读写)
	//读c
	fseek(pf, 2, SEEK_SET);//现在我要读c,刚开始cur和set都是起始位置,用cur也可
	int ch = fgetc(pf);
	printf("%c\n", ch);
	//读b
	fseek(pf, -2, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c\n", ch);
	int a=ftell(pf);//b读完之后指针自动往后一位到c,c相对a偏移量为2
	printf("%d", a);//打印2
	//3.关闭文件
	return 0;
}

继上一段代码,我们读完b之后指针自动往后移动一位指向c,c相对起始位置a的偏移量为2,所以ftell会返回2

5.3rewind

函数声明:void rewind(FILE *stream);
不管pf现在在什么位置,传过去,pf重新指向文件起始位置

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

根据数据的组织形式,数据文件被称为文本文件或者二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件

如果要求在外存上以ASCII码的形式存储,则需要在存储前进行转换。以ASCII码的形式存储的文件就是文本文件

一个数据在内存中是怎么存储的呢?
字符一律以ASCII码的形式进行存储,数值型的数据即可用ASCII码的形式存储,也可以用二进制形式存储,如下,我们进行10000的存储
在这里插入图片描述
(图片来自比特就业课)
如果我们按ASCII形式存储,把10000共5位,我们把每位上的数字看做一个字符,共要5个字节

如果我们直接按二进制形式进行存储,二进制的10000,是
00000000 00000000 00100111 00010000共需占4个字节(1个整形)

七、文件读取结束的判断

7.1被错误使用的feof

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

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

*fgetc判断是否为EOF
fgetc读到一个字符返回int,如果文件结束没读到或者遇到错误,返回EOF

原文件里有abcdef5个字符,现在我们怎么利用fgetc进行打印,并判断是否结束呢?
在这里插入图片描述
代码如下:

#include<stdio.h>
int main()
{
    
    
	    //打开文件
		FILE*pf=fopen("E:\\c-language-notes\\test.21.10.11\\data.txt", "r");
		if (pf == NULL)
		{
    
    
			perror("fopen");
			return -1;
		}
		//打印文件内容
		int ch = 0;
		while ((ch = fgetc(pf)) != EOF)
		{
    
    
			printf("%c ", ch);
		}

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

在这里插入图片描述

*fgets判断返回值是否为NULL
函数声明: char *fgets(char *string, int n, FILE *stream);
fgets会从流stream里读n个字符到string这个字符串首元素地址里,如果遇到错误,或者遇到文件结尾,函数会返回一个空指针

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

回归开头,feof函数是用来判断文件结束时,是由于文件读完了结束,还是文件读取错误而结束

feof函数使用实例:

#include<stdio.h>
int main()
{
    
    
	    //打开文件
		FILE*pf=fopen("E:\\c-language-notes\\test.21.10.11\\data.txt", "r");
		if (pf == NULL)
		{
    
    
			perror("fopen");
			return -1;
		}
		//打印文件内容
		int ch = 0;
		while ((ch = fgetc(pf)) != EOF)
		{
    
    
			printf("%c ", ch);
		}
		//判断是什么原因结束的
		if (ferror(pf))//文件读取遇到错误结束
		{
    
    
			puts("I/0 error when reading");
		}
		else if (feof(pf))//feof函数如果返回非0,说明是遇到文件末尾结束,如果是0,则是其他情况
		{
    
    
			puts("End of file reached sucessfully");
		}
		//关闭文件
		fclose(pf);
		pf = NULL;
		return 0;
}

总结
在这里插入图片描述

八、文件缓冲区

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

在这里插入图片描述
(图片来自比特就业课)
比如我现在写了一个通讯录程序,然后我们程序运行的时候产生了100个人的信息,为了信息的长久使用,我们需要把这些数据写入文件中。

但我们这些数据不是直接进入文件的,而是内存中找一块空间,先放到我们的输出缓冲区。当我们输出缓冲区数据已经满了,然后我们才把输出缓冲区里的数据放到文件中。就好比你煮饭,你不可能有一粒米就下锅,你至少得有一碗米饭的量才下锅煮。

然后同样的,当我们想从文件中读取数据到程序中,我们也不是直接读取到程序数据区的,先将数据放到输出缓冲区,然后输出缓冲区满了放入程序数据区


# 总结 本文详细介绍了c语言的文件操作,内容量较多,读者可逐步分区块阅读,对于其中的一些文件操作函数需要读者耐心观看。以上,祝读者学习愉快,学业有成

猜你喜欢

转载自blog.csdn.net/m0_57180439/article/details/120665916