C语言文件操作超详解(万字解读,细致入微)-程序员宅基地

技术标签: c语言  C语言专栏  

本期博客会对C语言中文件操作进行详解

        在我们在写程序时,我们输入的数据在程序运行结束后将不会被记录保存,那么这时我们可以使用C语言中的文件操作将我们所需要的数据永久的保存在磁盘上,做到数据持久化,以便于下次我们不必再重复输入数据,可以直接在文件中读取数据。


目录

一、什么是文件

        1.程序文件

        2.数据文件

二、文件名

三、文件的打开和关闭

        1.文件指针

        2.文件操作——打开和关闭

        fopen函数(包含在头文件stdio.h中)的解析:

        fclose函数(包含在头文件stdio.h中)的解析:

四、文件的读写

        1.顺序读写

                fputc函数(包含在头文件stdio.h中)

                fgetc函数(包含在头文件stdio.h中)

                fputs函数(包含在头文件stdio.h中)

                fgets函数(包含在头文件stdio.h中)

                fprintf函数(包含在头文件stdio.h中)

                fscanf函数(包含在头文件stdio.h中)

                fwrite函数(包含在头文件stdio.h中)

                fread函数(包含在头文件stdio.h中)

                                延伸:输入流与输出流

                                延伸:sprintf和sscanf解析

        2.随机读写

                                文件指针的偏移

                fseek函数(包含在头文件stdio.h中)

                ftell函数(包含在头文件stdio.h中)

                rewind函数(包含在头文件stdio.h中)

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

六、文件读取结束的判定

        feof函数 (包含在头文件stdio.h中)

七、文件缓冲区


一、什么是文件

存放在磁盘上的文件就是文件

但是在程序设计中我们一般将文件分为两类:程序文件和数据文件(按文件的功能分类)。

        1.程序文件

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

        2.数据文件

                文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

        本博客主要讲述的是数据文件的读写操作。

        在没有文件操作的C语言终端程序中,数据的输入和输出都是以终端为对象的,即从终端的键盘输入数据,运行结果在屏幕上显示。但是在有文件操作的程序中,我们可以将终端的键盘输入的数据保存在磁盘中,或者将磁盘中的文件数据都取到程序内存中使用。

二、文件名

        一个文件需要有唯一的文件标识,以便用户的识别和引用。

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

        文件名包含三个部分:文件路径+文件名主干+文件后缀

        例如:D:\study\编程\C语言\学习\文件操作\test.c

                \之前的内容都是表示文件所在的路径,比如此test.c文件在D盘的study目录下的:编程目录下的:C语言目录下的:学习目录下的:文件操作目录下的位置。

                > 此文件的文件名主干是test

                > 此文件的文件名后缀是c

三、文件的打开和关闭

        在我们读写文件时需要先建立一个文件信息区来记录所要使用文件的各种数据(如文件的名字、文件状态及文件所在的当前位置等),而在C语言中记录这些状态的类型为文件指针:

        1.文件指针

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

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

                下图可以很直观的理解FILE类型的结构体和文件读取时的关系:

                

                 在VS2013中结构体FILE是被这样定义的:

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

                >不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

                >每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

                >一般都是通过一个FILE的指针来维护这个FILE结构的变量(通过FILE类型的指针找到文件信息区的起始地址),这样使用起来更加方便。

        例如:

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

        定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

        2.文件操作——打开和关闭

        在弄清楚文件操作的关联原理后,我们来进行文件打开和关闭的实操:

                >文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。

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

                >在编写程序的时候,在打开文件的同时,fopen函数都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

        fopen函数(包含在头文件stdio.h中)的解析:

         从对fopen函数的定义上来看此函数含有两个相同类型的const char*的参数filename和mode:

                >filename:传入将要打开的文件名(可以是相对路径也可以是绝对路径)。

        列如:

                此时text.txt是文件的相对路径:

                 我们看一下text.txt文件所在的的位置是在电脑D磁盘的study->编程->C语言->比特->学习->文件操作目录下:

                这样我们也可以写text.txt的绝对位置(目录下用“//”隔开,磁盘目录后不要忘了加“:”):

                >mode:传入文件的打开方式(打开方式有:"r","w","a","rb","wb","ab","r+","w+",

"a+","rb+","wb+","ab+"):

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

                注:当使用“w”,“wb”,“w+”,“wb+”打开文件时会清除文件原本存储的数据。

        该函数的返回值是一个FILE类型的指针,在文件成功打开后返回此文件信息区的起始地址,打开失败则返回一个NULL(空指针),所以使用fopen时需要一个FILE类型的指针来接收其返回值。

        例如

	//打开
	FILE* pf = fopen("text.txt","w");
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

        fclose函数(包含在头文件stdio.h中)的解析:

        我们先来看到fclose函数的定义:

        可以看到该fclose函数只有一个FILE*类型的参数stream:

                >stream:传入将要关闭的文件的文件指针。

        该函数的返回值是int类型的:如果关闭成功就返回0值,否则返回EOF(-1)值。

   下面是该函数的使用举例:

int main()
{
	//打开
	FILE* pf = fopen("text.txt","w");
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

 在关闭文件成功之后我们应该将原本的文件指针置为空(NULL),避免野指针的产生。

四、文件的读写

文件打开之后是要进行一系列读写操作的,下面来详细讲解一系列文件的读写操作:

        读写文件时我们有两种读取方式:顺序读取随机读取。

        1.顺序读写

                对于顺序读写我们会常用到以下函数:

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

                下面我们来一一介绍:

                fputc函数(包含在头文件stdio.h中)

                        该函数的作用是用来向文件输入单个字符数据的,先来看到该函数的定义:

                         该函数有int类型的character和FILE*类型的stream两个参数:

                >character:接收所要存入的字符的ASCII值

                >stream:接收指向所要存入文件的指针

        当该函数成功运行时返回所存入字符的ASCII码值,否则返回EOF(-1)值。

        运用举例:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "w");
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写入数据
	int i = 'a';
	for (i = 0; i < 26; i++)
	{
        fputc('a' + i, pf);
	}
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

        运行效果:

                fgetc函数(包含在头文件stdio.h中)

                该函数一般被用来读取文件中单个字符数据,来看定义:

                         该函数有一个FILE*类型的参数stream:

                >stream:接收指向所要读取文件的指针

        该函数成功读入数据时会返回读取字符的ASCII值,反之则会返回EOF(-1)值。

        运用举例:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "r");//此时读取文件要用"r"的方式打开
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读入数据
	int i;
	while ((i = fgetc(pf)) != EOF)
	{
		printf("%c ", i);
	}
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

        运行效果:

注:在程序运行时每次使用fgetc函数读入数据时文件指针在向后移动使其读取的数据不会重复。

                fputs函数(包含在头文件stdio.h中)

                读写文件一个一个字符操作太麻烦了,我们可以使用fputs函数直接向文件写入一个字符串:

                         可以看到该函数有const char*类型的str和FILE*类型的stream两个参数:

                >str:接收指向将要存入字符串的指针

                >stream:接收指向所要存入文件的指针

        该函数成功运行返回一个非负值,否则返回EOF(-1)值。

运用举例:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "w");
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//存入数据
	fputs("hello", pf);
	fputs("world", pf);
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

运行效果:

 从上图我们可以看出在文件中由于是顺序存储hello和world之间并没有任何的分隔标识,我们可以在存入字符串时在后面加上换行符“\n”来分隔:

 如此一来我们可以清晰看出文件中hello和world之间换行了(实际换行符也被存入了文件)。

                fgets函数(包含在头文件stdio.h中)

                对于输出文件文本行(字符串)数据一般使用fgets函数来进行操作,各位看官请移步至定义:

 

                 该函数有char*类型str、int类型num、FILE*类型stream三个不同类型的参数:

        >str:接收指向储存读出数据的字符串指针

        >num:接收要复制到 str 中的最大字符数(包括终止空字符

        >stream:接收指向所要读取文件的指针

成功后,该函数返回 str的头指针
如果在尝试读取字符时遇到文件结尾,则会设置 eof 指示符 。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(并且 str 的内容保持不变)。
如果发生读取错误,则设置错误指示器 (ferror) 并返回空指针(但 str 所指向的内容可能已更改)。

运用举例:

在此例子中我们可以看到当我们想读取五个字符时,实际只读取了“hello”四个字符和一个‘\0’,使用在我们使用时要在想读取字符串的长度上+1:

如上图:我们成功的读到了hello五个字符。

我们现在看一下我们读取文件的内容:

 可以看到文件所储存的第一行字符串为hello第二行字符串为world

现在我们让fgets函数读取10个字符的数据:

 可以看到当fgets函数读完第一行heiio数据后并未继续向后读取数据了,所以使用该函数只能读取一行的数据(现在各位应该理解了该函数为什么叫文本行输入函数了吧)。

但是我们可以连续进行两次读取来读取完整的数据: 

                fprintf函数(包含在头文件stdio.h中)

                目前我们所用的函数都是来操作字符的,那有没有一种函数函数可以向文件输出任意数据呢?fprintf函数就出现了:

                 我们来对比一下printf函数的定义:

                 仔细观察发现fprintf比printf函数唯一多出来的一个参数就是FILE*类型的stream。

                如此一来就很好解释fprintf函数的用法了:

        >stream:接收指向所要输出文件的指针

        >format:接收将要输出数据的格式(和printf函数一样有%d,%x,%c,%s等等格式类型)

        至于后面的...是接收所要输出的数据(如printf函数在格式后输入所要打印的变量一样)

该函数成功运行时会返回所输出的字节总数,否则返回一个负数并报错。

运用举例:

struct S
{
	int age;
	char name[20];
	char sex[5];
};
int main()
{
	struct S L = { 20,"张三","男" };
	//打开
	FILE* pf = fopen("text.txt", "w");
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//输入数据
	fprintf(pf, "%d %s %s", L.age, L.name, L.sex);
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

运行效果:

                fscanf函数(包含在头文件stdio.h中)

                既然都有各类型数据都能输入的fprintf,那为什么不能有一个可以读取各种类型的数据的函数fscanf函数存在呢:

                 接着和scanf函数的定义来做一下比较:

                 仔细观察发现fscanf比scanf函数唯一多出来的一个参数也是FILE*类型的stream。

                那和scanf函数的用法也大同小异了:

        >stream:接收指向所要输入文件的指针

        >format:接收将要输入数据的格式(和scanf函数一样有%d,%x,%c,%s等等格式类型)

        至于后面的...是接收所要输入的数据的变量(如scanf函数在格式后输入所要改变的变量一样)

运行成功后,该函数返回所读取数据的个数(甚至为零),如果在读取时发生读取错误或到达文件末尾,则会设置正确的指示器(feof 或 ferror)。而且,如果在成功读取任何数据之前发生任何一种情况,则返回 EOF

运用举例:

struct S
{
	int age;
	char name[20];
	char sex[5];
};
int main()
{
	struct S s;
	//打开
	FILE* pf = fopen("text.txt", "r");
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//输出数据
	fscanf(pf, "%d %s %s", &(s.age), s.name, s.sex);
	printf("%d %s %s", s.age, s.name, s.sex);
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

注:使用fscanf和scanf函数时一样非字符串类型的所要储存输入数据的变量需要&

运行效果:

                fwrite函数(包含在头文件stdio.h中)

                我们在向文件输出数据时都是以文本格式输出的,但是fwrite函数可以直接将计算机内存中所存储的二进制数据输出到文件中(此时此文件就是一个二进制文件),我们来速览定义:

                 此函数有着四个参数:const void*类型的ptr、size_t类型的size、size_t类型的count和FILE*类型的stream

        >ptr:接收指向所要输入数据的指针或地址

        >size:接收输出的每个数据的大小(以字节为单位)。

        >count:所要输出数据的个数

        >stream:接收指向所要输出文件的指针

成功运行返回输出的数据总数,如果此数字与 count 参数不同,则写入错误会阻止函数完成。在这种情况下,将为设置误差指示器ferror),如果大小计数为零,则该函数返回零,并且错误指示器保持不变。

运用举例:

struct S
{
	int age;
	char name[20];
	char sex[5];
};
int main()
{
	struct S L = { 20,"李四","女" };
	//打开
	FILE* pf = fopen("text.txt", "wb");//二进制输出时用wb
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//输出数据
	fwrite(&L, sizeof(struct S), 1, pf);
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

运行效果:

我们可以看到当使用二进制向文件输出时,记事本文件中所读取的数据我们肉眼是看不懂的(因为它是以文本形式读取的)。

                fread函数(包含在头文件stdio.h中)

                对于二进制的文件数据我们就要使用二进制的方式读取,fread函数就很好的做到了这一点:

                 该函数包含了void*类型的ptr、size_t类型的size、size_t类型的count、FILE*类型的stream四个参数:

        >ptr:传入指向将要存储数据的变量的指针或地址

        >size:传入每次从文件读取数据的大小(以字节为单位)

        >count:传入将要读取的次数

        >stream:接收指向所要输入文件的指针

该函数运行成功后返回读取数据的次数,否则返回0值。

运用举例:

struct S
{
	int age;
	char name[20];
	char sex[5];
};
int main()
{
	struct S s;
	//打开
	FILE* pf = fopen("text.txt", "rb");//二进制输出时用rb
	//判断是否打开成功
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//输入数据
	if (fread(&s, sizeof(struct S), 1, pf) != 0)
	{
		printf("%d %s %s", s.age, s.name, s.sex);
	}
	//关闭
	if (fclose(pf) == EOF)
	{
		//关闭失败
		perror("fclose");
		return 1;
	}
	pf = NULL;
	return 0;
}

运行效果:

                                延伸:输入流与输出流

                在我们编写的程序中我们可以将程序运行时的内存数据通过printf函数输入到屏幕,也可以将键盘上的数据通过scanf函数输入到内存中,这两种从屏幕输出数据和从键盘输入数据的方式分别被称为标准输出流标准输入流:

                那我们将文件数据输入或输出内存呢?这种方式输入或输出数据被称为输入(输出)流:

                 那我们这些输入和输出文件数据的函数是否能输入或输出外部设备数据(键盘、屏幕)呢?

        对任何一个c程序,只要运行起来,就默认打开3个流:

                stdin——标准输入流——键盘

                stdout——标准输出流——屏幕

                stderr ——标准错误流——屏幕

        也就是在C程序运行时,系统自动打开三个FILE*类型的流:stdin(接收键盘数据),stdout(接收所要向屏幕输出的数据),stderr(接收所要向屏幕输出的数据)。

下面我们开始代码实战:

int main()
{
	int ch = fgetc(stdin);
	fputc(ch, stdout);
	return 0;
}

        我们使用fgetc函数向stdin流读取数据,在用fputc函数向stdout流输出数据:、

效果展示:

 使用键盘向程序输入N字符结果将N字符打印在屏幕上。

我们再用fscanf函数和fprintf函数试一下:

int main()
{
	int a;
	fscanf(stdin, "%d", &a);
	fprintf(stdout, "%d", a);
	return 0;
}

效果演示:

 可以看到这样使用fscanf函数和fprintf函数相对于scanf函数和printf函数是等价的。

fscanf(stdin,···)等价于scanf(···)          fprintf(stdout,···)等价于printf(···)

                                延伸:sprintf和sscanf解析

                从上述延伸可以发现scanf和printf函数是对应于标准输入输出流,fscanf和fprintf函数(都包含在头文件stdio.h中)对应于所有输入输出流。另外还有sscanf函数和sprintf它们可以实现对于字符串和各种类型数据的相互转换。

                废话不多说,直接上定义:

                对于sprintf函数它可以将各种数据以各种格式(如%d,%s,%c等等)转换为字符串类型输入到char*类型的str参数中。

                对于sscanf函数它可以将字符串类型的str参数的数据以各种格式(如%d,%s,%c等等)输出到各变量中。

运用实例:

struct S
{
	int age;
	char name[20];
	float point;
};
int main()
{
	struct S l = { 15,"lisi",88.9f };
	char arr[20];
	struct S j = { 0 };
	//将各类型数据转换为字符串
	sprintf(arr, "%d %s %.1f", l.age, l.name, l.point);
	printf("%s\n", arr);
	//将字符串数据转换为各种类型
	sscanf(arr, "%d %s %f", &(j.age), j.name, &(j.point));
	printf("%d %s %.1f", j.age, j.name, j.point);
	return 0;
}

运行效果:

 从上述例子可以看出sprintf函数很好的将l结构体的各成员数据以特定的格式全部转换为字符串类型储存到了字符串arr中,sscanf函数也很好的将字符串arr以特定的格式转化到了j结构体的各成员中。

        2.随机读写

        我们在读写文件时不仅仅可以按顺序读写还可以随机读写(从文件某一位置开始读)

        在介绍随机读写函数之前我们要了解一下

                                文件指针的偏移

        我们直接用实例演示:

                首先我们先创建一个文件,文件名为text.txt

                 在我们创建文件时可以直接用fopen函数的“w”模式来创建,同时也可以手动创建:

        注:在自己手动创建文件时要注意将文件扩展名显示出来以避免我们重复命名文件后缀

                 接着在文件输入一些数据并保存:

                                 (我在文件中随机输入了一些数据来举例)

                 之后我们用fgetc函数来读取文件信息:

                 仔细观察发现fgetc函数每读完一次数据时下一次再重新读取时不会从文件开始的位置去读,而是从上一次读取结束的地方再次读取,这实际是FILE*类型的指针的偏移:

                 上图是对文件指针偏移的模拟。

        下面我们一一来分析随机读写文件的函数:

                fseek函数(包含在头文件stdio.h中)

                 该函数可以设置文件指针指向的位置及偏移量。

                 可以看到该函数有FILE*类型stream,long int类型offset,int类型origin三个参数:

        >stream:传入将要改变的文件指针(文件流)

        >offsrt:传入指针所要偏移的偏移量

        >origin:设置传入的stream指针的起始位置(可传入3种参数:SEEK_SET(设置指针指向文件开头)、SEEK_CUR(不改变指针位置)、SEEK_END(设置指针指向文件结束位置))

该函数运行成功,返回零。否则回非零值。

如果发生读取或写入错误,则设置错误指示器ferror)。

运用举例:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//设置pf指针从文件起始位置向后偏移3个单位
	fseek(pf, 3, SEEK_SET);
	//读入数据
	int i = fgetc(pf);
	printf("%c", i);
	//关闭
	fclose(pf);
	pf = NULL;
	return 0;
}

运行效果:

                ftell函数(包含在头文件stdio.h中)

                ftell函数可以返回文件指针相对于起始位置的偏移量:

                 该函数只有一个FILE*类型的参数stream:

        >stream:传入所需计算偏移量的文件指针

成功后,返回位置指示器的当前值。失败时,返回 -1L,并将 errno 设置为系统特定的正值。

运用举例:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//设置偏移量
	fseek(pf, 0, SEEK_END);
	//计算偏移量
	printf("%d", ftell(pf));
	//关闭
	fclose(pf);
	pf = NULL;
	return 0;
}

运行效果:

                rewind函数(包含在头文件stdio.h中)

                该函数可以将所传入的文件指针设置指向文件初始位置:

                 该函数只有一个FILE*类型的参数stream:

        >stream:传入将要改变的文件指针

运用举例:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//设置偏移量
	fseek(pf, 0, SEEK_END);
	//将指针返回到初始位置
	rewind(pf);
	//计算偏移量
	printf("%d", ftell(pf));
	//关闭
	fclose(pf);
	pf = NULL;
	return 0;
}

 运行效果:

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

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

                >数据在内存中以二进制的形式存储,如果不加转换的输出到外存(磁盘文件等等),就是二进制文件。

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

                >数据在内存中字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

                我们拿10000这个数据来举例,用ASCll码来存储需要5个字节(因为10000是五位数,每一位数都要用一个字节的ASCll码来表示),而用二进制存储只需要四个字节(一个整型大小为4字节)。

 上图是模拟计算机用两种不同的方式存储10000这个数据。

下面我们使用二进制存储的方式存入10000这个数据:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//存入
	int a = 10000;
	fwrite(&a, sizeof(int), 1, pf);
	//关闭
	fclose(pf);
	pf = NULL;
	return 0;
}

 存入后使用记事本打开此文件:

此时显示的是乱码(记事本看不懂二进制文件)

那我们想看懂二进制文件该怎么办呢?

VS就可以帮助我们读取二进制文件(VS可以看懂二进制文件): 

 将二进制文件添加到编译器中使用二进制编译器的打开方式打开:

可以看到10000这个数据在二进制中存储为10 27 00 00(前面00000000是地址不需要在意)。10 27 00 00是将二进制数据转换为十六进制并以小端存储方式显示的。 

六、文件读取结束的判定

        在我们使用读取文件的函数时,都讲解了其返回值,我们可以通过其返回值来判定对文件的读取是否成功(这里不再一一举例)。

        补充一个用来判断文件为什么结束读取的函数:

        feof函数 (包含在头文件stdio.h中)

                对于文件读取结束之后,我们可以使用feof函数来文件是为何而读取结束的:

  

                该函数只有一个FILE*类型的参数:

        >stream:传入所需要判断的文件指针

当传入的文件流是遇到文件末尾而结束读取时该函数返回非0值,其他原因返回0。

七、文件缓冲区

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

        以下是程序向硬盘输入数据和硬盘向程序输出数据的流程:

 我们可以用以下例子来感受一下文件缓冲区的存在:

int main()
{
	//打开
	FILE* pf = fopen("text.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//存入
	int a = 10000;
	fwrite(&a, sizeof(int), 1, pf);
	printf("此20秒数据在文件缓冲区内,打开文件是没有数据的\n");
	Sleep(20000);//睡眠10秒
	fflush(pf);//此函数可以刷新缓冲区中的数据,使其存入硬盘文件中
	printf("此20秒数据从文件缓冲区内读入到文件中,打开文件是有数据的\n");
	Sleep(20000);
	//关闭
	fclose(pf);
	pf = NULL;
	return 0;
}

运行效果: 

 

 

这里可以得出一个结论: 
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。


本期博客对C语言中文件操作做了全方位的讲解,内容量大难免会有误区,还请各位看客不吝赐教。

如果本博客对您有帮助的话请多多关注,博主持续更新中~

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_70811813/article/details/127218742

智能推荐

解决win10/win8/8.1 64位操作系统MT65xx preloader线刷驱动无法安装_mt65驱动-程序员宅基地

文章浏览阅读1.3w次。转载自 http://www.miui.com/thread-2003672-1-1.html 当手机在刷错包或者误修改删除系统文件后会出现无法开机或者是移动定制(联通合约机)版想刷标准版,这时就会用到线刷,首先就是安装线刷驱动。 在XP和win7上线刷是比较方便的,用那个驱动自动安装版,直接就可以安装好,完成线刷。不过现在也有好多机友换成了win8/8.1系统,再使用这个_mt65驱动

SonarQube简介及客户端集成_sonar的客户端区别-程序员宅基地

文章浏览阅读1k次。SonarQube是一个代码质量管理平台,可以扫描监测代码并给出质量评价及修改建议,通过插件机制支持25+中开发语言,可以很容易与gradle\maven\jenkins等工具进行集成,是非常流行的代码质量管控平台。通CheckStyle、findbugs等工具定位不同,SonarQube定位于平台,有完善的管理机制及强大的管理页面,并通过插件支持checkstyle及findbugs等既有的流..._sonar的客户端区别

元学习系列(六):神经图灵机详细分析_神经图灵机方法改进-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏27次。神经图灵机是LSTM、GRU的改进版本,本质上依然包含一个外部记忆结构、可对记忆进行读写操作,主要针对读写操作进行了改进,或者说提出了一种新的读写操作思路。神经图灵机之所以叫这个名字是因为它通过深度学习模型模拟了图灵机,但是我觉得如果先去介绍图灵机的概念,就会搞得很混乱,所以这里主要从神经图灵机改进了LSTM的哪些方面入手进行讲解,同时,由于模型的结构比较复杂,为了让思路更清晰,这次也会分开几..._神经图灵机方法改进

【机器学习】机器学习模型迭代方法(Python)-程序员宅基地

文章浏览阅读2.8k次。一、模型迭代方法机器学习模型在实际应用的场景,通常要根据新增的数据下进行模型的迭代,常见的模型迭代方法有以下几种:1、全量数据重新训练一个模型,直接合并历史训练数据与新增的数据,模型直接离线学习全量数据,学习得到一个全新的模型。优缺点:这也是实际最为常见的模型迭代方式,通常模型效果也是最好的,但这样模型迭代比较耗时,资源耗费比较多,实时性较差,特别是在大数据场景更为困难;2、模型融合的方法,将旧模..._模型迭代

base64图片打成Zip包上传,以及服务端解压的简单实现_base64可以装换zip吗-程序员宅基地

文章浏览阅读2.3k次。1、前言上传图片一般采用异步上传的方式,但是异步上传带来不好的地方,就如果图片有改变或者删除,图片服务器端就会造成浪费。所以有时候就会和参数同步提交。笔者喜欢base64图片一起上传,但是图片过多时就会出现数据丢失等异常。因为tomcat的post请求默认是2M的长度限制。2、解决办法有两种:① 修改tomcat的servel.xml的配置文件,设置 maxPostSize=..._base64可以装换zip吗

Opencv自然场景文本识别系统(源码&教程)_opencv自然场景实时识别文字-程序员宅基地

文章浏览阅读1k次,点赞17次,收藏22次。Opencv自然场景文本识别系统(源码&教程)_opencv自然场景实时识别文字

随便推点

ESXi 快速复制虚拟机脚本_exsi6.7快速克隆centos-程序员宅基地

文章浏览阅读1.3k次。拷贝虚拟机文件时间比较长,因为虚拟机 flat 文件很大,所以要等。脚本完成后,以复制虚拟机文件夹。将以下脚本内容写入文件。_exsi6.7快速克隆centos

好友推荐—基于关系的java和spark代码实现_本关任务:使用 spark core 知识完成 " 好友推荐 " 的程序。-程序员宅基地

文章浏览阅读2k次。本文主要实现基于二度好友的推荐。数学公式参考于:http://blog.csdn.net/qq_14950717/article/details/52197565测试数据为自己随手画的关系图把图片整理成文本信息如下:a b c d e f yb c a f gc a b dd c a e h q re f h d af e a b gg h f bh e g i di j m n ..._本关任务:使用 spark core 知识完成 " 好友推荐 " 的程序。

南京大学-高级程序设计复习总结_南京大学高级程序设计-程序员宅基地

文章浏览阅读367次。南京大学高级程序设计期末复习总结,c++面向对象编程_南京大学高级程序设计

4.朴素贝叶斯分类器实现-matlab_朴素贝叶斯 matlab训练和测试输出-程序员宅基地

文章浏览阅读3.1k次,点赞2次,收藏12次。实现朴素贝叶斯分类器,并且根据李航《统计机器学习》第四章提供的数据训练与测试,结果与书中一致分别实现了朴素贝叶斯以及带有laplace平滑的朴素贝叶斯%书中例题实现朴素贝叶斯%特征1的取值集合A1=[1;2;3];%特征2的取值集合A2=[4;5;6];%S M LAValues={A1;A2};%Y的取值集合YValue=[-1;1];%数据集和T=[ 1,4,-1;..._朴素贝叶斯 matlab训练和测试输出

Markdown 文本换行_markdowntext 换行-程序员宅基地

文章浏览阅读1.6k次。Markdown 文本换行_markdowntext 换行

错误:0xC0000022 在运行 Microsoft Windows 非核心版本的计算机上,运行”slui.exe 0x2a 0xC0000022″以显示错误文本_错误: 0xc0000022 在运行 microsoft windows 非核心版本的计算机上,运行-程序员宅基地

文章浏览阅读6.7w次,点赞2次,收藏37次。win10 2016长期服务版激活错误解决方法:打开“注册表编辑器”;(Windows + R然后输入Regedit)修改SkipRearm的值为1:(在HKEY_LOCAL_MACHINE–》SOFTWARE–》Microsoft–》Windows NT–》CurrentVersion–》SoftwareProtectionPlatform里面,将SkipRearm的值修改为1)重..._错误: 0xc0000022 在运行 microsoft windows 非核心版本的计算机上,运行“slui.ex