本期介绍🍖
主要介绍:为什么使用文件,什么是文件,文件的打开和关闭的操作方法,文件的顺序读写于随机读写,文件读取结束的判定👀。
文章目录
- 一、为什么使用文件🍖
- 二、什么是文件🍖
- 2.1 程序文件🍖
- 2.2 数据文件🍖
- 2.3 文件名🍖
- 三、文件的打开和关闭🍖
- 3.1 fopen函数🍖
- 3.2 fclose函数🍖
- 四、文件读写🍖
- 4.1 文件的顺序读写🍖
- 4.1.1 按字符读和写【fgetc && fputc】🍖
- 4.1.2 按字符串读和写【fputs && fgets】🍖
- 4.1.3 格式化的读和写【fprintf && fscanf】🍖
- 4.1.4 以二进制的形式读和写【fwrite && fread】🍖
- 4.2 文件的随机读写🍖
- 4.2.1 fseek函数🍖
- 4.2.2 ftell函数🍖
- 4.2.3 rewind函数🍖
- 五、文件读取结束的判定🍖
一、为什么使用文件🍖
文件存在的意义:就是为了能够让数据持久化的存在于电脑中。为什么这么说呢?你想啊,如果你在运行一个程序时没有将其数据保存下来,那么下一次再打开这个程序时,之前运行的数据必然不存在了(因为在程序运行时数据只是暂时存放于内存中,如若没有及时保存一旦程序结束,操作系统会回收这些运行时所占用的空间,故下一次运行时之前的数据自然就不存在了)。
所以为了能够将一些数据记录下来、保存下来、能在下一次调用程序时使用,我们可以将数据保存到文件中(也就是硬盘上),这样就可以做到数据的持久化了。
注意:涉及到数据持久化的问题,我们一般持久化的方法有两种:1. 保存到硬盘上;2. 保存到数据库中。
二、什么是文件🍖
文件是以硬盘为载体存储在计算机上的信息集合。在程序设计中我们一般谈的文件分为两种:程序文件、数据文件(是从功能的角度来分类的)。
2.1 程序文件🍖
包含源程序文件(后缀为.c)、目标文件(后缀为.obj)、可执行文件(后缀为.exe)。其中源文件就是存储我们所编写代码的文件,目标文件是由源文件经过编译(将.c文件转化成二进制文件的过程)后生成的,可执行文件是由目标文件经过链接(就是将.obj文件与C语言提供的库函数连接起来的过程)后生成的。
2.2 数据文件🍖
数据文件的内容不一定是程序,还有可能是程序运行时读写的数据。而本章主要讨论的就是这个数据文件,我们该怎么把数据写到文件中去,或者该怎么把数据从文件中读取到内存里面去。
根据数据的存放形式,数据文件还可以被分为:文本文件和二进制文件。我们知道,数据在内存中是以二进制的形式存放的,若不加任何转换直接输出到文件中,那么就称为二进制文件。如果数据在存储前要进行转换,将数据转换成ASCII码的形式然后再存放到文件中,那么就称文本文件。
举个例子:int型变量10000以文本和二进制的形式在文件中的内存布局,如下图所示。
如若我们想查看文件中存储数据的二进制编码,我们把数据文件在VS编译器中打开,然后选择打开方式,接着选择二进制编译器,最后点击确定就可以查看了。
2.3 文件名🍖
每一个文件都必然存在唯一的文件标识(为了方便起见,文件标识通常被称为文件名),以便用户辨别和引用。
每个文件名都是由三部分组成的:文件路径 + 文件名主干 + 文件后缀。举个例子:c:\code\test.txt
,其中c:\code\
是这个文件的路径,test
是文件名主干,.txt
是文件的后缀。
那有人就要问了,为什么文件名一定要有路径呢? 那你想啊,你在C盘里放一个test.txt
的文件,然后又在D盘中放一个test.txt
的文件。那么问题来了,当我想要引用test.txt
时,我到底是引用的C盘中的还是D盘中的呢?这就出现了歧义,所以为了能够使不同位置的文件名主干和后缀相同的文件有所区分,就必须在前面加上一个相对的路径。
三、文件的打开和关闭🍖
值得注意的是,对文件操作的步骤应该是先打开文件,然后对文件进行读或写,最后再关闭文件。那有人就要问了:为什么不直接对文件进行操作,而是需要先打开才行?
因为只有文件后我们才能与文件之间建立联系,才能对其进行修改,也可以这么说打开文件的目的是为了能够进行之后的读写操作。
那为什么只有fopen
打开文件后才能与文件之间建立联系? 因为每当使用fopen
打开文件都会在内存中建立一个文件信息区,然后返回该区域的起始位置的地址(类型为FILE*
类型,所以我们称之为文件指针)。之后我们就可以通过文件指针找到该信息区,又由于文件信息区与文件之间是紧密绑定的,故我们就可以通过文件信息区访问到该文件。也就是说,打开文件后返回的FILE*
文件指针与文件之间建立了某种联系,使得我们通过文件指针就能找到与它关联的文件。关系图如下:
至于“ 文件信息区 ”到底是什么,我之前有一篇博客讲述了,下面是它的链接【什么是文件信息区】。
3.1 fopen函数🍖
FILE *fopen( const char *filename, const char *mode );
fopen
第一个参数filename
表示需要被打开的文件名(也就是一个字符串),第二个参数mode
表示文件被打开模式(而打开的模式有很多种,如下图所示),返回值类型是一个FILE*
类型的指针。注意:若打开文件失败,则返回空指针NULL,所以在打开文件后一定要检查一下返回的值。
注意:
1. 用'r'
的形式打开文件后就只能对其进行读的操作,用'w'
的形式打开文件后就只能对其进行写的操作。绝对不能出现用读的形式打开你在那卡卡写,用写的形式打开你在那卡卡读。
2. 如果在读文件时我们想要打开的文件不存在,则表示打开失败返回NULL。但如果是在写或追加文件时我们想要打开的文件不存在,则会在相对路径底下新建一个文件。
3.2 fclose函数🍖
int fclose( FILE *stream );
fclose
的参数stream
是想被关闭文件的文件指针,类型为FILE*
。(我们会发现打开文件、读写文件、关闭文件的操作,与动态内存管理时的申请动态空间、使用动态空间、释放动态空间,这一整套操作非常相似。都会先占用内存一块空间,而这块空间又都不会自动释放,需要我们手动释放,若不释放则会一直占用这块资源,导致资源的浪费)
下面我们来实际运用fopen
和fclose
来尝试打开和关闭文件。
#include<stdio.h>
int main()
{
//打开文件(此时不存在test.txt文件)
FILE* pf = fopen("test.txt", "r");
//判断打开时是否发生错误
if (pf == NULL)
{
//perror打印错误信息函数
perror("fopen::");
return 1;
}
//…
//关闭文件
fclose(pf);
pf = NULL;
printf("打开关闭文件成功\n");
return 0;
}
结果为:No such file or directory,意思是不存在这样一个文件。可如果我们在相对路径(相对路径就是指:在当前目标项目的路径)底下给他建一个test.txt
的文件,再来看一下结果。
注意:参数filename
可以是绝对路径的也可以是相对路径的。打开相对路径下的文件时,文件名前是不需要加文件的路径的,就如上面的test.txt
就没有加路劲。而如果时访问绝对路径下的文件,则必须在文件名前加上文件的路径。下面举个例子,打开桌面上的test.txt
文件。
注意:在写文件的绝对路径时,字符串中的‘\’
其实是转义字符,并不表示字符\
,若像其表示\
我们需要在为其转义一下,如上图所示。
四、文件读写🍖
4.1 文件的顺序读写🍖
文件的顺序读写就是按照一定的顺序一个接着一个的对文件进行读写操作,而下面的这些都是顺序读写的函数。
4.1.1 按字符读和写【fgetc && fputc】🍖
int fputc( int c, FILE *stream );
我们先来讲述fputc()
字符输出函数,该函数可以向所有 “输出流” 输出数据,每次调用只能向流中写一个字符。fputc()
函数有两个参数:
- 参数
int c
:想要写入的字符- 参数
FILE* stream
:表示想要向哪个文件输出,这是一个与文件关联的指针。
注意:fputc
的返回值是int
型的变量,若成功写入则返回每一个写入的字符,若写入失败则返回EOF
。
下面让我们来实际操作一下:向文件中输出26个英文字母。
#include<stdio.h>
int main()
{
//以写的方式打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//按字符写到文件中去
int i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
可以看到test.txt
文件中有着从a到z的26个字母,而且是按照一个接着一个的顺序往文件里面放的,这就是顺序写。
int fgetc( FILE *stream );
fgetc()
字符输入函数,该函数适用于所有的“输入流”,可以从流中每次读取一个字符然后放到内存中去。fgetc()
函数只有一个参数FILE *stream
表示从哪一个文件中读取数据。至于读到的字符作为函数的返回值返回,类型为int
。
下面来实际操作一下,接着从上一个test.txt
的文件中读取数据(注意:此时文件中存放着26个英文字母):
#include<stdio.h>
int main()
{
//以读的形式打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//按字符读取文件中的数据
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意:如果文件中没什么数据可以被读取时,fgetc
会返回EOF
,我们可以以此为判断结束的标志。
4.1.2 按字符串读和写【fputs && fgets】🍖
前面所说的fputc()
和fgetc()
只能一个字符一个字符的读写,感觉效率有点低下,那是否存在一次性可以读写一整句话的函数呢?当然是存在的,fputs()
和fgets()
就是这样的输入输出函数。
int fputs( const char *string, FILE *stream );
fputs()
文本行输出函数,该函数适用于所有“输出流”,可以一次性写一整个字符串到所要输出的流或者文件中去。fputs()
函数有两个参数:
- 参数
const char *string
:是一个指向需要写入的字符串的指针。- 参数
FILE *stream
:与被写文件关联的指针。
函数fputs()
的返回值类型是int
,若输出成功则返回一个非负值,若输出失败则返回EOF
。下面来实际操作一下该函数:
#include<stdio.h>
int main()
{
//以写的形式打开文件
FILE* pf = fopen("test.txt", "w");
//向内存中输出一个字符串
fputs("hello world!!!", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们发现当我们以‘w’
的形式打开文件后,那些原本就存在于文件中的内容会消失不见。其实这是向文件中写数据的一个特点,必然会在fopen
打开文件时将文件中所有数据全部清空,然后再把我们所需要写的内容放进去。如果想要在原先存放的数组后添加新内存,我们可以以‘a’
追加的形式打开文件。
char* fgets( char *string, int n, FILE *stream );
fgets()
文本行输入函数,该函数适用于所有“输入流”,可以从内存中读取字符串放到内存中去。fgets()
函数有3个参数:
- 参数
char *string
:指向用于存放读取到的字符串的那块空间。- 参数
int n
:最大读取个数。- 参数
FILE *stream
:与被读文件关联的指针。
fgets()
函数的返回值为一个char*
类型的指针,若读取成功则返回读到字符串的起始地址,即:就是第一个参数指向那块空间的地址。若读取失败则返回NULL。下面来实际应用一下:
#include<stdio.h>
int main()
{
//以读的形式打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[20] = { 0 };
//从pf指向的文件中读一个长度为5的字符串到arr数组中
fgets(arr, 5, pf);
//将arr数组中存放的字符串打印出来
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们发现打印结果与我们的预期不相符,按要求因该会一次性读取5个字符的内容,也就是“ hello ”这个字符串(现在test.txt
文件中存放的内容是“ hello world!!! ”)。可实际上读到的却是“ hell ”,莫名其妙的少了一个字符,这是为什么呢?
原因是:fgets()
函数的第二个参数n
表示你一次可以读n个字符,但正真读到字符数只有n-1个,因为最后一个位置是要留给字符串结束标志\0
的。如下图所示:
4.1.3 格式化的读和写【fprintf && fscanf】🍖
上面所述的fputc、fgetc、fputs、fgets顺序读写函数仅适用于读写字符和字符串类型的数据,一旦要读写别的类型的数据时就不再适用了。所以为了能够向文件读写整型、浮点型、结构体等,C语言给出了fprintf 和 fscanf两个函数。
int fprintf( FILE *stream, const char *format [, argument ]…);
fprintf()
格式化输出函数,该函数适用于所有“输出流”,且用法与printf
几乎一致,就是前面多出来个FILE*
类型的参数,表示向什么文件输出。fprintf()
的返回值是写入的字符个数。下面来实际用一下该函数:
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
//以写的形式打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//向文件中输出一个结构体
struct S s = { "zhangsan", 20, 67.5 };
fprintf(pf, "%s %d %f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
int fscanf( FILE *stream, const char *format [, argument ]… );
fscanf()
格式化输入函数,该函数适用于所有的“输入流”,且用法与scanf
几乎一致,就是前面多出来个FILE*
类型的参数,表示从什么文件中读取数据。fscanf()
的返回值是成功读取到数据的项数,若遇到文件结尾则返回EOF
。下面来实际用一下该函数:
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
//以读的形式打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取一个结构体类型的数据
struct S s = { 0 };
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
值得注意的是,在用fscanf()
读取数据的时候,一定是大概知道文件中存放的是什么数据,以及这些数据的格式。尤其是具有某种特殊格式的文件,否则将无法下手。
4.1.4 以二进制的形式读和写【fwrite && fread】🍖
我们发现不管是字符输入/输出函数、文本行输入/输出函数还是格式化输入/输出函数操作的文件中存储的数据都是以文本的形式存在的,也就是以我们能够看懂的书面表达形式来存放。但fwrite() 和 fread() 却是以二进制的形式写到文件里,再以二进制的形式读取出来,这就导致了我们无法读懂文件当中存放的到底是什么(因为文件是以文本的视角来看待文件中存放的数据的)。
注意:前面所述的3对顺序读写函数是适用于所有“流”的,也就是说这些函数可以对所有的外部设备进行操作。但fwrite() 和 fread() 函数只能对“文件流”进行读写操作,这点要注意。
注意:要使用fwrite() 和 fread() 函数对文件进行读写操作,前提条件是必须以二进制读或者写的形式打开文件,即fopen
的打开模式为wb
或rb
。
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
fwrite()
二进制输入函数,该函数具有4个参数:
- 参数
const void *buffer
:指向要被写入文件的数据的指针。- 参数
size_t size
:数据的大小。- 参数
size_t count
:写入的数据的最大个数。- 参数
FILE *stream
:与被写文件关联的指针。
fwrite()
函数返回值为实际写入文件中数据的个数,如果发生错误返回值就会小于计数。下面来实际运用一下该函数:
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
//以二进制写的形式打开文件
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//将结构体以二进制的形式输出到文件中去
struct S s = { "zhangsan", 20, 67.5 };
fwrite(&s, sizeof(s), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
可见将数据以二进制的形式写道文件中去,以文件的视角来开是一堆乱码。至于为什么姓名不是乱码呢,因为字符在内存中是以ASCII码的形式保存的,而文件也是以这种形式来看待的,所以不是乱码。
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
fread()
二进制输入函数,该函数具有4个参数与fwrite
一摸一样:
- 参数
void *buffer
:指向存放被读取数据空间的指针。- 参数
size_t size
:数据的大小。- 参数
size_t count
:写入的数据的最大个数。- 参数
FILE *stream
:与被读文件关联的指针。
fread()
函数返回值 为实际读取到数据的个数,如果发生错误或在达到计数之前遇到文件末尾,则可能小于 count。下面来实际运用一下该函数:
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
//以二进制读的形式打开文件
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//从文件种以二进制的形式读取结构体数据
struct S s = { 0 };
fread(&s, sizeof(s), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.2 文件的随机读写🍖
根据上面对文件顺序读写的了解,我们发现顺序读写只能按照某种顺序一个接着一个的读或者写。但在C语言中还提供了一些随机读写的函数,如fseek()
、ftell()
、rewind()
。它们能够让文件指针定位到我们想要的位置上去,从而做到想读哪就读哪。
4.2.1 fseek函数🍖
int fseek( FILE *stream, long offset, int origin );
该函数是通过参考位置位置和偏移量来定位文件指针的。其中,参数long offset
表示需要从参考位置处偏移的量,参数int origin
表示参考位置。而fseek
函数提供了三个参考位置:SEEK_SET
(文件起始位置)、SEEK_END
(文件末尾位置)、SEEK_CUR
(文件指针当前位置)。下面举个例子:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//偏移定位文件指针,相对于文件起始位置
fseek(pf, 2, SEEK_SET);
//读取字符
int a = fgetc(pf);
printf("%c\n", a);
fclose(pf);
pf = NULL;
return 0;
}
注意:文件指针的偏移量的单位是字节,这道例题中SEEK_END
(文件末尾位置)是指向字符f
后一个字节,也就是说相对于末尾来说要定位f
需要偏移-1
个单位。
4.2.2 ftell函数🍖
long ftell( FILE *stream );
ftell()
函数返回当前文件指针相对于起始位置的偏移量,参数是文件指针。下面来举个例子:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//偏移定位文件指针,相对于文件起始位置
fseek(pf, 10, SEEK_SET);
//打印当前文件指针相对于起始位置的偏移量
int sz = ftell(pf);
printf("%d", sz);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.2.3 rewind函数🍖
void rewind( FILE *stream );
rewind()
函数可以让文件指针返回文件起始位置,参数是文件指针。下面来举个例子:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//偏移定位文件指针,相对于文件起始位置
fseek(pf, 10, SEEK_SET);
//打印当前文件指针相对于起始位置的偏移量
int sz = ftell(pf);
printf("%d\n", sz);
//是文件指针返回文件起始位置
rewind(pf);
sz = ftell(pf);
printf("%d\n", sz);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
五、文件读取结束的判定🍖
文本文件读取是否结束,判断返回值是否为EOF
或NULL
:
- 判断
fgetc()
返回值为EOF
- 判断
fgets()
返回值为NULL
- 判断
fscanf()
返回值为EOF
二进制文件读取是否结束,判断返回值是否小于实际要读的个数
- 判断
fwrite()
返回值是否小于实际要读的个数
下面举几个例子:
#include<stdio.h>
int main()
{
int ch = 0;
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件信息并判断是否结束
while ((ch = fgetc(pf)) != EOF)
{
putchar(ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
//写文件
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
FILE* pf = fopen("test.txt", "wb");
fwrite(arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), pf);
fclose(pf);
pf = NULL;
//读文件
int cover[10] = { 0 };
int tmp = 0;
int i = 0;
pf = fopen("test.txt", "rb");
while (1 == fread(&tmp, sizeof(cover[0]), 1, pf))
{
cover[i] = tmp;
i++;
}
fclose(pf);
pf = NULL;
//打印数组cover中数
for (i = 0; i < 10; i++)
{
printf("%d ", cover[i]);
}
return 0;
}
注意:函数feof()
并不是用来判断文件是否已经读取结束,而是应用于当文件读取结束时,判断到底是什么原因导致的,是读取失败还是遇到文件结尾了。下面举个例子:
#include<stdio.h>
int main()
{
//写文件
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
FILE* pf = fopen("test.txt", "wb");
fwrite(arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), pf);
fclose(pf);
pf = NULL;
//读文件
int cover[10] = { 0 };
int i = 0;
pf = fopen("test.txt", "rb");
if (fread(cover, sizeof(cover[0]), 15, pf) == 15)
{
printf("成功读取数据\n");
//打印数组cover中数
for (i = 0; i < 10; i++)
{
printf("%d ", cover[i]);
}
}
else
{
//判断是否是遇到文件末尾而结束
if (feof(pf))
{
printf("End of file reached successfully");
}
else if (ferror(pf))
{
printf("I/O error when reading");
}
}
fclose(pf);
pf = NULL;
return 0;
}
这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。