随笔——文件操作

文件

看下面的程序,我们发现,第二次运行程序的时候n不是我们第一次输入的数字,为什么呢?

#include<stdio.h>

int main()
{
	int n = 0;
	printf("%d\n", n);
	scanf_s("%d", &n);
	return 0;
}

第一次运行:
在这里插入图片描述
第二次运行:
在这里插入图片描述
这是因为我们写的程序数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了。如果想把数据长时间保存下来,就要用到文件。

文件是什么?

广义上来讲,文件是计算机上存储信息的基本单位。可以理解为一些电子数据的集合,这些数据可以是文本,图片,音频,视频等不同形式的内容。文件储存在电脑硬盘或者其他存储设备(如U盘,移动硬盘盘,光盘等)上,并且每个文件都有唯一的文件名,特定的文件格式。

但在程序设计中,我们所说的文件通常是指“程序文件”“数据文件”这两种(从文件功能的角度来说)。

程序文件是用于指导计算机执行特定操作的文件,包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)

数据文件 用于储存由用户或程序生成的数据。它们可以包含各种类型的信息,如文本、图片、音乐、视频等。数据文件一般有特定的格式,这取决于创建它们的程序。

本文讨论的是数据文件。

以前我们所说的数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实更多时候,我们需要把这些数据变为文件存入到存储设备中,当需要的时候再从存储设备上把数据读取到内存中使用。

每个文件都有一个唯一的文件标识,以便于用户识别和引用。
文件名包括三部分:文件路径+文件名主干+文件后缀,比如:c: \ code\ test.txt;为了方便起见,文件标识常被称为文件名。

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

我们都知道,数据在内存中是以二进制的形式存储的,如果把这二进制序列串不经过任何转换,直接输出到硬盘U盘光盘等存储设备而产生的文件就是二进制文件;

如果这二进制串是经过某种转换,比如ASCII码,再存入到存储设备中所产生的文件就是文本文件;

⼀个数据在文件中是怎么存储的呢?

字符⼀律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节,分别是’1’‘0’‘0’‘0’‘0’),而二进制形式输出,则在磁盘上占用4个字节(因为10000是个整数,也就是整型,整型四字节)。

下面是代码:

#include<stdio.h>
#include<string.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");//把文件test.txt以"w"(只写)的形式打开
	if (pf == NULL)
	{
		perror("Open test.txt:");//如果打开失败,显示错误信息
		return;
	}
	fputs("10000", pf);//把五个字符(严格来说是六个,因为后面还有'\0')写进去
	fclose(pf);
	pf = NULL;
	return 0;
}

执行过上面程序后,项目文件夹就会多了一个叫做"test.txt"的文件,这是因为,如果项目文件夹没有这个文件,fopen就会自己建一个。
点击VS打开文件按钮:

在这里插入图片描述
在这里插入图片描述
双击一下就可以看到了:
在这里插入图片描述
刚刚是以字符串的形式存,接下来是以二进制的形式存:

#include<stdio.h>
#include<string.h>

int main()
{
	int n = 10000;
	FILE* pf = fopen("test.txt", "wb");//把文件test.txt以"wb"(只写)的形式打开
	if (pf == NULL)
	{
		perror("Open test.txt:");//如果打开失败,显示错误信息
		return;
	}
	fwrite(&n, 4, 1, pf);//以二进制的方式把内存中四字节大小数据一次性写入文件
	fclose(pf);
	pf = NULL;
	return 0;
}

执行完后,照着之前的步骤,我们发现:
在这里插入图片描述
弹出了这个东西:
在这里插入图片描述
不是这么看的,要换个流程:
首先打开解决方案资源管理器:
在这里插入图片描述
右键源文件,添加现有项:
在这里插入图片描述
双击test.txt:
在这里插入图片描述
右键test.txt,选择打开方式:
在这里插入图片描述
找到二进制编辑器,确定:
在这里插入图片描述
然后就看到了数值1000了,其它东西不用管。(前面那串零个人估计可能是类似地址的东西)
在这里插入图片描述
这里的10 27 00 00就是内存中的二进制序列串(为了方便观察,是以十六进制显示的);
10000的二进制原码为:00000000 00000000 00100111 00010000,转换成补码就是00000000 00000000 00100111 00010000,超过一个字节,VS小端存储,内存实际存入的是00010000 00100111 00000000 00000000,换成十六进制就是10 27 00 00。

我不确定你们在高中时是否选择了生物作为选修课,但我可以告诉你们,我选择了。在我们的生物课本中,我们了解了细胞膜是细胞与外界进行物质和信息交换的媒介,可以说,这可能是我们第一次系统地接触到了信息交流的概念。在信息通信方面,细胞膜上的受体蛋白能感知到细胞外部环境中的特定信号分子(例如激素或神经递质),并通过内部的信号路由,将这些信息传递到细胞核,于是,特定的基因片段被激活,对环境做出了动态调整。

类似地,计算机程序也需要一种媒介来进行信息交流,这就是我们所说的流。和细胞膜的作用相似,如果我们需要操作文件或者处理数据,就需要借助于某种特定流。

流的存在,使得程序与外界进行信息交流变得尽可能直观简单。如果没有流,那么程序员可能还需要深入学习信息论,考虑串行并行通信等问题,而这需要具有深厚的计算机科学知识,毕竟,能理解并精通这些理论的,已然不再是普通的程序员,而是值得尊重的计算机科学家。还好,流的存在,让信息的输入和输出变得直接简单,对于程序员来说,我们只需要将信息投入流中,或者从流中读取信息,其他事情则不需要考虑。

C/C++中流的分类

在C/C++中,有四种流:

  • 输入/输出(I/O)流
  • 字节流和字符流
  • 缓冲与非缓冲流
  • 数据流

其中,输入/输出(I/O)流和字节流和字符流将是本文讨论的重点。

标准流

回到之前的代码:

#include<stdio.h>

int main()
{
	int n = 0;
	printf("%d\n", n);
	scanf_s("%d", &n);
	return 0;
}

我们发现,这每一步我都看得懂呀,我怎么没看到有什么把流打开的操作呢?没把流打开,我怎么把键盘输入的信息读出来,又把这段信息显示在屏幕上呢?

事实上,在这段代码中,确实没有对流打开的操作,这是因为,C语言程序在启动的时候,默认打开了3个流:

  • stdin-标准输入流,在大多数环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
  • stdout-标准输出流,大多数环境中输入至显示器界面,printf函数就是把信息输出到标准输出流中。
  • stderr-标准错误流,大多数环境中输出到显示器界面。

这三种流都从属于输入/输出(I/O)流;就是因为默认打开了这三种流,我们才可以直接使用scanf,printf这种函数进行信息交流;

正如我们使用指针来管理和操作变量一样,我们也可以利用FILE*,即流的指针,来管理和操作数据流。

FILE* 在中文中常被翻译为’文件指针’,然而我个人认为这种翻译有些偏颇,可能会导致误解。因为实际上,FILE* 是作为流——即文件与程序进行信息交流的媒介,而非文件本身。然而,由于FILE* 最常被用于文件的读写操作,所以’文件指针’这种说法逐渐成为了通用词汇。


备注:这里讲的输入指外界向程序输入信息,或者说是程序从外界读取信息;输出指程序向外界输出信息,或者说是程序向外界写入信息;后面的也是这样,不要弄错主语了。


文件指针

文件指针,全称为文件类型指针,是用于文件操作的重要工具。当我们打开一个文件时,系统会根据文件的具体情况,在内存中建立一个用以描述该文件的结构体变量。这个结构体包含了如文件名、文件状态、文件当前位置等文件相关信息。此种结构体类型在stdio.h头文件中声明,类型名为FILE。

在版本较低的VS中,我们在文件stdio.h中可以直接看到该结构体的声明,(版本高的封装太多层了,可能看不到)

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

请注意,不同编译器厂商所设计的FILE类型并不完全一致,但大同小异。

文件的打开和关闭

我们对文件的操作是有固定流程的:想要使用某个文件,先打开它,然后进行各种操作,用完了,就把文件关上。

C标准规定用fopen函数来打开文件,fclose来关闭文件

fopen

头文件:stdio.h

函数声明:

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

其中,filename指的是文件名,一般情况下不包括文件路径,若系统支持,也可包含路径;
mode是个字符串指针,用来描述哪些对文件的操作是被允许的,这些操作又是如何执行的,具体如下:

模式含义
“r”(只读)为了输入数据,打开一个已经存在的文本文件;如果文本文件不存在,则打开失败
“w”(只写)为了输出数据,打开一个文本文件;如果文本文件存在,则清除原有内容,使之变为空白文件;如果文本文件不存在,则新建一个同名文件
“a” (追加)为了输出数据,打开一个文本文件;如果文本文件存在,则不改变原有内容;如果文本文件不存在,则新建一个同名文件
“r+”(读写)为了输入和输出数据,打开一个已经存在的文本文件;如果文本文件不存在,则打开失败
“w+”(读写)为了输出和输入数据,打开一个文本文件;如果文本文件存在,则清除原有内容,使之变为空白文件;如果文本文件不存在,则新建一个同名文件
“a+”(追加)为了输出和输入数据,打开一个文本文件;如果文本文件存在,则不改变原有内容;如果文本文件不存在,则新建一个同名文件

下面讲一下各模式的光标起始位置

模式光标起始位置
“r”(只读)文本开始
“w”(只写)文本开始
“a” (追加)文本末尾
“r+”(读写)文本开始
“w+”(读写)文本开始
“a+”(追加)文本末尾

注:
加上 “b” :以二进制方式打开文件,例如 “rb”、“wb”、“ab”、“r+b”、“w+b”、“a+b”;后三种模式也可写作"rb+“、“wb+”、“ab+”。
加上 “x”:如果文件不存在,新建并打开用于读写,否则操作失败,比如"wx”、“w+x”。
mode是个字符串指针,输的时候一定要用双引号,而不是单引号。
文件打开失败会返回空指针,并产生特定的错误代码,可以用strerror或者perror接收。

fclose

头文件:stdio.h

函数声明:

int fclose ( FILE * stream );

没什么好说的,参数就是文件指针,成功关闭返回0,不成功,返回EOF。

示例

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	//文件操作
	fputs("hello world.", pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	//打开文件
	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	//文件操作
	char str[13] = { 0 };
	fgets(str, 13, pf);
	printf("%s\n", str);
	//文件关闭
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

文件的顺序读写

fputc

头文件:stdio.h
函数主要功能:把字符(c)放置(put)到文件(f)中
函数声明:

int fputc ( int character, FILE * stream );

stream是文件指针,character是字符,你说这字符怎么是int类型的?因为这是ASCII码形式的字符,如果成功,返回这个字符;如果不成功,返回EOF,错误信息可以用strerror接收。
比如我们输出一个字符‘h’:

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	//文件操作
	fputc('h', pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

我们打开test.txt文件看一下:
在这里插入图片描述
现在我把26个英文字母都写进去:

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	//文件操作
	char ch = 0;
	for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, pf);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

你看,写入成功了;有人讲,这光标位置不对呀,怎么在前面呀?前面讲错了?前面没讲错,我们前面说的光标是对读写文本来说的,这张图片里的光标和我们前面讲的光标不是同一个光标,图片里的光标默认在起始位置
在这里插入图片描述


刚刚我们说的是fputs的一般用法,但实际上fputs适用于所有输入流,而上文中对文件进行输入的流只是输入流中的一种,为了区分,我们下面把进行文件输入\输出的流称为文件输入\输出流,文件输入\输出流只是输入\输出流的一部分,比如下面我们把文件指针换成标准输出流指针

#include<stdio.h>

int main()
{
	char ch = 0;
	for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, stdout);
	}
	return 0;
}

在这里插入图片描述

fgetc

头文件:stdio.h
函数主要功能:从文件(f)中得到(get)字符(c)
函数声明:

int fgetc ( FILE * stream );

stream 指的是文件指针,读取成功返回读取得到的字符(ASCII码形式,所以返回值是int);读取失败返回EOF(EOF是-1,还是int),int是专门为那个EOF定的

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	//文件操作
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c", ch);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述


同样的,文件输入流只是输入流中的一部分,而fgetc适用于所有输入流,比如标准输入流

#include<stdio.h>

int main()
{
	char ch = 0;
	//键盘输入字符'h'
	ch = fgetc(stdin);
	printf("ch=%c", ch);
	return 0;
}

在这里插入图片描述

fputs

头文件:stdio.h
函数主要功能:把字符串(s)放置(put)到文件(f)中
函数声明:

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

将str指向的字符串 写入文件。 该函数从指定的地址 ( str ) 开始复制,直到到达终止空字符 ( ‘\0’ )。此终止空字符不会复制到文件中;如果成功,则返回一个非负值;如果出错,该函数将返回EOF,错误信息可以用strerror接收

如果这样写的话,字符串不会换行:

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	//文件操作
	fputs("hello world", pf);
	fputs("Genshin Impact, Start Up!", pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

换行要这样写:

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	//文件操作
	fputs("hello world\n", pf);
	fputs("Genshin Impact, Start Up!\n", pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述


同样的,文件输出流只是输出流的一部分,而fputs适用于所有输出流,下面我们把输出流改成标准输出流

#include<stdio.h>

int main()
{
	fputs("hello word", stdout);
	return 0;
}

在这里插入图片描述

fgets

头文件:stdio.h
函数主要功能:从文件(f)中得到(get)字符串(s)
函数声明:

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

从文件中读取字符并将它们作为 字符串存储到str中,直到读取到 ( num -1) 个字符或者到达换行符或文件末尾(以先发生者为准)。换行符会使fgets停止读取,但函数会将其视为有效字符,并将其包含在复制到str 的字符串中。在读取并拷贝完后,函数会自动在字符串后追加一个终止空字符’\0’;成功时,该函数返回str,失败时返回空指针

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	//文件操作
	char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
	fgets(str, 10, pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

通过调试可以明显看到,只读取了9个字符,第十个字符是’\0’
在这里插入图片描述
在这里插入图片描述
现在把num改成20再试试

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	//文件操作
	char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
	fgets(str, 20, pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
把’\n’读取后就不再读取了
在这里插入图片描述
现在我们把全部内容都读出来:

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	//文件操作
	char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
	char* i = NULL;
	while ((i = fgets(str, 20, pf)) != NULL)
	{
		printf("%s", str);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
要是把num改成2也能全部读出来吗?(改成1可就真读不出来了,因为实际读取个数是num-1)

#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	//文件操作
	char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
	char* i = NULL;
	while ((i = fgets(str, 2, pf)) != NULL)
	{
		printf("%s", str);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
为什么呢?
当num等于2的时候,实际读取字符是一个;
第一次读取,读到’h’,str就是"h\0"(后面内容打印不出来,我们就不考虑了)打印了一个’h’
第二次读取,读到’e’,str就是"e\0",打印了一个’e’
读了很多次,读到’\n’,str就是"\n\0",打印换行

随笔——文件操作——使用fgets读取文本


同样,文件输入流只是输入流的一部分,而fgets是适用于所有输入流的,比如标准输入流

#include<stdio.h>

int main()
{
	char str[20] = { 0 };
	//输入"hello world\n"
	fgets(str, 20, stdin);
	printf("str = %s", str);
	return 0;
}

在这里插入图片描述
如果最后不按回车,则必须输满19个字符才能进行下一步的打印操作

fprintf

头文件:stdio.h
函数主要功能:将格式化的数据输出到文件上
函数声明:

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

一看这名字就知道这函数肯定和printf有关系,实际上,printf就是fprintf的简化版,printf的函数声明是

int printf ( const char * format, ... );

就比fprintf少一个参数,其它都是一样的,返回值都是成功输出的数据个数,如果出错,就返回EOF,错误信息可以用strerror来接收

#include<stdio.h>

 //这里为了多整几个格式,声明一个结构体
struct t
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct t e = { "张三",20,65.6f };
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fprintf(pf, "%s %d %f", e.name, e.age, e.score);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述


同样的,文件输出流仅是输出流中的一部分,而fprintf适用于所有的输入流,实际上,printf对fprintf简化就是默认那个输出流是标准输出流;

#include<stdio.h>

 //这里为了多整几个格式,声明一个结构体
struct t
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct t e = { "张三",20,65.6f };
	fprintf(stdout, "%s %d %f", e.name, e.age, e.score);
	return 0;
}

在这里插入图片描述

fscanf

头文件:stdio.h
函数主要功能:把文件中格式化的数据输入到程序中
函数声明:

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

一看这名字就知道这函数肯定和scanf有关系,实际上,scanf就是fscanf的简化版,scanf的函数声明是

int scanf ( const char * format, ... );

就比fscanf少一个参数,其它都是一样的,返回值都是成功输入的数据个数,如果出错,就返回EOF,错误信息可以用strerror来接收

#include<stdio.h>

 //这里为了多整几个格式,声明一个结构体
struct t
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct t e = { 0 };
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	fscanf(pf, "%s %d %f", e.name, &(e.age), &(e.score));
	fprintf(stdout, "%s %d %f", e.name, e.age, e.score);
	fclose(pf);
	pf = NULL;
	return 0;
}

同样的,文件输入流只是输入流中的一种,而fscanf适用于所有输入流,比如,标准输入流

#include<stdio.h>

 //这里为了多整几个格式,声明一个结构体
struct t
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct t e = { 0 };
	//输入张三 20 65.6f
	fscanf(stdin, "%s %d %f", e.name, &(e.age), &(e.score));
	fprintf(stdout, "%s %d %f", e.name, e.age, e.score);
	return 0;
}

在这里插入图片描述


题外话

sprintf

头文件:stdio.h
函数功能:将格式化的数据输出到字符流,或者通俗的说,是把格式化的数据转化成字符串
函数声明:

int sprintf ( char * str, const char * format, ... ) ;

如果成功,返回输出的数据个数,失败,返回负数

#include<stdio.h>

 //这里为了多整几个格式,声明一个结构体
struct t
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct t e = { "张三",20,65.6f };
	char str[100] = { 0 };
	sprintf(str, "%s %d %f", e.name, e.age, e.score);
	fprintf(stdout, "%s", str);
	return 0;
}

在这里插入图片描述

sscanf

头文件:stdio.h
函数功能:把字符流中的格式化数据输入到程序中,或者说把字符串转换成格式化的数据
函数声明:

int sscanf (const char * s,const char * format,...);

返回值是成功输入的数据个数,如果出错,就返回EOF

#include<stdio.h>

 //这里为了多整几个格式,声明一个结构体
struct t
{
	char name[20];
	int age;
	float score;
};

int main()
{
	struct t e = { "张三",20,65.6f };
	struct t s = { 0 };
	char str[100] = { 0 };
	sprintf(str, "%s %d %f", e.name, e.age, e.score);
	sscanf(str, "%s %d %f", s.name, &(s.age), &(s.score));
	fprintf(stdout, "%s %d %f", s.name, s.age, s.score);
	return 0;
}

在这里插入图片描述


fwrite

头文件:stdio.h
函数功能:把数据以二进制的形式输出到文件中,本函数只适用于文件输出流
函数声明:

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

从ptr指向的地址开始,把count个元素(元素大小为size字节)的数据以二进制的形式输出到stream 指向的文件输出流中

#include<stdio.h>

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("Open test.txt(wb)");
		return;
	}
	size_t sz = sizeof(arr) / sizeof(arr[0]);
	fwrite(arr, sizeof(arr[0]), sz, pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

右键源文件,选择现有项,添加test.txt,右键test.txt,点击打开方式,选择二进制编辑器
在这里插入图片描述

fread

头文件:stdio.h
函数功能:把二进制数据输入到程序中,本函数只适用于文件输入流
函数声明:

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

从stream指向的文件输入流中读取二进制序列,将它们视为const个大小为size的数据存入ptr指向的内存块中
返回成功读取的元素总数

#include<stdio.h>

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("Open test.txt(wb)");
		return;
	}
	size_t sz = sizeof(arr) / sizeof(arr[0]);
	fwrite(arr, sizeof(arr[0]), sz, pf);
	fclose(pf);
	pf = NULL;

	int a[5] = { 0 };
	pf = fopen("test.txt", "rb");
	if (pf == NULL)
	{
		perror("Open test.txt(rb)");
		return;
	}
	size_t t = sizeof(a) / sizeof(a[0]);
	fread(a, sizeof(int), t, pf);
	int i = 0;
	for (; i < 5; i++)
	{
		printf("%d ", a[i]);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
有时候,可能不知道文件里到底有多少数据,那怎么一次性全读出来呢?这时候就要看返回值了,如果返回值小于count不就说明读到末尾读完了吗?(当然前提是数组足够大)

#include<stdio.h>

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("Open test.txt(wb)");
		return;
	}
	size_t sz = sizeof(arr) / sizeof(arr[0]);
	fwrite(arr, sizeof(arr[0]), sz, pf);
	fclose(pf);
	pf = NULL;
	pf = fopen("test.txt", "rb");
	if (pf == NULL)
	{
		perror("Open test.txt(rb)");
		return;
	}
	int a[5] = { 0 };
	int i = 0;
	while (fread(a + i, sizeof(int), 1, pf))
	{
		i++;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", a[i]);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

文件的随机读写

前面我们说的都是顺序读写,这顺序读写的顺序体现在哪里呢?体现在光标的移动方向是固定的,光标只能被动地跟随数据的读写而改变。比如说,现在test.txt文件内容是这样的:
在这里插入图片描述
我们以"r"的形式打开这个文件流,初始的光标位置就在文本开始位置,也就是"h"前面,那现在我们用来fgetc读一个字符,就会读出字符"h",光标也会移动,跑到"e"前面,以此类推,我们每读取一定长度的文本,光标也会向后移动一定长度,其他模式也是如此。

如果要对文章进行随机读写,或者说,主动地改变光标位置,则需要借助于一些函数

fseek

头文件:stdio.h
函数功能:更改文件流的光标位置
函数声明:

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

其中的stream是要更改光标的文件流,offset是偏移量,origin 是偏移量的参考基准,origin 有三种选择,分别是SEEK_SET(文件开头),SEEK_CUR(光标当前位置),SEEK_END(文件末尾)。
在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, 0, SEEK_SET);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, 1, SEEK_SET);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, 2, SEEK_SET);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, 2, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, 3, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, 0, SEEK_END);
	ch = fgetc(pf);
	printf("%d", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, -1, SEEK_END);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, -2, SEEK_END);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, -3, SEEK_END);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

ftell

头文件:stdio.h
函数功能:返回基于起始位置的偏移量
函数声明:

long int ftell ( FILE * stream );

失败返回EOF

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c\n", ch);
	fseek(pf, -3, SEEK_END);
	printf("%d", ftell(pf));
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
这个函数的妙用是计算文本的字节数

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	fseek(pf, 0, SEEK_END);
	printf("%d", ftell(pf));
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
当然,这里"\n"被拆分成"\r\n"了,要注意一下。

rewind

头文件:stdio.h
函数功能:将光标移动到文本开头
函数声明:

void rewind ( FILE * stream );
#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open text.txt(r)");
		return;
	}
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);
	fseek(pf, -3, SEEK_END);
	ch = fgetc(pf);
	printf("%c", ch);

	rewind(pf);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

文件读取的判定

经过上面的学习,我们知道有两种情况会导致函数读取失败(或者说,返回EOF或NULL):

  • 读到了文本末尾
  • 读取错误

如何辨别这两种情况呢?
这就需要函数feofferror了,之前我们说过,结构体FILE中存储着流的相关信息,对于文件流来说,有些信息就是用来描述读取失败的原因,当读取失败后,就可以通过feof和ferror来调用这些信息。如果是因为读到文本末尾而导致的读取失败,feof会返回非零,ferror会返回零;如果是因为读取错误而导致的读取失败,feof会返回零,ferror会返回非零。它们都只有一个参数,就是文件指针。

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("Open test.txt(w)");
		return;
	}
	fputs("hello world\n", pf);
	fputs("abcdefghi\n", pf);
	fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c", ch);
	}
	printf("\n");
	if (feof(pf))
	{
		printf("成功读取至文本末尾");
	}
	else
	{
		perror("fgetc(pf)读取错误");
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述
这里我们故意写个错的程序(错误原因:光标到末尾后没有用rewind移到开头):

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "w+");
	if (pf == NULL)
	{
		perror("Open test.txt(w+)");
		return;
	}
	fputs("hello world\n", pf);
	fputs("abcdefghi\n", pf);
	/*fclose(pf);
	pf = NULL;

	pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("Open test.txt(r)");
		return;
	}*/
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c", ch);
	}
	printf("\n");
	if (feof(pf))
	{
		printf("成功读取至文本末尾");
	}
	else
	{
		perror("fgetc(pf)读取错误");
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

在这里插入图片描述

文件缓冲区

内存块中的数据并不是直接与硬盘上的文件进行信息交流的,而是借助于内存中的一个特殊区域“文件缓冲区”进行交流的,如果是输出数据,就先把数据存在文件缓冲区,等文件缓冲区满了之后,或者刷新文件缓冲区再把数据输出到文件中;同样的,如果是输入数据,也会暂存在文件缓冲区,等刷新或者满了再传过去。
在这里插入图片描述
试想一下,你现在就是操作系统,如果没有缓冲区,每来一个字节我就要传过去,这是不是特别麻烦?而且这样的话,我几乎不能做其他事了?有了文件缓冲区,就可以先把一定大小的数据暂存下来,等够了一次性传过去,或者用fclose的形式刷新缓冲区(使用fclose关闭文件流就会刷新缓冲区,缓冲区被刷新之后,里面的内容也会被传输过去)以下代码可以检测缓冲区的存在:

随笔——文件操作——实验文件缓冲区的存在

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


  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值