文件IO学习【二】

文件操作接口说明

Linux系统为了简化不同类型文件的操作流程,在设计访问接口时也遵循POSIX标准,而POSIX标准就是对不同操作系统的访问接口做出统一的规范目的是提高程序的兼容性和可移植性。

大家经常使用的C语言同样具有语法标准,并且C语言标准在发布的时候也会发布对应的库函数提供给用户。这些库函数也同样遵循POSIX标准进行设计,而遵循POSIX标准设计出来的函数的集合也被称为标准库,比如大家使用的标准C库中提供了标准的输入输出函数,这些函数在Linux系统可以使用,同样也可以在Windows系统中使用。用户可以根据标准输入输出头文件<stdio.h>中的函数声明进行调用,Linux系统下该头文件路径为 /user/include。

另外,由于任何一种操作系统都会有访问磁盘文件的需求,所以POSIX标准中同样对访问文件的输入输出接口做出了约束,这些访问文件的函数接口在C语言标准中都有具体的描述。

标准IO

标准C库中关于文件输入输出的函数接口一般被称为标准IO,访问文件常用的标准IO函数有fopen()、fread()、fwrite()、fclose()、fgetc()、fputc()、fgets()、fputs()、fprintf()、fscanf()等。

标准IO函数介绍

打开文件:fopen()

想要对文件进行读写访问的前提是必须先打开文件,标准IO中提供了一个函数叫做fopen(),用户只需要包含标准输入输出头文件 #include <stdio.h> 即可调用。

如上图所示,调用fopen时需要传入两个参数,前者为即将要打开的文件,格式为 “xxx.c”,在未标明路径的前提下,默认打开当前路径下的文件,若是想要打开别的路径下文件,需要加上路径名,格式为“/demo/demo.c”;后者为需要以怎样的方式打开该文件,具体分类如下图:

fopen函数是有返回值的,如果文件打开成功,则返回值返回指向该文件的文件流指针,如果文件打开失败,则返回值为NULL。

fopen使用相关知识补充
  • 为什么某些情况下,打开的文件大小与Linux内储存文件大小不同?

原因:因为在使用上图mode打开文件时,默认是将文件以文本(.txt)形式打开,该打开过程中,系统会对文件内容进行解释转换,最终导致两个文件大小不一致

解决办法:在C99标准中,提供了几个mode,其与上图mode的区别在于,打开文件时是以二进制形式打开,此时打开方式与linux一致,便不会出现上图中文件大小不一致的情况。其他特性与不加b的mode保持一致。

注意:

  1. 多出来的mode只在C99后标准有效,在C89标准中,使用无效,系统还是会按照文本形式打开文件。
  2. 使用"a"与”a+“打开文件时,光标会被定位至文件末尾,而其余模式,光标则是会被定位至文件开头。
  • fopen函数的返回值是一个指向被打开文件的FILE类型的指针,请问FILE类型是什么?

回答:

FILE类型其实是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,比如包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。头文件stdio.h中有关于FILE类型的相关描述,如:

  • 可以看到FILE类型其实就是一个结构体,结构体类型名称为struct _IO_FILE,但是经过查找之后发现头文件stdio.h中并没有关于该结构体的定义,那这个结构体中到底都有哪些成员?

回答:阅读stdio.h中的条件编译选项可以发现在stdio.h中还包含了另一个头文件<libio.h>,这个头文件中才有关于FILE结构体类型的定义,该头文件的路径同样在Linux系统的/user/include目录下。

可以看到FILE结构体类型中有一个成员是FILE类型的指针变量chain,该指针可以指向下一个被打开文件的文件信息区,也就是可以把FILE类型当做数据结构中的链表的结点,结点中除了可以存储数据域之外,还可以利用指针域存储下一个结点的地址。

简单理解:用户可以在一个程序中利用fopen函数打开多个文件,每次打开一个文件,内核就会从*堆内存*中申请一块FILE结构体大小的空间用来存储文件的所有信息,然后按照文件打开的顺序把每个打开的文件的结构体形成一条链表,然后使用链表头进行管理。

注意:打开文件的目的无非就是对文件进行读写操作,所以每次当程序运行的时候已经有三个文件流被打开分别是标准输入stdin、标准输出stdout、标准出错stderr,这三者在stdio.h中也是FILE指针。

所以内核在管理被打开文件的时候,链表中已经有三个结点存在,然后再把新节点头插入到链表中。

  • 请问为什么内核在为文件流申请内存的时候是申请的堆内存?请问有什么具体依据?

回答:如上图所示,当我们对打开的一个文件进行两次关闭时,系统在执行时会报不能两次释放该内存的错误,我们可知,fclose实际上是间接调用了free函数进行文件的关闭,侧面验证了内核为文件流申请内存时申请的是堆内存。

注意:

  1. 使用标准IO的时候,是不可以反复关闭相同的文件,因为释放已经被释放的堆内存,会导致段错误!!
  2. 但是可以反复打开同一文件,只不过申请的堆内存的地址是在变化的,且关闭时需要一一对应关闭,即打开几次就需要关闭几次。

关闭文件:fclose

利用fopen()打开文件之后内核会申请一块堆内存用来存储文件信息,申请的堆内存大小就是FILE结构体类型的大小,那么如果用户完成了对文件的读写访问之后,则需要利用fclose()函数来关闭文件,这样这块堆内存就会被内核先从链表中删除,然后再释放掉。

读取数据

用户打开文件后可以从文件中读取数据,标准C库中提供了多个读取函数来满足用户的不同需求,这些函数大体分为三类:字符读取(fgetc)、按行读取(fgets)、按块读取(fread)。

字符读取(fgetc)

标准库中提供了一个fgetc函数,通过C99标准可以知道该函数的作用是从文件指针stream指向的文件中读取一个字符,并在读取一个字节后把文件的光标位置向后移一个字节,然后把读取到的字符所对应的ASCII码通过返回值返回。

在调用该函数时如果文件的光标已经到达文件末尾或者遇到读取错误时,则函数会返回EOFEOF是文件结束标志,其实是个宏定义,宏定义的值为 -1,在头文件libio.h中有相关描述。

另外,在标准库中还提供了另一个*函数getc()*,这个函数的作用等效于fgetc()函数,只不过getc()函数的实现是利用宏定义而已。二者的作用是一致的,总体上来说这两个函数是等价的,但是fgetc函数的使用频率会更高。

而还有一个函数可以完成读取字符的工作---getchar(),但是该函数相较于fgetc()和getc()来说,存在局限性,getchar()函数只能从stdin(标准输入)中读取一个字符。

注意:

  • 某种特殊情况下,三个函数的作用一致,如:

getchar() == fgetc(stdin) == getc(stdin)

  • 当读取数据失败时,我们无法通过返回值判断是到达文件末尾还是遇到了错误

练习:在本地磁盘打开一个存储少量数据的文本demo.txt,利用fgetc函数把文本中的字符输出到屏幕,当文本中所有字符都输出完成后就结束程序。

按行读取

标准库中提供了一个*fgets*函数,通过C99标准可以知道该函数的作用是从文件指针stream指向的文件中读取一行字符,并把读取的字符存储在指针s所指向的字符串内,n为自定义的缓冲区大小,FILE *为需要读取的目标文件。读取成功后,返回自定义缓冲区指针s,读取失败时,返回NULL;

fgets读取结束情况:

  • 当读取到n-1个字符时
  • 已经读取到文件末尾(EOF)
  • 读取到换行符’\n’时

**思考: ** 为什么fgets函数读取到换行符\n时会结束?fgets函数中的参数n的意义是什么??

回答:用户调用fopen打开文件之后,可以把数据写入到文件中以及从文件中读取数据,但是实现读取和写入的过程中其实内核并没有直接操作文件,而是在操作指向文件的结构体指针FILE,也就是用户写入的数据和读取的数据会先存储在FILE结构体的*缓冲区*当用户调用刷新缓冲区的函数或者其他读写函数时,FILE结构体的缓冲区会被刷新,数据才会被系统写入文件。

可以看到,每当使用标准IO的读操作函数,试图将数据从文件 a.txt读取出来时,数据都会流过标准*输入**缓冲区*,然后再在适当的时刻冲洗(或称刷新,flush)到内核缓冲区,最后才真正得到数据。

思考:什么是缓冲区?为什么要有缓冲区?

缓冲区的出现其实就是由于输入设备和输出设备对于数据的读写速度比较慢,其实就是CPU为了降低输入输出次数,目的是为了提高运行效率,避免长时间的等待,所以内核就在内存中提供了一块空间作为缓冲区,缓冲区也可以称为缓存(Cache),是属于内存空间的一部分。

根据IO设备的不同,可以把缓冲区分为输入缓冲区和输出缓冲区【也可以叫做读缓存区和写缓冲区】,同样,根据刷新形式的不同,可以把缓冲区分为三种:全缓冲、行缓冲、无缓冲。

  • 全缓冲:指的是当缓冲区被填满就立即把数据冲刷到文件、或者在关闭文件、读取文件内容以及修改缓冲区类型时也会立即把数据冲刷到文件,一般读写文件的时候会采用
  • 无缓冲:指的是没有缓冲区,直接输出,一般linux系统的标准出错stderr就是采用无缓冲,这样可以把错误信息直接输出。
  • 行缓冲:指的是当缓冲区被填满(一般缓冲区为4KB,就是4096字节)或者缓冲区中遇到换行符’\n’时,或者在关闭文件、读取文件内容以及修改缓冲区类型时也会立即把数据冲刷到文件中,一般操作IO设备时会采用,比如printf函数就是采用行缓冲。

当然,全缓冲和行缓冲除了以上几种情况外,当程序结束时缓冲区也会被刷新,另外,也可以采用函数库中的fflush函数手动刷新缓冲区。

注意:

缓冲类型 全缓冲 无缓冲 行缓冲
例子 普通文件 stderr(标准出错) stdout(标准输出)
按块读取

标准库中提供了一个fread函数,通过C99标准可以知道该函数的作用是从给定的文件输入流stream中读取最多nmemb个对象到指针ptr指向的字符串中,每个对象的大小为size字节,函数返回成功读取的对象个数,若出现错误或到达文件末尾,则可能小于nmemb。即读取是否成功需要拿返回值与预计值进行比较。

注意:若size或nmemb为零,则fread函数返回0且不进行其他动作。但是这样使用并不会报错,只是意义而已。

思考:可以知道函数的返回值如果小于nmemb则说明可能出现读取错误或者到达文件末尾,那应该如何区分这两种情况?

回答:可以通过标准库中提供的两个函数区分,一个函数是feof(),另一个则是ferror函数。

注意:

feof 函数在C语言中用于检测文件结束标志是否设置。但是, feof 的行为可能会让人有些误解,因为它并不直接检测文件是否已到达未尾。相反,它检测的是在上一次调用文件读取函数(如 fgetc、fread 等)时是否遇到了文件结束(EOF)标记

也就是说,feof并不是通过此时光标所在位置来判断是否到达文件末尾,所以并不能通过结合使用fseek函数来判断是否到达文件末尾。

具体来说,feof 的工作原理是这样的:
1.当你尝试读取一个文件时,如果文件尚未到达末尾,feof 将返回0(假)
2.当你读取到文件的末尾时,并不会立即设置文件结束标志。相反,当你尝试再次读取(即超过文件的未尾)时,文件结束标志会被设置,并且此时 feof 将返回非0值(真)。
3.如果你在读取文件末尾后没有再次尝试读取,那么 feof 仍然会返回0(假),!即使文件实际上已经读取完毕。

因此,在使用 feof 时,,一个常见的做法是在一个循环中读取文件,并在循环结束后检查 feof 的值来确定是否已到达文件末尾。但是,请请注意,如果文件读取操作因为其他原因(如磁盘错误、权限问题等)而失败,feof 也可能返回非0值。因此,通常还需要检查 ferror 函数来确定是否发生了错误。

写入文件

字符写入

注意:

​ 特殊情况下,三种函数作用一致,如:

putchar(a) == fputc(a, stdout) == putc(a, stdout)

字符串写入

注意:

  • 字符串写入时,fputs和puts均遇到'\0'便会结束写入
  • puts函数拥有着自动换行自动刷新缓冲区的特性
按块写入

与按块读取函数fread特性大体一致,均是依靠返回值与目标值比较来判断是否写入成功,且若size或nmemb为零,则fread函数返回0且不进行其他动作。但是这样使用并不会报错,只是意义而已。

读取文件位置

每个被打开文件的结构体中都有一个位置指示器(简单理解:位置指示器是文件光标),注意:被打开的文件的光标默认是在文件开头的。除非打开的模式是“a"或者"a+"。

设置位移

注意:

该设置光标位置可以灵活使用,来满足当前需要的条件。如:

当我们利用模式“a”打开文件后,又需要将光标偏移至文件首部,则可以利用 fseek(p,0,SEEK_SET)指令

获取位移

注意:

该函数返回的文件位置偏移量是相对于文件开头来说的

练习:

要求利用标准IO函数接口实现计算一个本地磁盘某个文件的大小,要求文件名称通过命令行进行传递,并进行验证是否正确( ls -l)。

格式访问

标准库中除了以上关于文件读写的函数之外,还提供了一些可以对文件进行格式化读写的函数接口,在C99标准中有关于这些函数的描述,如下:

注意:一般常用的关于文件IO的格式化函数有printf、fprintf、scanf、fscanf、sprintf、snprintf。

热门相关:末日之最终战争   神级幸运星   无敌天下   绝天武帝   我真不是学神