> 文档中心 > 【文本文件与二进制文件的区别;文件打开与关闭的方法、不同读写方式】(学习笔记19--文件上)

【文本文件与二进制文件的区别;文件打开与关闭的方法、不同读写方式】(学习笔记19--文件上)

目录

  • 文件的打开与关闭
    • 文件的打开
    • 文件的关闭
    • 标准文件流
    • 文件流的重定向
  • 文件的读写
    • 字符的方式读写文件
    • 以行的方式读写文件
    • 以格式化的方式读写文件
    • 以块的方式读写文件

文件的打开与关闭

文件的实质就是存储在外部储存介质上的一段连续的二进制数据。在程序中,可能需要从文件中读取数据,也可能会将程序的数据或运行日志输出、记录到文件中。因此程序和文件之间的关系是非常密切的

可否阅读的角度来看,可将文件分为文本文件和二进制文件两大类。
文本文件是可阅读的,例如用Windows自带的记事本、写字板所编辑出来的文件,就是文本文件,文本文件是以字符码(字符的二进制码)的形式进行存储的,用户可以随时打开文本文件,阅读文件的内容。
二进制文件并非以字符码形式进行存储的文件,例如图片、音乐、视频都是属于二进制文件,由于这些文件所存储的并非是字符,无法以字符的形式进行阅读,通常要用专门的软件进行图片的查看或者音乐、视频的播放。
因此,我们所编写的程序源代码文件就属于文本文件,而编译生成的可执行文件就属于二进制文件

C语言程序对文件的处理采用文件流的形式,程序运行在内存中的,而文件存储在外部存储介质上,例如硬盘、光盘、U盘等。
在程序运行时,就会在指定的 文件之上建立一条管道,当读取文件时,数据就会像流水一样从文件端流向程序端,而写入文件时,数据就会像流水一样从程序端流向文件端。

从文件端向程序端的文件流称为输入流,从程序端向文件端的文件流称为输出流
在这里插入图片描述
只要打开一个文件,就会得到一个文件流。有了文件流之后,就可以对文件进行相应的读写操作。

文件的打开

下面就来讲述如何在C程序中打开和关闭一个文件,即如何在C程序中得到一个文件流与关闭一个文件流。在C语言标准库中,有一系列和文件相关的库函数,只需在程序中包含stdio.h这个头文件,就可以使用这些函数

C语言中,打开文件需要使用fopen函数,该函数原型如下

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

fopen函数的返回值是一个文件流,其实就是FILE结构体类型的指针。函数执行成功会返回一个指向FILE结构体变量的指针,这个结构体变量包括了文件的名称、大小、属性、缓冲区等相关信息;若函数执行失败,则返回的是空指针。因此,可以通过对fopen函数所返回的文件流进行判断,从而获知文件打开是否成功

fopen函数的两个参数都是字符类型的常量指针,即指向字符串常量的指针。其中fname用于指定所要打开的文件,可以是文件的绝对路径(从盘符开始的文件路径),也可以是文件的相对路径(从当前工作目录开始的文件路径)

参数mode用于指定文件的打开模式,以确定用何种方式对文件进行处理得到相关的文件流

模式字符串 功能
“r” 以只读模式打开文本文件
“w” 以只写模式打开文本文件
“a” 以追加模式打开文本文件
“r+” 以读写模式打开文本文件
“w+” 以读写模式打开文本文件
“a+” 以读写模式打开文本文件
“rb” 以只读模式打开二进制文件
“wb” 以只写模式打开二进制文件
“ab” 以追加模式打开二进制文件
“rb+” 以读写模式打开二进制文件
“wb+” 以读写模式打开二进制文件
“ab+” 以读写模式打开二进制文件

说明:
1.只读方式要求文件必须存在,否则打开文件失败
2.只写方式会清空所打开文件的内容,并做好从文件头写入数据的准备。若文件不存在,则会自动创建
3.追加方式不会清空文件内容,并做好从文件末写入数据的准备。若文件不存在,则会自动创建
4.读写方式打开文件,则会对打开的文件具有读取和写入的能力。不过,它们仍然要受到基本属性的限制,如r+仍需文件必须存在,w+仍会清空文件内容,而a+仍会在文件末进行数据的写入
5.模式字符串中含有字符b表示对二进制文件的操作,没有字符b,表示对文本文件的操作

从文件流的角度来说,用r模式打开文件会得到一个文件的输入流,用w模式打开文件会得到一个文件的输出流,而用带+模式打开文件会得到文件的输入、输出流

下面使用fopen函数来打开一个文件

FILE *pfile = fopen("D:\\test.txt","r");if(pfile != NULL)printf("File opened successfully.\n");elseprintf("Failed to open file.\n");

代码中定义了一个FILE结构体类型的指针变量pfile,并调用fopen函数,以r(只读)模式打开D盘(确保Windows系统下有D盘)下面的文本文件test.txt,即想得到一个文件的输入流。fopen函数的返回值被初始化给指针变量pfile。需要注意的是,在fopen函数的第一个字符串参数中,使用的是绝对路径D:\test.txt,由于斜杠\在C语言中是作为转义字符来使用的,因此,必须用\(连续的两个斜杠),才能表示一个斜杠字符本身

编译运行程序,如果在计算机的D盘中有test.txt这个文件的话,就会打印如下结果

File opened successfully.

如果D盘下没有test.txt这个文件,则会得到如下结果

Failed to open file.

若是将fopen函数的文件打开模式修改为只写,即使用模式字符串w,即表示想得到一个文件的输出流

FILE *pfile = fopen("D:\\test.txt","w");

则无论在D盘(确保Windows系统下有D盘)下是否存在test.txt这个文件,它也会自动创建一个。但若此时在test.txt文件中手工输入一些内容并保存,当再次执行该程序后,会发现test.txt文件中原先输入的内容被清空了,即test.txt变成了一个空文件

如果既想打开一个文件,得到文件的输出流,而又不想让文件的内容被清空,那么就使用含有字符a的模式字符串

FILE *pfile = fopen("D:\\test.txt","a");

文件的关闭

可以使用fclose函数来关闭一个文件,该函数原型为

int fclose(FILE *stream);

fclose函数只有一个参数stream,是FILE结构体类型的指针,表示一个文件流。fclose函数的功能就是将该函数所指定的文件流关闭。函数的返回值为int类型,当函数执行成功,返回整型值0,当函数执行失败,返回一个EOF。EOF是一个宏的名字,是End of file(文件终点)的首字母缩写,它表示一个非零值,通常被定义为-1

我们可以将fopen函数所返回的文件流作为实参,调用fclose函数,以关闭与该文件流相关联的文件

FILE *pfile = fopen("D:\\test.txt","r");if(pfile){printf("File opened successfully.\n");if(!fclose(pfile))printf("File closed successfully.\n");elseprintf("File closure failed successfully.\n");}elseprintf("Failed to open file.\n");

代码中,首先通过fopen函数打开D盘的test.txt文件,并返回与该文件相关联的输入流,然后通过if语句对文件流pfile进行检查,若文件流pfile是正常的、可用的,则执行大括号中的语句;若文件流是不可用的,则执行else部分,打印输出文件打开失败的信息

在大括号中,首先通过printf函数打印文件打开成功的信息,然后再次使用if…else语句。在小括号内的条件表达式中,将文件流pfile作为实参调用fclose函数,这会将与文件流pfile相关联的文件test.txt关闭。如果关闭成功,则fclose函数的返回值为0,由于前面使用了逻辑非运算符,因此,小括号内的表达式的值为真,这会通过printf函数在控制台窗口打印文件关闭成功的信息;若文件关闭失败,fclose函数的返回值为EOF,通过逻辑非运算符转换,小括号内表达式的结果为假,就会执行else部分,打印文件关闭失败的信息

由于之前已经在D盘上创建了test.txt文件,结果为

File opened successfully.File closed successfully.

将D盘上的test.txt文件删除,并重新运行该程序

Failed to open file.

标准文件流

C语言还有3个特殊的文件流,即标准输入流(stdin)、标准输出流(stdout)和标准错误输出流(stderr)
称其特殊,主要是因为它们所关联的不是普通的文件,而是设备文件,即将计算机中的输入、输出设备当作文件来看待,例如默认情况下,标准输入流是和键盘相关联的,标准输出流和标准错误输出流是和控制台窗口相关联的。
另外一个特别之处就是用户不需要考虑这3个文件流的启用与关闭,它们是由系统管理的。当程序启动时,这3个文件流会自动被启用,用户可以直接使用,而在程序关闭时,这3个文件流会被自动关闭

像用户之前所用到的printf、scanf、getchar、putchar等函数都是使用的标准文件流,其中printf和putchar是通过标准输出流向控制台窗口打印输出数据,而scanf和getchar是通过标准输入流获取由键盘所输入的数据。标准输入流和标准输出流是带缓冲的文件流,数据在文件端和程序端之间进行传输时,需经过缓冲处理,即先将数据存入缓冲区,当缓冲区满了或是强制刷新缓冲区时,数据才会真正地到达目的端
在这里插入图片描述
使用缓冲区的好处是能够提高程序效率,要知道对(存储在硬盘上)文件的读写效率是远远低于对内存的读写效率的。因此,在读取文件时,可以先从文件中获取特定量的数据放入缓冲区中,程序再从缓冲区里读取数据;同样地,在写入文件时,先将数据写入缓冲区中,当缓冲区满或强制刷新时,再将缓冲区中的数据一次性写入文件,这样就大大地减少了对(存储在硬盘上)文件的读写次数,从而提高程序的运行效率

标准错误输出流用于程序发生错误或特殊情形发生时,能够打印输出相关信息。标准错误输出流是一种不带缓冲的输出流,这样做的目的,是为了不受缓区影响,能够及时地将信息打印显示出来

文件流的重定向

在C语言中,还可以使用文件流重定向的方式来改变文件流所关联的文件。这就需要使用freopen函数,函数原型如下

FILE *freopen(const char *fname,const char *mode,FILE *stream);

freopen函数的前两个参数和fopen函数的参数意义相同,第三个参数stream,即是需要重定向的文件流。freopen函数的返回值为重定向之后的新的文件流,如果重定向失败则为空指针。freopen函数的功能就是将文件流stream关联到以mode模式打开的fname文件上

可以将标准输出流重定向,让其关联到D盘的test.txt文件上。这样再使用printf、putchar、puts等函数进行打印输出,就不会再显示到控制台窗口,而是写入到D盘的test.txt文件中

freopen("D:\\test.txt","w",stdout);printf("Hello");putchar(' ');printf("World");

在freopen语句中,第三个参数是stdout,表示需要重定向的是标准输出流,而第一个参数字符串指定了所关联的文件,第二个参数字符串指定了所关联文件的打开模式,由于输出流是往文件中写入数据,所以,这里使用的是w模式

编译运行程序,会发现在控制台窗口上无任何信息。打开D盘上的test.txt文件,可看到有如下内容

Hello World

也可以将标准输入流重定向,让其关联到D盘的test.txt文件上。这样再使用scanf、gettchar、gets等函数获取数据的时候,就不会再到控制台窗口上去获取由键盘输入的数据,而是到了D盘的test.txt文件中去获取数据

char ch1,ch2;char str[100];freopen("D:\\test.txt","r",stdin);scanf("%c",&ch1);ch2 = getchar();gets(str);printf("ch1:%c\nch2:%c\nstr:%s\n",ch1,ch2,str);

结果如下

ch1:Hch2:estr:llo World

通过对标准输入流的重定向之后,scanf、getchar和gets函数都变为从D盘的test.txt文件获取数据。其中由scanf函数获取了第一个字符H保存到变量ch1中,由getchar函数获取了第2个字符e保存到变量ch2中,由gets函数获取剩余的字符,然后组成字符串llo World保存到字符数组中

文件的读写

以字符的方式读写文件

对于文本文件来说,可以使用C语言标准库中提供的fgetc函数和fputc函数,以字符的方式对文件进行读写操作

fputc函数是以字符的方式向文件中输出(写入)数据。函数原型如下

int fputc(int ch,FILE *stream);

函数有两个参数,第一个参数ch为欲写入文件的字符,第二个参数stream为欲写入文件相关联的文件流。函数的功能就是将参数ch所表示的字符写入文件流stream所关联的文件中。函数返回值为所写入的字符,如果出错,返回EOF

#include int main(){char str[] = "Red apple";FILE *pfile = fopen("D:\\test.txt","w");if(pfile){char *ptmp = str;while(*ptmp){fputc(*ptmp,pfile);++ptmp;}fclose(pfile);puts("Write to complete.");}elseputs("File opening failed.");return 0;}

程序代码中,通过临时指针ptmp的移动来遍历字符数组中的字符串各字符,如果是非空字符,就通过fputc函数,将其写入到文件流pfile所关联的文件中,直至ptmp指向字符串末尾的空字符时止

需要注意的是,==在对文件处理完毕后,应养成及时关闭文件的好习惯。==因为在程序对文件进行读写时会使用缓冲区,向文件写入数据,其实是将数据写入到缓冲区中,只有在缓冲区满或是强制刷新的情况下,数据才会从缓冲区写到文件中。如果发生程序异常、崩溃或是突然断电等情况,可能会存在缓冲区中数据未写入文件的情况,造成数据的丢失

刷新缓冲区的函数为fflush,函数原型如下

int fflush(FILE *stream);

该函数对参数stream所对应的输出缓冲区进行刷新,将输出缓冲区中的数据强制写入文件。函数执行成功返回0,如果出错,则返回EOF

在完成对文件的数据写入后,我们可以调用fflush函数来刷新输出缓冲区,将数据真正地写入到文件中

if(pfile){char *ptmp = str;while(*ptmp){fputc(*ptmp,pfile);++ptmp;}fflush(pfile);//刷新输出缓冲区fclose(pfile);//关闭文件puts("Write to complete.");}

在关闭文件之前调用fflush函数,刷新pfile所对应的输出缓冲区,将数据强制写入D盘的test.txt文件中

其实,在fclose函数中也会隐含地调用fflush函数来刷新输出缓冲区,所以,当调用fclose函数对文件进行关闭时,也间接地达到了刷新输出缓冲区的目的。因此,在这儿是可以省略掉fflush函数的语句。但千万别既不用fflush显式地刷新缓冲区,也不使用fclose来隐含地刷新缓冲区

fgetc函数是以字符的方式从文件中读取数据,函数原型如下

int fgetc(FILE *stream);

fgetc函数的参数stream应该是一个文件输入流,函数的功能就是从stream相关联的文件中读取一个字符作为函数的返回值。若是读到了文件末尾,或是发生了错误,则返回EOF。EOF和用作字符串末尾结束标记的空字符有点类似,空字符用于标记字符串的末尾,而EOF用于标记文件的末尾

编写程序,以字符的方式读取D盘test.txt文件中的内容,将其存储到字符数组中,并打印输出

#include int main(){char buf[128];FILE *pfile = fopen("D:\\test.txt","r");if(pfile){char *p = buf;while((*p = fgetc(pfile)) != EOF)++p;*p = '\0';fclose(pfile);printf("The read content is: %s\n",buf);}elseputs("File opening failed.");return 0;}

在while循环的条件检测处,首先使用fgetc函数从文件中读取字符,并通过对指针p的解引用将字符存储到字符数组中,并判断读取到的字符是否为EOF字符。若非EOF字符,则移动指针,进行下个字符的读取与存储,若为EOF字符,则终止while循环。需要注意的是,由于赋值运算符的优先级低于关系运算符,因此,需要用小括号来提升赋值表达式的优先级

在while循环之后,通过指针p的解引用,将最后存储在字符数组中的EOF字符修改为空字符,以作为字符串的结束标记

编译运行程序,结果如下

The read content is: Red apple

以行的方式读写文件

可以使用fgets函数和fputs函数,非常方便地对文本文件进行一行字符的读取或写入

fputs函数用于向文件输出(写入)一行字符,函数原型为

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

参数str是一个字符串的指针,即将所要写入文件的一行字符看作为一个字符串,str为该字符串的首地址;参数stream为与所写入文件相关联的输出流。函数执行成功返回非负值,执行失败则返回EOF

#include int main(){FILE *pfile = fopen("D:\\test.txt","w");if(pfile){if(fputs("Red appple",pfile) != EOF)printf("File written successful.\n");elseprintf(Failed to write file.\n);fclose(pfile);}elseprintf("File opening failed.\n");return 0;}

使用了fputs函数,不再是逐字符地写入,而是将一行字符一次性地写入文件

编译运行程序,正常情况下,写入文件成功,会打印如下结果

File written successful.

和fputs函数相对应的就是fgets函数,函数原型如下

char *fgets(char *str,int num,FILE *stream);

fgets函数可以从文件中读取一行或指定数量的字符,组成字符串存储到指定的数组或内存空间中。参数str是数组或内存空间的首地址,参数num是数组或内存空间的大小,参数stream是与文件相关联的输入流。函数执行成功返回str,若执行失败返回NULL

编写程序,以行的方式读取D盘test.txt文件中的内容,并打印输出

#include int main(){char buf[128];FILE *pfile = fopen("D:\\test.txt","r");if(pfile){if(fgets(buf,128,pfile))printf("The read content is: %s\n",buf);elseprintf("Failed to read file.\n");fclose(pfile);}elseprintf("File opening failed.\n");return 0;}

代码中,定义了一个长度为128的字符数组buf,作为从文件读取字符的存储区域。在fgets函数的调用语句中,第一个参数为buf数组名,第二个参数为buf的大小128.这表示最多可以从文件中读取127个字符,并添加一个空字符,作为字符串的结束标记,存储到buf数组中

结果

The read content is: Red apple

fgets函数在文件中读取字符时,并不会总是固定地读取127个字符,当遇到换行字符或读到文件末尾(EOF)时,就会停止读取
fputs函数在向文件输出字符时,只会输出字符串本身所包含的字符,并不会像puts函数那样,在输出完字符串后再自动加上一个换行符

以格式化的方式读写文件

前面的两种文件的读写方式,适合于字符和字符串的处理,但若遇到数值类型的数据,就可以用C标准库中提供的fprintf函数和fscanf函数,可以对文件进行格式化方式的读写。它们像printf函数和scanf函数一样,可以处理多种类型的数据

fprintf函数的函数原型为

int fprintf(FILE *stream,const char *format,...);

fprintf函数比printf函数多一个参数stream,即与文件相关联的输出流。printf函数可以将格式化后的字符串打印输出到控制台窗口,而fprintf函数是将格式化后的字符串打印输出到与stream相关联的文件中。fprintf函数的返回值为所打印输出的字符数,如果函数执行出错,则返回一个负整数值

假若有如下的一个学生的结构体

struct STU{int num;char name[20];float score;};

接下来,在主函数中定义该结构体类型的变量并进行初始化。然后就可以通过fprintf函数将结构体变量的内容写入到文件中

int main(){struct STU stu = {100,"zhangsan",90.5};FILE *pfile = fopen("D:\\test.txt","w");if(pfile){fprintf(pfile,"%d %s %.2f",stu.num,stu.name,stu.score);fclose(pfile);}elseprintf("File opening failed.\n");return 0;}

程序代码中,通过fprintf函数,将结构体变量stu的3个成员值写入到D盘test.txt文件中,各成员数据之间使用空格隔开。把数据隔开是为了后面能够方便地读取

编译运行程序,在控制台窗口不会显示任何信息,但打开D盘上的test.txt文件,会有以下内容

100 zhangsan 90.50

可见,fprintf函数会将格式化后的字符串输出到指定的文件中

还有一个和fprintf函数非常类似的函数,函数名为sprintf,它可以将格式化后的字符串输出到一个指定的内存空间中,用户可以非常方便地通过它来将一些数值类型的数据转换为字符串
sprintf函数的原型如下

int sprintf(char *buffer,const char *format,...)

和fprintf函数的唯一区别就是第一个参数buffer,它并非一个文件输出流,而是一个字符类型的指针,指向一块可以存储字符串的内存空间,通常称这块内存空间为缓冲区。可以在上面的代码中再定义一个字符数组,并将fprintf函数换成sprintf函数,这样就可以将结构体变量stu中的数据写入到字符数组中

int main(){struct STU stu = {100,"zhangsan",90.5};char buf[1024];sprintf(buf,"%d %s %.2f",stu.num,stu.name,stu.score);puts(buf);return 0;}

编译运行程序,结果如下

100 zhangsan 90.50

fscanf函数和scanf函数非常相似,唯一不同的是,fscanf函数也多出了一个参数,是和文件相关联的输入流,表示从文件中读取数据。fscanf函数的原型如下

int fscanf(FILE *stream,const char *format,...);

使用fscanf函数来对D盘上的test.txt文件进行数据读取,程序代码如下

int main(){struct STU stu;FILE *pfile = fopen("D:\\test.txt","r");if(pfile){fscanf(pfile,"%d%s%f",&stu.num,stu.name,&stu.score);fclose(pfile);printf("num:%d\nname:%s\nscore:%.2f\n",stu.num,stu.name,stu.score);}elseprintf("File opening failed.\n");return 0;}

首先定义一个结构体变量stu,然后通过fscanf函数读取文件数据到变量stu的3个成员,最后通过printf函数将结构体变量stu的所有成员打印输出到控制台窗口

结果

num:100name:zhangsanscore:90.50

以块的方式读写文件

通过格式化的方式读写文件,可以很方便地在文件中处理像结构体这种类型的数据。下面再来介绍块方式读写文件的函数,它可以对二进制文件进行读写,对于在文件中处理结构体、数组等类型的数据非常适合

块的写入函数fwrite的函数原型如下

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

fwrite函数有4个参数,其中buffer为所要写入数据的首地址,即指向写入数据的指针;size为数据块的大小;count为数据块的数量;stream为与文件相关联的输出流。fwrite函数的功能就是将buffer所指向的count个size大小的数据块写入到stream相关联的文件中。函数的返回值为所写入文件的数据块的数量,正常情况下,应该为参数count的值

假设有一个结构体STU类型的变量,现在想将其写入到D盘的test.txt文件,就可以这样使用fwrite函数,参数size为结构体变量的大小,而参数count为1,即将一个结构体变量大小的数据块写入到D盘test.txt文件

#include struct STU{int num;char name[20];float score;};int main(){struct STU stu = {100,"zhangsan",90.5};FILE *pfile = fopen("D:\\test.dat","wb");if(pfile){fwrite(&stu,sizeof(stu),1,pfile);fclose(pfile);}elseprintf("File opening failed.\n");return 0;}

fopen函数中,打开文件的模式字符串为wb,即以二进制写入的模式打开文件。在fwrite函数中,第一个参数为结构体变量stu的内存地址,第二个参数为一个数据块的大小,即结构体变量stu的大小,第三个参数为数据块的数量,这里为1,第四个参数为与D盘test.dat文件相关联的输出流

编译运行该程序,正常情况下,在控制台窗口没有任何信息,但打开计算机上的D盘,会发现一个名为test.dat的文件。以文本的方式打开该文件,会看到许多乱码,这是因为test.dat并非文本文件,而是一个二进制文件,即存储在文件中的并非字符的ASCII码,因而无法转换为可读的字符

fread函数,它可以以数据块的方式从文件中读取数据。fread函数的原型如下

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

fread函数的功能为从stream相关联的文件中读取num个size大小的数据块,存储到buffer所指向的内存空间中。fread函数的返回值为所读取到的数据块的个数,正常情况下,应为参数num的值

下面使用fread函数,从D盘test.dat文件读取数据,并在控制台窗口上打印结果

#include struct STU{int num;char name[20];float score;};int main(){struct STU stu;FILE *pfile = fopen("D:\\test.dat","rb");if(pfile){fread(&stu,sizeof(stu),1,pfile);fclose(pfile);printf("num:%d\nname:%s\nscore:%.2f\n",stu.num,stu.name,stu.score);}elseprintf("File opening failed.\n");return 0;}

在打开D盘test.dat文件时,采用的是rb模式,即以二进制读取的模式打开文件。在fread函数中,数据块大小为结构体变量stu的大小,数据块的数量为1,将读取到的数据存储至结构体变量stu中

结果

num:100name:zhangsanscore:90.50

在这里插入图片描述

湖北工具网