文件操作详解

      所属专栏:C语言

      创作不易,望得到各位佬们的互三呦

前言:我们为什么要使用文件呢?

如果没有文件,我们写的程序的数据在电脑的内存中,如果程序退出,内存回收,数据就会丢失,等程序再运行,是看不到上次程序的数据的,如果将数据进行持久化的保存,我们就可以使用文件

1.什么是文件

硬盘上的文件是文件。其包括两种:程序文件和数据文件

1.1程序文件

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

1.2数据文件

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

1.3文件名

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

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

例如:c:\code\test.txt

其中c:\code\为文件路径,test为文件主干,.txt为文件后缀

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

根据数据的组织形式,数据文件被分为 文本文件二进制文件。
数据在内存中的以二进制的形式存储,如果不加转换的输出到外存中的文件中,就是 二进制文件(二进制文件我们是看不懂的)
那么一个数据在文件中的存储时怎样的呢?
字符一律以ASCII形式存储,数据型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而以二进制形式输出,则在磁盘上只占4个字节(vs2019测试)
注:1的ASCII码值为49,其二进制为0011001,0的ASCII码值为48,其二进制为0011000

 测试代码:

#include <stdio.h>
int main()
{
 int a = 10000;
 FILE* pf = fopen("test.txt", "wb");
 fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
 fclose(pf);
 pf = NULL;
 return 0;
}
那要怎么查看生成的文件的二进制存储呢?
下面我就以图片的形式展示

 

这里他是以16进制展示的,因为vs中数据存储方式是小端存储(具体可以看之前的博客哦:数据在内存中的存储),即低字节的存储到低地址处,高字节存储到高字节处,所以其以16进制展示为10 27 00 00。

3.文件的打开与关闭

  3.1 流与标准流

     3.1.1流

我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了⽅便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输⼊输出操作都是通过流操作的。
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后进行操作。

     3.1.2标准流

在C语言程序启动时,会默认打开三个流:
stdin - 标准输⼊流,在大多数的环境中从键盘输入,scanf函数就是从标准输⼊流中读取数据。
stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。

stderr - 标准错误流,大多数环境中输出到显示器界面。

这是默认打开了这三个流,我们使用 scanf,printf 等函数就可以直接进行直接进行输入输出操作的。
stdin,stdout,stderr三个流的类型是 FILE*,通常称为文件指针。
C语言中,就是通过 FILE*的文件指针来维护流的各种操作的。
这是默认打开了这三个流,我们使用 scanf,printf 等函数就可以直接进行直接进行输入输出操作的。
stdin,stdout,stderr三个流的类型是 FILE*,通常称为文件指针。
C语言中,就是通过 FILE*的文件指针来维护流的各种操作的。

  3.2 文件指针 

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

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

例如:vs2012编译环境提供的stdio.h头文件中有以下的文件类型申明:

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* pf ;

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

比如:

     3.3文件的打开与关闭

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

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

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

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

 mode表示文件打开的方式,如:

如:

#include <stdio.h>
int main()
{
	FILE* pFile;
	//打开⽂件
	pFile = fopen("tx.txt", "r");//文件以读的方式打开
	//如果指定文件不存在,就会出错
    //⽂件操作
	if (pFile == NULL)//文件打开失败就会返回空
	{
		perror("fopen fail!");
		return 1;
	}
	return 0;
}

4.文件的顺序读写

     4.1 文件的顺序读写

上面所说的适用于标准输入流一般指适用于标准输入流和其他输出流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。

在这里给大家推荐一个网站(里面可以查到函数的有关作用和函数原型以及返回值等):cplusplus.com - The C++ Resources Network

     4.1.1 fputc

函数原型:int fputc ( int character, FILE * stream );

character表示要写入的字符,FILE*stream表示文件指针指向的文件流

作用:将字符写进文件指针指向的文件中(顺序写入,就相当于一个一个写进去)。

返回值:成功后,将返回写入的字符。
如果发生写入错误,则返回EOF并设置错误指示符ferror )。​​​​​​​​​​​​

#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch;
	for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, pf);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

 

可以发现文件中写入了'a'到‘z’的字符。

     4.1.2 fgetc

函数原型:int fgetc ( FILE * stream );

FILE*stream表示文件指针指向的文件流(注:为什么这里的返回值为int呢?这里会在下面的返回之中讲到)

作用:从文件中读取字符,并将其返回

返回值:成功时,返回读取的字符(提升为int值)。
返回类型为int以适应特殊值EOF,该值指示失败:如果位置指示符位于文件末尾,则该函数返回EOF并设置流的eof 指示符feof ) 。 如果发生其他读取错误,该函数也会返回EOF,但会设置其错误指示符ferror )。

下面举个栗子来表示其返回值有关的知识:

#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch;
	while ((ch = fgetc(pf)) != EOF)//返回值不等于EOF表示没有碰到文件末尾
	//或者没有发生读取错误
	{
		printf("%c", ch);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

 

     4.1.3 fgets

函数原型:char * fgets ( char * str, int num, FILE * stream );

str表示指向复制读取的字符串的char数组的指针

num表示要复制到str中的最大字符数(包括‘\0’),也就是最多读取num-1个字符,以为它要为指向的char*类型的数组添加一个终止空字符

stream表示表示文件指针指向的文件流

作用:从流中读取字符并将它们作为 C 字符串存储到str中,直到读取 ( num -1) 个字符或到达换行符或到达文件末尾(以先发生者为准)。

换行符使fgets停止读取,但该函数将其视为有效字符并包含在复制到str的字符串中。复制到str的字符后会自动附加一个终止空字符。 请注意,fgets与gets完全不同:不仅fgets接受流参数,还允许指定str的最大大小并在字符串中包含任何结尾换行符。

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

下面一一举栗子让大家理解上面的知识:

4.1.3.1 读取num个字符,只读取num-1个字符
#include<stdio.h>
int main()
{
	FILE* pf = fopen(pf, "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char a[100] = "**********";//这样初始化来观察是否只读取num-1个字符
	//,并在其后添加了个'\0',想要更细致的观察,可以通过调试来观察
	fgets(a, 6, pf);
	return 0;
}

test.txt

 

可以观察到其其确实只读了5个字符,并在其后添加了个‘\0’。

4.1.3.2 遇到换行符号就读取结束,并在其后添加一个‘\0’
#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char a[100];
	fgets(a, 10, pf);
	return 0;
}

test.txt

4.1.3.3 遇到文件末尾返回空指针
#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char a[100];
	while (fgets(a, 10, pf) != NULL)
	{
		printf("%s", a);
	}
	return 0;
}

test.txt

4.1.4 scanf / printf / fscanf / fprintf / sscanf / sprintf的对比

    4.1.4.1 scanf / printf

scanf函数原型:int scanf ( const char * format, ... );

作用:从标准输入流读取格式化的数据

printf函数原型:int printf ( const char * format, ... );

作用:把数据以格式化的形式打印在标准输出流上

    4.1.4.2 fscanf / fprintf

函数原型:int fscanf ( FILE * stream, const char * format, ... );

作用:从指定的输入流上读取格式化的数据

函数原型:int fprintf ( FILE * stream, const char * format, ... );

作用:把数据以指定的格式打印在指定的输出流上

    4.1.4.1 sscanf / sprintf

函数原型:int sscanf ( const char * s, const char * format, ...);

作用:从字符串中读取格式化的数据

函数原型:int sprintf ( const char * s, const char * format, ...);

作用:把格式化的数据转换为字符串

 可能看到上面的并不是很理解这些函数,但没关系,下面我会用代码对其进行讲解(代码上有对应注释

代码1:

include<stdio.h>
struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	char buf[200] = { 0 };
	struct S s = { "一代",18 ,90.5f };
	sprintf(buf, "%s %d %f", s.name, s.age, s.score);
	//将格式化的数据转换成字符串
	printf("%s", buf);
	//把数据以格式化的形式打印在标准输出流上
	return 0;
}

 

代码2:

#include<stdio.h>
struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	char buf[200] = { 0 };
	struct S s = { "一代",18 ,90.5f };
	sprintf(buf, "%s %d %f", s.name, s.age, s.score);
	//将格式化的数据转换成字符串
	struct S t = { 0 };
	sscanf(buf, "%s %d %f", t.name, &(t.age), &(t.score));
	//从字符串中读取格式化的数据
	printf("%s %d %f", t.name, t.age, t.score);
	return 0;
}

代码3:

#include<stdio.h>
struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	struct S s = { 0 };
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
	//从pf指向的文件流中读取格式化数据
	printf("%s %d %f", s.name, s.age, s.score);
	return 0;
}

对应test.txt

代码4:

#include<stdio.h>
struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	struct S s = { "一代",18 ,90.5f };
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	fprintf(pf, "%s %d %f", s.name, s.age, s.score);
	//把数据以指定格式打印在pf指向的文件流上
	fprintf(stdout, "%s %d %f", s.name, s.age, s.score);
	//把数据以指定格式打印在标准输出流上
	return 0;
}

程序运行之前对应txt

运行之后对应txt

 

fprintf 把数据以指定格式打印到标准输出流上

4.1.5 fwrite

函数原型:size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

ptr:指向要写入的元素数组的指针,转换为const void*。

size:要写入的每个元素的大小(以字节为单位)。
size_t是无符号整数类型。

count:元素数量,每个元素的大小为size字节。

stream:指向指定输出流的FILE对象的指针。

作用:从ptr指向的内存块到文件流中的当前位置写入count元素 的数组,每个元素的大小为size字节

#include<stdio.h>
int main()
{
	int a[] = { 1,2,3,4,5 };
	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写数据
	int sz = sizeof(a) / sizeof(a[0]);
	fwrite(a, sizeof(a[0]), sz, pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

对应txt文件

以为这里是以二进制是以文件写进去的,所以看不懂很正常。

4.1.6 fread

函数原型:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

ptr:指向大小至少(size*count)个字节的内存块的指针,转换为void*。

size:要读取的每个元素的大小(以字节为单位)。

count:元素数量,每个元素的大小为size字节。

stream:指向指定输入流的FILE对象的指针。

作用:从文件流中读取count元素 的数组,每个元素的大小为size字节,并将它们存储在ptr

指定的内存块中

#include<stdio.h>
int main()
{
	int a[] = { 1,2,3,4,5 };
	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写数据
	int sz = sizeof(a) / sizeof(a[0]);
	fread(a, sizeof(a[0]), sz, pf);
	for (int i = 0; i < sz; i++)
	{
		printf("%d", a[i]);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

对应二进制文件

经读取后得txt文件

4.1.7 fseek

函数原型:int fseek ( FILE * stream, long int offset, int origin );

stream:指向标识流的FILE对象的指针

offset:二进制文件:从origin偏移的字节数。
          文本文件:零或由ftell返回的值

origin:用作偏移参考的位置,其包含三个位置

返回值:如果成功,该函数返回零。
否则,它返回非零值。
如果发生读取或写入错误,则会设置错误指示器ferror )。

下面我用几个代码加图给大家演示:

对应txt文件:

代码1:

#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	//文件打开的时候文件指针指向字符a的前面
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(pf);
	printf("%c\n", ch);
	//读取一个字符,文件指针向后偏移一个位置,指向字符b的前面
	fseek(pf, 4 , SEEK_CUR);//从文件指针当前位置进行偏移四个字节
	//即从b开始读取,读到f的前面,读取了b,c,d,e这四个字符
	ch = fgetc(pf);
	//再读取一个字符,读到的是fa
	printf("%c", ch);
	return 0;
}

4.1.8 ftell

函数原型:long int ftell ( FILE * stream );

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

返回值:成功时,返回位置指示器的当前值。
失败时,返回-1L ,并将cplusplus.com - The C++ Resources Network设置为系统特定的正值。

这里代码以上面图示为例:

#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	//文件打开的时候文件指针指向字符a的前面
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(pf);
	//读取一个字符,文件指针向后偏移一个位置,指向字符b的前面
	fseek(pf, 4 , SEEK_CUR);//从文件指针当前位置进行偏移四个字节
	//即从b开始读取,读到f的前面,读取了b,c,d,e这四个字符
	ch = fgetc(pf);
	//再读取一个字符,读到的是f
	printf("%d", ftell(pf));//最后一次读取f,文件指针指向f的后面,
	//相对于起始位置的偏移量为6
	return 0;
}

 4.1.9 rewind

函数原型:void rewind ( FILE * stream );

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

返回值:无

 

#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	//文件打开的时候文件指针指向字符a的前面
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(pf);
	//读取一个字符,文件指针向后偏移一个位置,指向字符b的前面
	fseek(pf, 4 , SEEK_CUR);//从文件指针当前位置进行偏移四个字节
	//即从b开始读取,读到f的前面,读取了b,c,d,e这四个字符
	ch = fgetc(pf);
	//再读取一个字符,读到的是f
	rewind(pf);//将文件指针回到文件的起始位置
	printf("%d", ftell(pf));//回到起始位置,相对于起始位置的偏移量为0
	return 0;
}

5.文件读取结束的判定

前言:当打开一个流时,这个流上有两个标记值
1.是否遇到文件末尾

2.是否发生错误

5.1 feof

函数原型:int feof ( FILE * stream );

作用:如果设置了与流关联的文件结束指示符,则返回非零值。
否则,返回零

在这里feof是被很多人误认的一个函数,认为其的返回值是用来判断文件是否结束的标志

其实feof的作用是:当文件读取结束的时候,判断读取结束的原因是否是遇到文件尾结束。

这里有几个判断文本文件是否结束的函数

例如:

fgetc 判断是否为 EOF 
fgets 判断返回值是否为 NULL 
判断二进制文件的读取结束判断
例如:
fread判断返回值是否⼩于实际要读的个数
#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");//以写的形式打开文件
	if (pf == NULL)
	{
		perror("fopen");
	}
	char ch = 0;
	//对文件进行读取,因为是以写的形式打开文件,所以读取错误
	for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, pf);
	}

	//判断是什么原因导致读取结束
	if (feof(pf))
	{
		printf("遇到文件末尾结束,读取正常结束\n");
	}
	else if (ferror(pf))//读取时发生了错误
	{
		perror("fputc");
	}
	return 0;
}

 

6.文件缓冲区

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

 

#include <stdio.h>
#include <windows.h>
//VS2019 WIN11环境测试
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;
}

这里给个代码,大家下来可以用vs尝试一下。

这里可以得出⼀个结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写⽂件的问题。
      好了,文件的内容已经讲的差不多了,感谢大家的支持。留下一个小小的赞在走吧。
  • 115
    点赞
  • 94
    收藏
    觉得还不错? 一键收藏
  • 101
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 101
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值