目录

什么是文件

文件分类

  程序文件

  数据文件

文件的使用

文件指针

文件指针的使用

文件的打开和关闭

文件的使用方式

文件的顺序读写

1.写入一个字符

2.读取一个字符

3.连续每次读取一个字符

4.覆盖并写入一行数据

5.读取指定长度的数据

6.将结构体信息写入文件中

7.读取文件信息到结构体变量中

8.二进制写入文件

9.读取二进制文件信息

10.sscanf()函数、sprintf()函数

文件的随机读写

fseek()函数

ftell函数()

 rewind()函数

二进制文件和文本文件

文件读取结束判定

feof()函数

文本文件的判断

二进制文件的判断



📌————本章重点————📌

🔗文件指针

🔗文件的顺序读写

🔗文件的随机读写

🔗文件读取结束判定


 ✨————————————✨


什么是文件

        与普通文件载体不同,文件是以硬盘为载体存储计算机上的信息集合,文件可以是文本文档、图片、程序等等。文件通常具有点+三个字母的文件扩展名,用于指示文件类型(例如,图片文件常常以KPEG格式保存并且文件扩展名为.jpg)。

        将数据放入文件中,相比代码程序中堆栈上的数据,其优点在于可以随时做到需要时添加、舍弃时删除,数据可以持久化。

文件分类:

        文件一般讲两种:程序文件数据文件

  程序文件:

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

  数据文件:

        包括程序运行时所读写的数据。本篇所涉及的就是数据文件。


文件的使用

文件的操作一般分三步:1.打开文件;2.读/写;3.关闭文件;

文件指针

        想要对文件进行操作,“文件指针”就是一个关键桥梁(亦名:文件类型指针);                

        底层原理:每个被使用的文件,都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如:文件名、文件状态、文件位置等),这些信息被保存到一个结构体中,系统为其声明为FILE,每当打开一个文件的时候,系统就会根据情况自动创建一个FILE结构的变量,并且通过FILE*的指针来维护这个结构。

文件指针的使用:

    FILE* pf;

定义一个文件指针变量pf,它可以指向某个文件的文件信息区,通过其即可访问到该文件。


文件的打开和关闭

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

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

文件的使用方式:

按常用序:

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

文件的顺序读写

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

以上结合起来实例:

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

int main()
{
	FILE* pf= fopen("test.txt", "w+");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	//输入一个字符
	fputc('a', pf);
	//用完关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

如图示:在源文件所在目录下,原本没有test.txt文件,是w+创建了这个新的文件,并写入一个字符a 

1.写入一个字符:

//写文件    
    fputc('a', pf);

2.读取一个字符:

	//读取一个字符
	int ch = fgetc(pf);
	if (ch != EOF)
	{
		printf("%c\n", ch);
	}

3.连续每次读取一个字符:

	//文件中有abcdefg

	int ch = fgetc(pf);
	printf("%c\n", ch);	//a
	ch = fgetc(pf);
	printf("%c\n", ch);	//b
	ch = fgetc(pf);
	printf("%c\n", ch);	//c
	ch = fgetc(pf);
	printf("%c\n", ch);	//d

4.覆盖并写入一行数据:

	fputs("hello world", pf);

注意:这里fputs函数虽然是整行写入,但会覆盖掉原始数据、

5.读取指定长度的数据:

    //定一一个数组
	char arr[10] = { 0 };
	fgets(arr, 5, pf);    //将所读取的数据放入arr中
	printf("%s\n", arr);

6.将结构体信息写入文件中:

这里的结构体信息就是格式化的,那么就需要fprintf()函数了

#include<stdio.h>

typedef struct S
{
	char name[10];
	int age;

}Peo;
int main()
{
	FILE* pf = fopen("test.txt", "w");

	if (pf != NULL)
	{
		Peo p = { "zhangsan", 18 };
		fprintf(pf, "%s %d\n", p.name, p.age);

		fclose(pf);
		pf = NULL;
	}

	return 0;
}

7.读取文件信息到结构体变量中:

#include<stdio.h>

typedef struct S
{
	char name[10];
	int age;

}Peo;
int main()
{
	FILE* pf = fopen("test.txt", "r");

	if (pf != NULL)
	{
		Peo p = { 0 };

		fscanf(pf, "%s %d", p.name, &p.age);

		printf("%s %d", p.name, p.age);

		fclose(pf);
		pf = NULL;
	}

	return 0;
}

8.二进制写入文件:

  • size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
#include<stdio.h>
#include<string.h>
#include<errno.h>

typedef struct S
{
	char name[10];
	int age;

}Peo;
int main()
{
	FILE* pf = fopen("test.txt", "wb+");

	if (pf != NULL)
	{
		Peo p = { "lisi", 19};

		fwrite(&p, sizeof(Peo), 1, pf);

		fclose(pf);
		pf = NULL;
	}

	return 0;
}

9.读取二进制文件信息:

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

#include<stdio.h>

typedef struct S
{
	char name[10];
	int age;

}Peo;
int main()
{
	FILE* pf = fopen("test.txt", "rb+");

	if (pf != NULL)
	{
		Peo p = { 0 };

		fread(&p, sizeof(Peo), 1, pf);

		printf("%s %d\n", p.name, p.age);

		fclose(pf);
		pf = NULL;
	}

	return 0;
}

10.sscanf()函数、sprintf()函数:

这两个函数虽然和文件操作关系不大,但是容易与文件操作函数混淆;

  • sscanf()函数:
  • int sscanf( const char *buffer, const char *format [, argument ] ... );
  • 将一个字符串转化为格式化数据;
#include<stdio.h>

typedef struct S
{
	char name[10];
	int age;

}Peo;
int main()
{
	//定义一个字符串
	char buffer[] = { "zhansan 19" };
	//定义一个结构但不赋值
	Peo p = { 0 };

	sscanf(buffer, "%s %d", p.name, &p.age);

	return 0;
}

  • sprintf()函数:
  • int sprintf( char *buffer, const char *format [, argument] ... );

  • 将一个格式化数据转化为字符串;

#include<stdio.h>

typedef struct S
{
	char name[10];
	int age;

}Peo;
int main()
{	
	//定义一个结构
	Peo p = { "zhangsan",19};
	//定义一个字符串
	char buffer[50] = { 0 };

	sprintf(buffer, "%s %d\n", p.name, p.age);

	return 0;
}


文件的随机读写

所谓的随机读写,其实就是指定我们想要读写的位置。

fseek()函数:

  • 该函数可以从定位位置的偏移量处开始读写;
  • int fseek( FILE *stream, long offset, int origin );
                         文件流          偏移量    起始位置 
  • 返回值:
  1. 如果成功,fseek返回0;
  2. 否则,它返回一个非零值;
  3. 在无法查找的设备上,返回值未定义;        

 三种定位指针:

 使用实例:

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

int main()
{
	FILE* pf = fopen("test.txt", "r");

	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	//开始多次读取
	//定位指针:比如要读取从头开始向后偏移 2 个单位的一个字符
	fseek(pf, 2, SEEK_SET);
	int ch = fgetc(pf);
	printf("%c\n", ch);

	//第二次读取:要拿到当前文件指针所处位置向后偏移5个单位的字符
	fseek(pf, 5, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c\n", ch);

	//第三次读取:要拿到文件流末尾向前偏移8个单位的一个字符
	fseek(pf, -8, SEEK_END);
	ch = fgetc(pf);
	printf("%c\n", ch);

	fclose(pf);
	pf = NULL;

	return 0;
}

特别说明:

        在每使用完一次fseek函数后,文件指针会自动向后移动一位:

ftell函数():

  • 该函数可以返回文件指针相对于起始位置的偏移量;
  • long int ftell ( FILE * stream );

使用实例:

        我们直接在上一段代码的基础上加上ftell()函数即可直观得到每次文件指针所处的位置:

 rewind()函数:

  • 让文件指针回到文件初始位置;
  • void rewind ( FILE * stream );

使用实例: 


二进制文件和文本文件

我们知道数据在内存中是以二进制形式存储的,对于文件而言:如果不加转换直接输出到外存就是二进制文件;如果要在外存上以ASCII码形式存储,就需要提前转换最后以ASCII码值形式存储的文件就是文本文件。

对于字符,一律使用ASCII码形式存储,但对于数值型数据,即可以使用ASCII码存储也可以使用二进制形式存储

举例:

数字10000的两种存储形式:

二进制文件:

文本文件:

首先将10000分成'1','0','0','0','0', 这五个字符,用每个字符对应的ASCII码值进行转换:

 

 由此可见:对于10000在数,如果以二进制形式存储占用4个字节,如果以ASCII码存储占用5个字节。试想:那对于数字1呢?

显而易见,二进制文件存储和文本文件存储对不同范围的数字可以做到节省空间。

对二进制文件深入理解:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test.txt", "wb");
	int a = 10000;

	if (pf != NULL)
	{
		fwrite(&a, 4, 1, pf);

		fclose(pf);
		pf = NULL;
	}

	return 0;
}

对于上面这段代码,我们知道是将数值10000放入了test.txt文件中,但我们无法直接看到它在文件中的真实值,于是使用vs的二进制编辑器即可查看:



文件读取结束判定

feof()函数:

该函数被许多人错误用来判断文件是否读取结束,其实它的作用是判断文件读取结束的原因;

文件读取结束有两种情况:1.读取过程中出现异常; 2.读取到文件末尾;

要找出文件读取是哪个原因,就分为以下情况:

文本文件:

  • 如果用 fgetc() 读取,要判断 feof() 的返回值是否为EOF;
  • 如果用 fgets() 读取,要判断 feof() 的返回值是否为NULL(0);

二进制文件:

        都是使用 fread() 读取,要判断其返回值与指定读取个数的大小,如果小于实际要读的个数,就说明发生读取异常,如果等于实际要读的个数,就说明是因读取成功而结束;

对于读取异常的判断,我们考虑判断 ferror() 函数的返回值:

  1. 若ferrror()为真——异常读取而结束;
  2. 若feof()为真——正常读取到尾而结束;

文本文件的判断:

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

int main()
{
	FILE* pf = fopen("test.txt", "r");

	if (pf == NULL)
	{
		perror("fopen is failed !");
		return;
	}
	int c = 0;
	//由于要检查EOF——EOF本质是0——所以是int
	while (c = fgetc(pf) != EOF)
	{
		putchar(c);
	}
	//直到while不执行了—读取结束了—判断是什么原因结束的
	if (ferror(pf))
	{
		printf("读取中出现错误\n");
	}
	else if (feof(pf))
	{
		printf("读取到文件尾\n");
	}

	fclose(pf);
	pf = NULL;

	return 0;
}

二进制文件的判断:

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

int main()
{
	FILE* pf = fopen("test.txt", "rb");
	int arr[5] = { 0 };

	if (pf == NULL)
	{
		return;
	}

	size_t num = fread(arr, sizeof(int), 5, pf);

	if (num == 5)
	{
		//说明全部读取成功
		printf("Array read successfully\n");
	}
	else
	{
		//说明读取不够指定长度—判断是什么原因
		if (ferror(pf))
		{
			printf("读取中出现错误\n");
		}
		else if (feof(pf))
		{
			printf("读取到文件尾\n");
		}
	}

	fclose(pf);
	pf = NULL;

	return 0;
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐