我们前面学习结构体时,写通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
既然是通讯录,就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有:把数据存放在磁盘文件、存放到数据库等方式。
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
文件的存在使得内存中的数据与磁盘中的数据得以交互,如此,程序运行的结果可以以数据文件的形式永久保存在磁盘上;而磁盘中的数据文件又可以作为程序处理的数据来源,为程序提供了键盘输入(实际上也是一种文件写入)或赋值之外的另一种数据获得途径。
计算机以文件的形式来管理所有的软件和硬件资源。文件是一系列的数据按照某种次序组织起来的数据流,它们都是一些数据的集合,这就是文件。
文件一般分为两种:
1、程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
2、数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
接下来我们讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
对于文件的操作有两种——读(Read)和写(Write)。如果将数据从磁盘文件读取至内存,这个数据的输入的过程就称为读文件;反之,如果将数据从内存存放至磁盘文件上,这个数据输出的过程就称为写文件。数据的输‘入’和输‘出’都是相对于内存而言的。
文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.
例如,VS2013编译环境提供的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*的指针变量
FILE* pf;//文件指针变量
注:通过文件指针变量能够找到与它关联的文件
文件的打开操作
文件打开函数fopen,原型为:
FILE* fopen(char * filename,char * mode);
FILE* fp;
fp=fopen("D:\\data\\file1.txt","r");
if(!fp)//if(fp==NULL)
{
printf("can not open file\n");//perror("fopen"); return 1;
exit(1);
}
// 由于文件不存在、磁盘空间满、磁盘写保护等各种原因,
//文件打开可能会失败。因此,打开文件后需要进行一定的判断,
//确保正确打开后才可以继续后面的读写操作。
其中,filename 为需要打开的文件的名称,有时可包含文件路径;mode为文件的打开方式,具体可见下表。该函数的返回值为 FILE 类型的地址,如果文件打开失败,则返回值为空指针NULL。
其中,第1个参数 filename 是欲打开的文件名,不含路径时,表示打开当前目录(程序所在工作目录)下的文件;如果含有路径,则表示文件所在的绝对路径,需要注意,路径中的斜杠应使用转义字符 ' \\'。
mode 是文件的打开方式,可取的参数范围如下表:
r | 以输入方式打开一个文本文件 |
w | 以输出方式打开一个文本文件 |
a | 以输出追加方式打开一个文本文件 |
r+ | 以读/写方式打开一个文本文件 |
w+ | 以读/写方式建立 个新的文本文件 |
a+ | 以读/写追加方式打开一个文本文件 |
rb | 以输入方式打开一个二进制文件 |
wb | 以输出方式打开一个二进制文件 |
ab | 以输出追加方式打开一个二进制文件 |
rb+ | 以读/写方式打开一个二进制文件 |
wb+ | 以读/写方式建立 个新的二进制文件 |
ab+ | 以读/写追加方式打开一个二进制文件 |
① “r”“w”“a”分别表示打开文本文件,并将进行读、写和追加这3种操作。若有后缀“+”,表示文件打开后可读可写,若有后缀“b”表示打开的是二进制文件而不是文本文件。
② 以“r”或“w”开头打开文件时,文件内部的位置指针指向文件的开始位置。意即从文件起始处开始读取数据;而以“a”开头打开文件时,文件内部的位置指针指向文件的末尾位置。(注意,位置指针不是上文所说的文件指针fp, 而是指示当前数据读写位置的指针,每读/写一次,位置指针均会向后移动。)
以“r”开头的任何方式打开文件的目的是打开一个已存在的文件,以便从中读取内容。如果该文件存在则成功打开;如果该文件不存在,则返回空指针NULL,打开失败。
以“w”开头的任何方式打开文件的目的是建立一个新文件。如果文件不存在,
则建立一个新文件;如果文件已经存在,则会自动清空原文件内容,以一个空的新文件来覆盖旧文件,所以在实际编程时应当避免旧文件被误删的风险。
以“a”开头的任何方式打开文件的目的是向一个已经存在的文件末尾追加更多的内容。如果该文件存在,则正常打开,位置指针在文件的末尾:如果该文件不存在
则此时相当于 将自动建立一个新文件,位置指针定位于文件的开头。
综上所述,在选用打开文件的方式时,需要综合考虑以下3点:
① 打开的文件类型是什么,以便决定是否需要“b”后缀。
② 打开文件的目的是什么,以便决定到底是“r”“w”还是“a”开头。
③打开文件之后的执行方式是什么,是单一的读或写操作,还是可读可写操作,以便决定是。否需要后缀“+”。
文件关闭操作
当文件操作完毕后,就应当将其关闭。这是因为:文件打开后,可能有一些数据被缓冲在内存中,若不正常关闭,这些数据就不能真正写入到文件中,可能造成数据丢失。关闭文件后,文件指针将与当前文件切断联系。
文件关闭的函数是:
int fclose(FILE *fp);
//例如,要关闭 f1 指针打开的文件,可以使用如下语句。
fclose(f1);
f1=NULL;
其中fp是打开该文件时的指针。如果文件关闭成功,函数返回值为0,否则返回一个 EOF( EOF是一个定义在 stdio.h 中的符号常量,值为-1)。该函数虽然有返回值,但是在调用时常常忽略返回值,而只作为函数调用语句来调用,这与 printf 函数的调用类似。
文件的读写
字符读写
字符读写函数包括 fputc 和 fgetc 两个函数,它们主要用的文本文件的读写。
(1)、fputc——将字符写入文件。
函数 fputc 实现将指定的单个字符写入到指针打开的文件中,其原型如下:
int fputc( int c, FILE *fp );
其中,c是要写入文件的字符,它虽被定义为整型,但只使用最低位的一个字节,fp 是文件指针。fputc 的功能是、将字符c写入所指向的文件。如果成功,位置指针自动后移一个字节的位置,并且返回 c的ASCLL码值;否则返回 EOF。
(2)fgetc——从文件中读出一个字符。
函数 fgetc 实现读取位置指针当前所指向的字符并返回,其原型如下:
int fgetc( FILE *fp );
其中 fp 为文件指针。fgetc 的功能是,从 fp所指向的文件中读取位置指针所指向的一个字符,如果成功则返回读取的字符的ASCLL码值,位置指针自动后移一个字节的位置;否则返回 EOF。
while((ch=fgetc(fp))!= EOF) //读文件操作
{
putchar(ch); //屏幕输出刚刚读出的字符内容
}
putchar('\n');
fclose(fp); //关闭文件
1、在读文件操作中,本例使用了“while((ch=fgetc(fp))!= EOF)”循环。当发生
读文件错误,或者已读到文件结尾时,fgetc 函数返回一个 EOF,循环就结束。
2、读文件时,需要注意判断何时读到文件结尾。除上述根据 fgetc 的返回值来判断外,C语言还提供了一个feof函数, 其原型为:
int feof(FILE *fp);
该函数的作用是,当位置指针指向文件的末尾时,返回一个非0值,否则返回0。
因此,本例中的 while 循环,可用下列程序段等效代替:
ch=fgetc(fp); /* 需要在判断前先读取一个字符 */
while(!feof( fp) )/*该条件表示位置指针未指向文件的末尾,文件没结束*/
{
putchar(ch); /*向屏幕输出文件中读取的当前字符*/
ch=fgetc(fp);/*继续从文件中读取当前字符*/
}
2.字符串读写
字符串读写函数包括 fputs 和 fgets 两个函数,它们主要也是用于文本文件的读写。
(1)、fputs——将字符串写入文件。
函数 fputs 实现将指定的一个字符串写入到指针打开的文件中,其原型如下:
int fputs(const char *s, FILE *fp);
其中,s 是要写入的文件的字符串,fp 是文件指针。fputs 的功能是:将字符串s输出至 fp 所指向的文件,不含'\0”。如果成功,位置指针自动后移,函数返回一个非负整数;否则返回 EOF。
(2)、fgets——从文件读取字符串。
函数 fgets 实现从指针打开的文件中读取字符串到内存,其原型如下。
char *fgets(char* S ,int num, FTLE *fp);
其中,s指向待赋值字符串的首地址,num是控制读取个数的参数,fp 为文件指针。fgets的功能是,从位置指针开始读取一行或num-1个字符,并存入 s,存储时自动在字符串结尾加上\0'。如果函数执行成功,位置指针自动后移,并返回s的值,否则返回NULL。
3.格式化读写
(1)fprintf-——将内容写入文件。
函数 fprintf实现将指定内容按格式要求写入到指针打开的文件中,其原型如下:
int fprintf( FILE *fp,(const) char* format,输出参数1,输出参数2...);
其中,fp是文件指针,format 为格式控制字符串,输出参数列表为待输出的数据。fprintf的功能是根据指定的格式(format 参数)发送数据(输出参数)到 fp 打开的文件。
(printf是fprintf 的特殊形式,语句 printf("There are %d cats here.\n",2);与语句 printf(stdout,"There are %d cats here.\n",2);是完全等效的。事实上,stdout 就是默认的对应显示器的文件指针,不需要做特殊定义,输出内容自动输出到显示器这个特殊文件上。因此,使用 fprintf 的方式与 printf 几乎是一样的,fprintf函数中的格式控制方式不变,只需要在最前面加一个文件指针参数,输出内容就写入到文件指针打开的对应文件中而不是输出到显示器上。)
(2)fscanf——从文件读取内容给变量。
函数 fscanf实现按指定格式从指定文件中读取内容作为对应变量的值,其原型如下:
int fscanf( FILE *fp, const char* format, 地址1,地址 2…);
其中,fp是文件指针,format为格式控制字符串,地址列表为输入数据的存放地址。fscanf的功能是根据 format 参数指定的格式从 fp 打开的对应文件中读取数据存到地址参数指定的内存中。
(scanf是是fscanf的特殊形式,若有变量定义:int a;,则语句 scanf("%d ",&a);与语句 fscamf( stdin, “%d ", &a );完全等效。事实上,stdin 是默认的对应键盘的文件指针,不需要做特殊定义,输入的内容来自于键盘这个特殊文件。)
因此,使用fscanf 的方式与scanf 几乎是一样的,scanf 从键盘输入内容到变量,到了 fscanf函数中控制方式不变,只需要在最前面加一个文件指针参数,则这些内容就通过指针打开的文件自动输入到变量。
这里不得不提到sprintf、sscanf这两个函数:
把格式化数据转换成字符串。
把一个字符串,转换成对应的格式化数据。
4.块数据读写
块数据读写函数包括 fwrite 和 fread 两个函数,它们主要用于二进制文件的读写。(1)fwrite——将内容写入文件。
函数 fwrite 实现将指定起始位置开始的指定字节数的内容直接写入到指针打开的文件中,其原型如下:
int fwrite( const void *buffer, int size, int n, FILE *fp );
其中,buffer 表示要输出数据在内存中的首地址,size 为一个数据块的字节数,n 为数据块的个数,fp为文件指针。fwrite 的功能是:从内存的 buffer 地址开始,将连续 n*size 个字节的内容原样复制到 fp 打开的文件中。该函数的返回值是实际写入的数据块个数。
(2)fread——从文件读取内容给变量。
函数 fread 实现读取文件当前位置开始的指定字节数的内容,然后直接存到内存指定的起始地址开始的内存空间里,其原型如下:
int fread( void *buffer, int size, int n, FILE *fp );
其中,buffer 表示要输人数据在内存中的首地址,size为一个数据块的字节数,n为数据块的个数,fp 是文件指针。fread 的功能是:从 fp 打开的文件的当前位置开始,连续读取 n*size 个字节的内容,存入 buffer 作为首地址的内存空间里。该函数的返回值是实际读入的数据块个数。
位置指针的定位
前面介绍了4组文件读写的函数,读写操作完成后,位置指针都会往文件末尾顺序移动相应的距离。
从本质上说,这些操作均属于文件的顺序读写。
本节将介绍几个函数,可以对文件位置指针进行更改,从而实现文件的随机读写。
(1)、rewind——文件位置指针回到文件开头。
通过rewind函数可以实现使文件的位置指针回到文件开头,其原型如下:
void rewind ( FILE *fp );
其中fp为文件指针。该函数的作用是,使fp指向的文件的位置指针重新指向文件头、同时清除和文件流相关的错误和 eof标记
(2)、fseek——改变文件位置指针的通用函数。
通过 fseek 函数可实现将文件的位置指针做任意方向任意距离的移动,其原型如下:
int fseek ( FILE *fp, long offset,int from);
其中,fp 为文件指针,offset 为移动的字节数,from 为移动的起始位置。该函数的作用是,将文件的位置指针从from 开始移动 offset字节。执行成功返回 0,执行失败返回非零值且文件指针还在原位置 。
from的取值范围如下:
① 0 或者 SEEK SET : 起始位置为文件头。
② 1或者 SEEK CUR: 起始位置为当前位置。
③ 2 或者 SEEK END:起始位置为文件尾。
当 offset 为正数时,表示文件当前位置指针向文件末尾移动;当 offsct 为负数时,则表示文件当前位置指针向文件起始位置移动。需要注意的是,offset 必须为长整型。
(3)ftell——确知文件位置指针相对于文件头的位置。
通过 ftell 函数可以确知文件位置指针相对于文件为的偏移字节数,其原型如下:
long ftell(FILE *fp);
其中fp为文件指针。该函数的作用是,返回位置指针相对于文件头的偏移字节数,如果出错,则返回-1L。该函数用于 fseek(fp,0,SEEK_END);之后,就可以得到文件的长度。
feof函数
读文件时,feof函数判断何时读到文件结尾,即文件读取结束的判定。
该函数的作用是:当位置指针指向文件的末尾时,返回一个非0值,否则返回0。
1、被错误使用的feof:
牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。
feof的作用是:当文件读取结束的时候,判断是读取结束的原因是否是遇到文件尾结束。
1.文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
例如:
fgetc判断是否为EOF(当发生读文件错误时,或者已经读到文件末尾时,fgetc函数返回一个EOF。);
读取失败返回EOF:
(1).遇到文件末尾,返回EOF,同时设置一个状态,遇到文件末尾了,使用feof来检测这个状态;
(2).遇到错误,返回EOF,同时设置一个状态,遇到了错误,使用ferror来检测这个状态
用:
ch=fgets(fp);
while(!feof(fp))
{
putchar(ch);
ch=fgetc(fp);
}
来等效替代:
while((ch=fgetc(fp))!=EOF)
{
putchar(ch);
}
其实是不太准确的,因为它其实是读取结束后才去判断,而不是一边读一边判断,所以我们应该写成:
while ((c = fgetc(fp))!= EOF)// 标准C I/0读取文件循环
{
putchar(c);
}
//判断是什么原因结束的
if(ferror(fp))
puts("I/0 error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
fgets判断返回值是否为NULL;
if(!fp)
{
perror("File opening failed.");
return EXIT_FAILURE;//1
}
2.二进制文件的读取结束判断:
判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.,2.,3.,4.,5.};
FILE *fp=fopen("test.bin","wb"); // 必须用二进制模式
fwrite(a,sizeof *a,SIZE,fp); // 写 double 的数组 fc1ose(fp);
double b[SIZE];
fp=fopen("test.bin","rb");
size_t ret_code = fread(b, sizeof *b, SIZE,fp);// 读 double 的数组
if(ret_code == SIZE)
{
puts("Array read successfully,contents: ");
for(int n = 0; n < SIZE; ++n) printf("%f ",b[n]);
putchar('\n');
}
e1se {
// error handling
if(feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if(ferror(fp)){
perror("Error reading test.bin");
}
}
fclose(fp);
}
文件缓冲区的存在的证明
//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;
}
拷贝一个文件:
int main()
{
//实现一个代码,拷贝一个文件
//打开文件
//打开被读的文件
FILE* pfRead = fopen("test1.txt", "r");
if (pfRead == NULL)
{
perror("open file for read");
return 1;
}
//打开要写的文件
FILE* pfWrite = fopen("test2.txt", "w");
if (pfWrite == NULL)
{
fclose(pfRead);
pfRead = NULL;
pfRead = NULL;
perror("open file for write");
return 1;
}
//拷贝
int ch = 0;
while ((ch=fgetc(pfRead)) != EOF)
{
fputc(ch,pfWrite);
}
//关闭文件
fclose(pfRead);
pfRead = NULL;
fclose(pfWrite);
pfWrite = NULL;
return 0;
}
函数原型图片截图来源于网址:https://cplusplus.com