C语言文件操作图文详解

C语言文件操作


文章目录

  • 1、什么是文件
    • ⭐1.1 程序文件
    • ⭐1.2 数据文件
    • ⭐1.3文件名
  • 2、文件指针
    • ⭐2.1 文件信息区
    • ⭐2.2 文件的打开和关闭
      • 2.2.1 相对路径和绝对路径介绍
      • 2.2.2 fopen函数
      • 2.2.3 fclose函数
  • 3、文件的顺序读写
    • ⭐3.1 用fputc函数写文件
    • ⭐3.2 用fgetc函数读文件
    • ⭐3.3 简单介绍输出流和输入流
    • ⭐3.4 fputs() 和 fgets()函数
      • 3.4.1 fputs()函数
      • 3.4.2 fgets()函数
    • ⭐3.5 学以致用:代码实现拷贝文件
    • ⭐3.6 fprintf() 和 fscanf()函数
      • 3.6.1 fprintf()函数
      • 3.6.2 fscanf()函数
    • ⭐3.7 fwrite() 和 fread()函数
      • 3.7.1 fwrite()函数
      • 3.7.2 fread()函数
    • ⭐3.8 对比一组函数
      • 3.8.1 sprintf() 和 sscanf()函数的介绍
      • 3.8.2 总结
  • 4、文件的随机读写
    • ⭐4.1 fseek()函数
    • ⭐4.2 ftell()函数
    • ⭐4.3 rewind
  • 5. 文本文件和二进制文件
  • 6、文件读取结束的判定
    • ⭐6.1 feof()函数的错误使用
    • ⭐6.2 ferror()函数介绍
    • ⭐6.3 正确使用feof() 和 ferror() 函数的例子
  • 7、文件缓存区
    • ⭐7.1 刷新缓存区


1、什么是文件

在编写程序时,程序中编写的数据都会存放在内存中,当程序退出的时候,这些数据都会被删除。当我们想要存储数据的时候,就需要使用文件,文件是存放在磁盘上的,使用文件可以将数据存放在电脑的硬盘上,就算关闭系统,再次打开的时候还是能够看到。如下图,这些都是文件
C语言文件操作图文详解_第1张图片

但是在程序设计中,文件又从功能的角度分为两类:程序文件、数据文件。

-----------------------✂---------------------------

⭐1.1 程序文件

只要是与程序有关的,我们都可以归类为程序文件。程序文件包括:1.源程序文件(后缀为.c),2.目标文件(windows系统下后缀为.obj),3.可执行程序(windows系统下后缀为.exe)

-----------------------✂---------------------------

⭐1.2 数据文件

数据文件的内容可以是程序也可以不是程序,程序运行时读写的数据被称为数据文件,比如运行某个程序需要读某个文件的内容,或者通过程序写入内容到文件中,则这个文件就可以被称为数据文件,如图
(我们可以通过test1.c这个程序文件来读写data.txt的文件,也可以通过test1.c来读写test2.c这个程序文件,因此test2.c既是程序文件,也是数据文件)
C语言文件操作图文详解_第2张图片
先前代码的输入和输出都是以终端为对象的,也就是从键盘中输入数据,再将其打印在显示屏上,但其实有的时候我们需要将数据存放到磁盘中去,到需要的时候再从磁盘中拿出来使用

在本篇博客中我们将讨论的是如何通过我们的代码来操作数据文件

-----------------------✂---------------------------

⭐1.3文件名

文件名包含3个部分:文件路径+文件主干+文件后缀
例如:我在C盘的Code文件夹上创建一个text.txt的文件,那么它的文件名为

C:\Code\text.txt (C:\Code为文件路径,text为文件主干,txt为文件后缀)


2、文件指针

只要我们使用一个文件,该文件都会在内存中开辟一个文件信息区,用于存放文件的相关信息(如文件名字、文件状态、文件位置等等)。这些信息是保存在一个结构体变量中的,该结构体类型是由系统声明的,取名为FILE

-----------------------✂---------------------------

⭐2.1 文件信息区

这里博主用VS2013的编译环境来展示头文件中是如何声明文件指针的
包含头文件后,输入FILE转到定义
C语言文件操作图文详解_第3张图片

转到定义后我们发现FILE其实就是由结构体 struct_iobuf 定义而来的

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

对于结构体的成员,我们无需去了解具体是用来做什么的,我们只需要知道的是:只要我们使用一个文件,该文件都会在内存中开辟一个文件信息区,用于存放文件的相关信息。

-----------------------✂---------------------------

⭐2.2 文件的打开和关闭

在进行文件的读写前应该先打开文件,在使用结束后关闭文件

打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也就是建立了指针和文件的关系。

//打开文件
FILE *fopen( const char *filename, const char *mode );
//关闭文件
int fclose( FILE *stream );
-----------------------✂---------------------------

2.2.1 相对路径和绝对路径介绍

相对路径也就是从文件本身出发,相对于该文件的路径
假设我们此时正在编辑一个.c文件,那么

  • .c文件与要用到的数据文件位于同一个文件夹,即同级目录直接写文件主干+文件后缀
  • 若数据文件在.c文件的下一级目录:文件夹名称/文件主干+文件后缀
  • 若数据文件在.c文件的上一级目录:../文件主干+文件后缀

其他情况下为了防止混淆,我们使用fopen函数时就直接写它的绝对路径

绝对路径是指目录下的绝对位置
用鼠标右击文件,点击属性,就能看到它的路径
C语言文件操作图文详解_第4张图片
将路径复制下来,在写上文件主干+文件后缀就可以了:C:\Code\code_class104\class104_2022code\test_04_25\test_04_25\text.txt

-----------------------✂---------------------------

2.2.2 fopen函数

打开文件

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

参数:

  • const char* filename filename其实是文件名
  • mode是打开文件的方式,下面介绍几个常用的:
输入:文件->内存,也就是将文件内的数据存到内存中
输出:内存->文件,也就是将内存中的数据存到文件中

举两个简单的例子带大家了解一下 输入和输出的含义

输出 printf:内存中的数据 → 屏幕
输入 scanf:来自键盘的数据 → 内存

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

函数的返回值:

函数调用成功返回的是该文件的文件信息区的指针,调用失败则返回NULL指针,因此我们调用函数时可以用一个语句判断是否打开文件成功

fopen函数的使用:

int main()
{
	//由于fopen返回一个FILE*的指针,因此需要用一个pf来接收
	FILE* pf = fopen("C:\\Code\\text.txt", "w");
	//若打开文件失败,返回NULL
	if (pf == NULL)
	{
		printf("打开文件失败\n");
		return 0;
	}
	return 0;
}

这里前面讲到的相对路径和绝对路径的概念就用上了,本次调用fopen函数我们的文件名是直接写入文件主干+文件后缀因此,在.c文件所在的文件夹中本来并没有text.txt文件,执行完代码后,我们便可以看到新建了text.txt文件
C语言文件操作图文详解_第5张图片
若要对其他路径的文件使用fopen函数,比如在C盘中的Code文件夹中,则以绝对路径的形式进行书写(注意!!路径中的 ’ \ ’ 要记得用转义字符进行转义):

int main()
{
	//由于fopen返回一个FILE*的指针,因此需要用一个pf来接收
	FILE* pf = fopen("C:\\Code\\text.txt", "w");
	return 0;
}

C语言文件操作图文详解_第6张图片
当fopen打开text.txt文件的同时,就会创建一个文件信息区,并对其进行填充,文件信息区就是一个FILE结构体变量,而fopen返回的就是文件信息区的地址
C语言文件操作图文详解_第7张图片

-----------------------✂---------------------------

2.2.3 fclose函数

关闭文件

int fclose( FILE *stream );

参数:

实际上就是调用fopen函数时返回的文件信息区的指针,也就是pf

返回值:

关闭成功返回0,失败返回EOF

fclose函数的使用:

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Code\\text.txt", "w");
	if (pf == NULL)
	{
		printf("打开文件失败\n");
		return 0;
	}
	//写文件

	//关闭文件
	fclose(pf);
	//pf作为指针变量,在关闭文件后就没用了,可以置为空指针
	pf = NULL;

	return 0;
}

3、文件的顺序读写

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

⭐3.1 用fputc函数写文件

int fputc( int c, FILE *stream );

参数:

  • int c:写入文件的字符,用int存储也就是字符的ASCII码值
  • FILE *stream:待写入文件的文件指针

函数的返回值:

  • 调用成功:返回写入字符的ASCII码值
  • 调用失败:返回EOF

fputc函数的使用:

int main()
{
	//打开文件for write
	FILE* pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}
	//写入文件
	fputc('a', pf);
	fputc('b', pf);
	fputc('c', pf);

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

	return 0;
}

找到文件并打开,我们便发现已经成功写入文件了,而这三个字符一定是按顺序写入文件的,这就是所谓的顺序读写
C语言文件操作图文详解_第8张图片

-----------------------✂---------------------------

⭐3.2 用fgetc函数读文件

int fgetc( FILE *stream );

参数:

FILE *stream:待读文件的文件指针

返回值:

读取成功返回读取字符的ASCII码值,失败或读取结束则返回EOF

fgetc函数的使用:
先前在text.txt文件中写入了abc三个字符,这里我们用ch来存储从文件中读的字符,并打印出来

int main()
{
	//打开文件for read
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}
	//读文件
	int ch = 0;
	for (int i = 0; i < 3; i++)
	{
		ch = fgetc(pf);
		printf("%c", ch);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}
C语言文件操作图文详解_第9张图片

fgetc函数是如何返回其对应的字符呢?从代码中得知我们调用了三次fgetc函数,text.txt的文件指针其实是这样变化的

  1. 一开始文件指针指向第一个字符
  2. 没调用完一次text.txt文件指针就向后移动一个字符
  3. 直到第三次调结束,文件指针就再向后移动一个字符,此时文件指针不再指向字符
  4. 如果再次调用,则会返回EOF
C语言文件操作图文详解_第10张图片

了解此过程后,要想用一个循环实现读取文件的所有字符可以写下这样的代码:

//读文件
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c", ch);
	}

这里有个小细节,我们为什么不定义一个char ch来存放字符的ASCII码值呢?其实是因为EOF转到定义其实就是整数-1,而char字符只有一个字节,是无法装下-1这样的整型的,因此我们用int来接收。

-----------------------✂---------------------------

⭐3.3 简单介绍输出流和输入流

以fputc的例子为例:表格中提到fputc()函数适用于所有输出流,而输出流指的是什么?这里博主还将简单介绍一下输出流和输入流

  1. 为什么用fputc需要用fopen打开文件后,才能够将数据写入文件?
  2. 而平时用到的printf却不需要没有这种操作呢?

这是因为我们任何一个C语言程序,只要运行起来,就默认打开三个流

  • stdin   → 标准输入流
  • stdout → 标准输出流
  • stderr  → 标准错误流

C 语言中的 I/O (输入/输出) 通常使用 printf() 和 scanf() 两个函数。
当调用printf()函数的时候,会默认将内存中的数据输出到标准输出流stdout(屏幕)上,
而调用scanf()函数的时候,会默认将标准输入流stdin(键盘)的数据,输入到内存中。

而①stdin ②stdout ③stderr 这三个流都是FILE*类型的文件指针,要想将数据打印到屏幕上,也就可以写下代码:fputc('a', stdout)

//写个循环观察结果
for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, stdout);
	}
C语言文件操作图文详解_第11张图片

⭐3.4 fputs() 和 fgets()函数

文本行输入函数和文本行输出函数

3.4.1 fputs()函数

作用:输出单个字符到输出流

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

参数:

const char *string:要输出的字符串
FILE *stream:待读文件的文件指针

返回值:

读取成功返回一个非负数,失败则返回EOF

fputs函数的使用:

int main()
{
	//打开文件for write
	FILE* pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}
	//写文件
	fputs("hello ", pf);
	fputs("world", pf);
	
	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

运行程序我们可以发现 “world” 直接跟在 “hello ” 的后面,并没有换行,要想换行,得手动改成“hello \n”
C语言文件操作图文详解_第12张图片

-----------------------✂---------------------------

3.4.2 fgets()函数

作用:输入单个字符到输入流

char *fgets( char *string, int n, FILE *stream );

参数:

char *string:读取数据的存储位置
int n:最大的读取字符个数(真正读取的是n-1个字符)
FILE *stream:待读文件的文件指针

返回值:

读取成功返回存放数据的数组的指针,读取错误或者读取结束则返回NULL

fgets函数的使用:

  1. 我们现在记事本中写下以下内容
C语言文件操作图文详解_第13张图片
  1. 调用fgets()函数三次
int main()
{
	//打开文件for read
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}

	//每次读一行
	char a[100];
	//第一次
	fgets(a, 100, pf);
	printf("%s", a);
	//第二次
	fgets(a, 100, pf);
	printf("%s", a);
	//第三次
	fgets(a, 100, pf);
	printf("%s", a);
	
	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}
  1. 我们可以发现第三次打印的内容和第二次一模一样,由此我们可得知调用第三次函数后,数组a中的内容还是第二次所读到的字符串
C语言文件操作图文详解_第14张图片
  1. 尝试用下面这种写法编写代码
char a[100];
	while (fgets(a, 100, pf) != NULL)
	{
		printf("%s", a);
	}
  1. 我们可以得出结论:第三次调用的时候不会改变数组中的值,但是返回了一个NULL指针
C语言文件操作图文详解_第15张图片 6. 如果一行数据没有读完,再次调用fgets()函数不会访问到下一行
//每次读一行
	char a[100];
	//第一次
	fgets(a, 3, pf);
	printf("%s", a);
	//第二次
	fgets(a, 3, pf);
	printf("%s", a);
C语言文件操作图文详解_第16张图片

结果发现只有四个字符,其实第一次读了 “he” ,第二次读了 “ll” 。
这也印证了前面所说的:int n:最大的读取字符个数(真正读取的是n-1个字符)

-----------------------✂---------------------------

⭐3.5 学以致用:代码实现拷贝文件

首先我们在text.txt中添加一段话
目标:用代码拷贝一份相同的文件,命名为 → text2.txt
C语言文件操作图文详解_第17张图片

int main()
{
	//1.打开text.txt文件for read
	FILE* pf1 = fopen("text.txt", "r");
	if (pf1 == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}

	//2.打开text2.txt文件for write
	FILE* pf2 = fopen("text2.txt", "w");
	if(pf2 == NULL)
	{
		printf("%s\n", strerror(errno));
		//若程序运行到,则表示text.txt打开成功,在return 0之前要先关闭text.txt
		fclose(pf1);
		pf1 = NULL:
		return 0;
	}

	//3.拷贝文件
	int ch = 0;
	while ((ch = getc(pf1)) != EOF)
	{
		putc(ch, pf2);
	}

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

打开text2.txt文件,查看结果
C语言文件操作图文详解_第18张图片

-----------------------✂---------------------------

⭐3.6 fprintf() 和 fscanf()函数

格式化输入函数和格式化输出函数

3.6.1 fprintf()函数

作用:输出文本行数据到输出流

int fprintf( FILE *stream, const char *format [, argument ]...);
int printf( const char *format [, argument]... );

其实fprintf()函数很简单,只要会用printf(),同样也会用fprintf():

  • 相比于printf()函数, fprintf()就只多了第一个参数,也就是输出流的文件指针
  • 返回值都是打印的字符个数

如果我们想要输出格式化的数据到文件中,例如结构体,便需要使用fprintf()函数

struct Stu
{
	char name[20];
	int age;
	double score;
};
int main()
{
	struct Stu s1 = { "张三", 20, 88.8 };
	FILE* pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}
	fprintf(pf, "%s %d %lf", s1.name, s1.age, s1.score);
	return 0;
}
C语言文件操作图文详解_第19张图片
-----------------------✂---------------------------

3.6.2 fscanf()函数

作用:输入文本行数据到输入流

int fscanf( FILE *stream, const char *format [, argument ]... );
int scanf( const char *format [,argument]... );

同理:fscanf和scanf的差别同样也是多了第一个参数(输入流的文件指针)。

此时text.txt中的数据上保存着上一次fprintf输入的数据
C语言文件操作图文详解_第20张图片
如果我们想要将文件的格式化数据输入到内存中,便需要使用fprintf()函数

struct Stu
{
	char name[20];
	int age;
	double score;
};
int main()
{
	//定义一个结构体变量s1,初始化为0
	struct Stu s1 = { 0 };
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}

	//从文件中读取数据到内存中的s1结构体变量
	fscanf(pf, "%s %d %lf", s1.name, &(s1.age), &(s1.score));

	//打印内存中的s1结构体变量的数据
	printf("%s %d %lf", s1.name, s1.age, s1.score);

	fclose(pf);
	return 0;
}

结果可知,输入成功
C语言文件操作图文详解_第21张图片

-----------------------✂---------------------------

⭐3.7 fwrite() 和 fread()函数

二进制输入函数和二进制输出函数(输出流和输入流都只能是文件)

3.7.1 fwrite()函数

作用:以二进制的方式写入输出流

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

参数:

  • const void *buffer:待写入数据的指针
  • size_t size:数据中每个元素的字节大小
  • size_t count:元素个数
  • FILE *stream:文件的文件指针

返回值:

返回实际写入的完整元素个数,若发生错误,该值可能小于count

简述:从buffer里面取count个大小为size字节的数据存放到stream中去

fwrite()函数的使用:

struct Stu
{
	char name[20];
	int age;
	double score;
};
int main()
{
	//初始化一个结构体数组
	struct Stu s[2] = { {"张三", 20, 88.8},{"李四", 16, 66.6} };

	//为了以二进制写入而打开文件
	FILE* pf = fopen("text.txt", "wb");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}

	//从文件中读取数据到内存中的s1结构体变量
	fwrite(s, sizeof(struct Stu), 2, pf);

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

打开记事本我们发现是乱码,这是正常的,因为我们是以二进制的形式写入的,而txt文件是以文本的信息解析内容
C语言文件操作图文详解_第22张图片
用Visual Studio以二进制的形式打开,便可以看到其以二进制写入的数据

-----------------------✂---------------------------

3.7.2 fread()函数

作用:以二进制的方式读

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

参数:

  • void *buffer:读取数据存储的位置
  • size_t size:数据中每个元素的字节大小
  • size_t count:元素个数
  • FILE *stream:文件的文件指针

返回值

返回实际读取到的元素个数

简述:从stream中读取count个大小为size字节的数据储存到buffer中

fread()函数的使用:

struct Stu
{
	char name[20];
	int age;
	double score;
};
int main()
{
	//定义一个结构体变量数组,初始化为0
	struct Stu s[2] = { 0 };

	//为了以二进制读取而打开文件
	FILE* pf = fopen("text.txt", "rb");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}

	//从文件中读取数据到内存中的s1结构体数组
	fread(s, sizeof(struct Stu), 2, pf);
	printf("%s %d %lf\n", s[0].name, s[0].age, s[0].score);
	printf("%s %d %lf\n", s[1].name, s[1].age, s[1].score);

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

虽然二进制的数据我们看不懂,但是fread()可以看得懂,因此在屏幕上便打印出二进制数据的相应信息
C语言文件操作图文详解_第23张图片

-----------------------✂---------------------------

⭐3.8 对比一组函数

printf / fprintf  / sprintf
scanf / fscanf / sscanf

3.8.1 sprintf() 和 sscanf()函数的介绍

//对比sprintf和fprintf
int sprintf( char *buffer, const char *format [, argument] ... 
int fprintf( FILE *stream, const char *format [, argument ]...);

对于fprintf博主之前讲到过是将格式化的数据输出到输出流stream上;
而sprintf则是把格式化的数据直接转化成字符串,存放到buffer上。
C语言文件操作图文详解_第24张图片

struct Stu
{
	char name[20];
	int age;
	double score;
};
int main()
{
	//定义一个结构体变量数组,初始化为0
	struct Stu s = { "张三", 20, 90.5 };

	char buf[100] = { 0 };
	sprintf(buf,"%s %d %lf", s.name, s.age, s.score);
	printf("%s", buf);
	
	return 0;
}
C语言文件操作图文详解_第25张图片
-----------------------✂---------------------------
//对比sscanf和fscanf
int sscanf( const char *buffer, const char *format [, argument ] ... );
int fscanf( FILE *stream, const char *format [, argument ]... );

对于fscanf博主之前讲到过是从输入流上读取格式化的数据,存放到内存中指定的位置;
而sprintf则是提取字符串中的数据,转化成格式化的数据存放到内存中的指定位置。
C语言文件操作图文详解_第26张图片

struct Stu
{
	char name[20];
	int age;
	double score;
};
int main()
{
	//定义一个结构体变量数组,初始化为0
	struct Stu s = { 0 };

	char buf[] = "张三 20 90.5";
	sscanf(buf, "%s %d %lf", s.name, &(s.age), &(s.score));
	printf("%s %d %lf", s.name, s.age, s.score);

	return 0;
}
C语言文件操作图文详解_第27张图片
-----------------------✂---------------------------

3.8.2 总结

  • scanf :从标准输入流stdin(即输入设备,一般指键盘)上读取格式化的数据,存放到内存中指定的位置
  • printf :将内存中的数据按照格式化的方式,输出到标准输出流stdout(即输出设备,一般指屏幕)

  • fscanf :从标准输入流stdin/指定的文件流 上读取格式化的数据,存放到内存中指定的位置
  • fprintf :将内存中的数据按照格式化的方式,输出到标准输出流stdout/指定的文件流

  • sscanf: 可以将字符串提取(转化)出格式化数据
  • sprintf:把一个格式化数据转换成字符串

4、文件的随机读写

前面所谈到的读和写都是顺序读写,在解释fgetc()函数的时候有提到文件的指针会随着函数调用不断向后偏移:也就是从文件的开头,按顺序一个不差地进行读写。
但是如果我们想要跳过一个或者多个字符进行读和写应该怎么做呢?这里就涉及到我们的fseek()函数
,也就是文件的随机读写。

⭐4.1 fseek()函数

作用:根据文件指针的位置和偏移量来定位文件指针

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

参数

  • FILE *stream → 文件指针
  • long offset → 偏移量
  • int origin → 初始位置(有以下参考值)
    – SEEK_CUR(目前指针的位置)
    – SEEK_END(文件末尾)
    – SEEK_SET (文件开头)

返回值

调用成功返回0,否则返回非0的数

函数的使用:

在text.txt文件中添加以下字符
C语言文件操作图文详解_第28张图片
一开始文件指针指向第一个字符’a’用fgetc调用两次函数后,文件指针就应该指向’c’

int main()
{
	//打开文件
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}

	//读文件
	int ch = 0;
	//文件指针向后偏移一个字符
	ch = fgetc(pf);
	printf("%c", ch);
	//文件指针向后偏移一个字符
	ch = fgetc(pf);
	printf("%c", ch);

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

要想让文件指针指向g我们应该怎么操作呢?
fseek(pf, 4, SEEK_CUR);
C语言文件操作图文详解_第29张图片

int main()
{
	//打开文件
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}

	//读文件
	int ch = 0;
	ch = fgetc(pf);
	printf("%c", ch);

	ch = fgetc(pf);
	printf("%c", ch);
	//设置偏移量!
	fseek(pf, 4, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c", ch);
	//关闭文件
	fclose(pf);
	pf = NULL;
}

这时候再读取便可以读到我们想要的字符’g’了
C语言文件操作图文详解_第30张图片
三种不同位置定位到字符’g’的三种写法
C语言文件操作图文详解_第31张图片
SEEK_SET(向右偏移6个字符):fseek(pf, 6, SEEK_SET);
SEEK_CUR(向右便宜4个字符):fseek(pf, 4, SEEK_CUR);
SEEK_END(向左偏移1个字符):fseek(pf, -1, SEEK_END);

-----------------------✂---------------------------

⭐4.2 ftell()函数

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

long ftell( FILE *stream );
int main()
{
	//打开文件
	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}

	int ch = 0;
	//调用两次fgetc,此时偏移量应该为2
	ch = fgetc(pf);
	ch = fgetc(pf);

	//测试ftell
	long offset = ftell(pf);
	printf("偏移量为:%ld\n", offset);

	//关闭文件
	fclose(pf);
	pf = NULL;
}
C语言文件操作图文详解_第32张图片
-----------------------✂---------------------------

⭐4.3 rewind

作用:让文件指针回到起始位置

void rewind( FILE *stream );

例子
C语言文件操作图文详解_第33张图片

O.S:学习了以上的知识,想必大家都可以对文件进行许多常用的操作了。除了这些常用的文件操作函数之外,还有很多文件操作的函数,为大家推荐一个查阅相关函数的网址,大家可以自行查阅学习。
戳这里《C/C++参考文档》


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

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

  1. 数据在内存中以二进制的形式存储,如果不加转换直接输出到文件上,就是二进制文件
  2. 如果在输出到文件之前将内存中的数据转换为以ASCII字符的形式存储的文件就是文本文件

这里为大家举个例子,将一个数字8000存放入文件中

C语言文件操作图文详解_第34张图片

写一段代码将8000以二进制的形式写进文件进行测试

int main()
{

	FILE* pf = fopen("text.txt", "wb");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 0;
	}

	int n = 8000;
	//1个元素, 4个字节
	fwrite(&n, 4, 1, pf);

	fclose(pf);
	return 0;
}

将text.txt文件拉进Visual Studio中以二进制编辑器打开,我们就可以发现8000这个数字确实就是以二进制形式写入文件的
C语言文件操作图文详解_第35张图片


6、文件读取结束的判定

⭐6.1 feof()函数的错误使用

int feof( FILE *stream );

作用:feof()函数用来检测当前文件流上的文件结束标识,判断是否读到了文件结尾

对于feof()函数,博主参考了很多文献,最后总结了feof使用的误区:

feof()函数并非"检查文件是否到末尾",而是检查"end-of-file标记","end-of-file标记"和"文件读写标记"虽然都是前面提到的文件信息区,也就是FILE类型的结构体的内容,但两者却完全不同!
比如说当fgetc发现输入流中不存在数据时,它会返回EOF,并且会设置FILE对象的"end-of-file"标记,而feof()的原理正是用来检测该标记。

对此我们可以新建一个空文件进行测试:

int main(void)
{
    //打开文件
    FILE* pf = fopen("text.txt", "r");
    if (!pf)
    {
        printf("%s", strerror(errno));
        return 0;
    }
    //未读取
    if (feof(pf))
        printf("文件为空\n");
    else
        printf("文件不为空\n");

    //读取
    int ch = fgetc(pf);
    if (feof(pf))
        printf("文件为空\n");
    else
        printf("文件不为空\n");

    return 0;
}
C语言文件操作图文详解_第36张图片

得出结论:

该函数应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

三个读取函数的返回值

  1. fgetc:读取成功返回读取字符的ASCII码值,失败或读取结束则返回EOF
  2. fgets:读取成功返回存放数据的数组的指针,读取错误或者读取结束则返回NULL
  3. fread:返回实际读取到的元素个数

文件读取结束或者失败的判断

  • fgetc会在读取到文件末尾或者错误的时候返回EOF
  • fgets会在读取到文件末尾或者错误的时候返回NULL
  • fread则需要判定返回的值是否实际要读取的个数小:如果返回值小于实际要读取的值,那么一定读到文件结尾或者读取失败。
-----------------------✂---------------------------

⭐6.2 ferror()函数介绍

作用:用于判断读取文件是否有error

返回值:

没有错误,则返回0;有错误,则返回非零值

-----------------------✂---------------------------

⭐6.3 正确使用feof() 和 ferror() 函数的例子

文本文件

int main(void)
{
    //打开文件
    FILE* fp = fopen("text.txt", "r");
    if (!fp)
    {
        printf("%s", strerror(errno));
        return 0;
    }
    
    //读文件
    int ch;
    //读取到EOF才停下
    while((ch = fgetc(fp)) != EOF)
    {
        printf("%c ", ch);
    }
    printf("\n");

    //判断时遇到错误结束,还是读到文件末尾结束
    if (ferror(fp))
    {
        printf("I/O error when readingT_T\n");
    }
    else if (feof(fp))
    {
        printf("end-of-file reached successfully!\n");
    }
    return 0;
}

C语言文件操作图文详解_第37张图片

非文本文件

enum
{
    //元素大小
    SIZE = 4,
    //元素个数
    COUNT = 5
};
int main(void)
{
    int arr1[COUNT] = { 1,2,3,4,5 };
    //二进制写入文件
    FILE* pf1 = fopen("text.txt", "wb");
    if (!pf1)
    {
        printf("%s", strerror(errno));
        return 0;
    }

    fwrite(arr1, SIZE, COUNT, pf1);

    fclose(pf1);
    pf1 = NULL;
    //二进制读文件
    int arr2[COUNT];
    FILE* pf2 = fopen("text.txt", "rb");
    if (!pf2)
    {
        printf("%s", strerror(errno));
        return 0;
    }
    //成功读取的元素个数
    int ret = fread(arr2, SIZE, COUNT, pf2);
    if (ret == COUNT)
    {
        printf("读取成功:>\n");
    }
    else
    {
        if (feof(pf2))
        {
            printf("要读取元素个数大于已有元素个数..\n");
        }
        else if (ferror(pf2))
        {
            printf("读取时发生错误..\n");
        }
    }
    return 0;
}


7、文件缓存区

内存运行的速度是远远大于内存和硬盘传输数据的速度的,为了提高效率:ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。

从内存向磁盘输出数据:会先送到内存中的缓冲区,装满缓冲区或者刷新缓冲区后才一起送到磁盘上。
从磁盘向计算机读入数据:从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),充满或者刷新缓冲区后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。

缓冲区的大小根据C编译系统决定的。

C语言文件操作图文详解_第38张图片
缓存区默认是在充满后才将数据送到磁盘或者程序数据区上,比如用printf()输出时是先输出到缓冲区,直到缓存区充满,再从缓冲区送到屏幕上。 C语言文件操作图文详解_第39张图片

不过这种情形需要在Linux系统下才能看到,写下以下代码
(O.S:在Linux系统下sleep(1)指的是休眠1秒)
C语言文件操作图文详解_第40张图片

编译运行后我们发现:并没有成功输出到屏幕上,原因是此时程序中一秒往缓存区中写一个hehe,但是在没充满缓存区之前,是不会将缓存区的内容打印大屏幕上的。如果打印出来你将看到一屏幕hehe,也就是将缓存区中的信息一起打印到屏幕上来

修改一下代码
C语言文件操作图文详解_第41张图片
此时,再运行程序,便可以看到一秒再屏幕上打印了一个hehe,这时因为’\n’进入缓存区后会实现刷新缓存区的操作

⭐7.1 刷新缓存区

有以下几种方式可以刷新缓存区

  • ‘\n’,'\r’进入缓存区时
  • 运行scanf()函数的时候
  • 使用fflush() 强制刷新
  • fclose关闭文件
  • 程序结束的时候
//用下面的代码为大家举一个例子,让大家感受到缓存区的存在,可自行测试
#include 
#include 
//VS2013 WIN10环境测试
int main()
{
 FILE*pf = fopen("test.txt", "w");
 fputs("abcdef", pf);//先将代码放在输出缓冲区
 printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
 Sleep(10000);
 printf("刷新缓冲区\n");
 fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
 //注:fflush 在高版本的VS上不能使用了
 printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
 Sleep(10000);
 fclose(pf);
 //注:fclose在关闭文件的时候,也会刷新缓冲区
 pf = NULL;
 return 0;
}

总结:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。

你可能感兴趣的:(C语言进阶篇,c语言)