资讯专栏INFORMATION COLUMN

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

Alfred / 3051人阅读

摘要:二什么是文件磁盘上的文件就是文件。文件指针变量定义是一个指向类型数据的指针变量。表示向何种流中输出,可以是标准输出流,也可以是文件流。文件结构体指针,将要读取的文件流。

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


一、为什么使用文件

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

所以这些通讯录数据我们仅仅放在内存里是不行的,大家都知道,我们电脑的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//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//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进行打印,并判断是否结束呢?

代码如下:

#includeint main(){	    //打开文件		FILE*pf=fopen("E://c-language-notes//test.21.10.11//data.txt", "r");		if (pf == NULL)		{			perror("fopen");			return -1;		}
            
                     
             
               

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/122562.html

相关文章

发表评论

0条评论

最新活动
阅读需要支付1元查看
<