不管用系统IO函数还是标准IO函数,操作文件的第一步,都是“打开(open/fopen)”文件,需要注意:
系统I/O通过文件描述符来操控文件,标准I/O中通过文件指针来操作文件的。
系统 I/O 通常是不缓冲的,每次读写操作都会直接与内核进行交互,提供更细粒度的控制。
标准 I/O 提供了简单易用的函数接口,并且通常会对文件进行缓冲,这样可以提高 I/O 性能。
文件指针 (FILE *
) 在 C 语言中并不指向实际的文件,而是指向一个由标准 I/O 库管理的 FILE
结构体。这个结构体包含了对实际文件操作所需的信息,包括文件描述符、缓冲区等。
注意:
注意:fclose函数涉及内存释放,不可对同一个文件多次关闭。
int main() { // 以可读可写的方式打开文件,且要求文件必须存在 FILE *fp = fopen("a.txt", "r+"); if(fp == NULL) { perror("fopen失败"); } // 关闭文件指针,并释放文件所关联的缓冲区内存 fclose(fp); }
这里我还是要强调一点,不仅要看一个函数如何使用,我们同时也要重点关注这个函数的返回值(包括返回值的类型,正确执行的返回值和错误执行的返回值),这里函数的返回值类型是文件指针类型,函数执行正确返回的是文件指针,函数执行错误返回的是NULL。
在之前讲系统I/O的时候,我们提到系统会默认为进程打开三个文件并且分配对应的文件描述符0,1,2
默认分配的文件描述符:
stdin
):文件描述符为 0
stdout
):文件描述符为 1
stderr
):文件描述符为 2
系统也会为这三个文件自动分配对应的文件指针,具体过程为:
FILE
类型的文件指针(stdin、stdout、stderr)。printf
、scanf
等)通过这些文件指针来调用底层的系统I/O操作,以此来达到操作文件的目的。下面我们会学习到这些使用文件指针的标准库函数。fopen和open分配的东西的区别
使用fopen
打开文件时,操作系统先为文件分配文件描述符,然后标准I/O库会将该文件描述符与一个FILE
类型的文件指针关联起来。
使用open
系统调用时,操作系统会为文件分配文件描述符,但不会自动分配FILE
类型的文件指针
fgetc/getc/getchar都是每次从文件流中读取单个字符
fgetc和getc是从我们指定的文件流中读取一个字符。getchar只能从标准输入流(stdin)读取一个字符(从键盘输入)。
fputc/putc/putchar都是每次向文件流中写入单个字符,与上面对应。
feof是通过传入文件指针来检验文件是否读取到文件结尾的一个函数,主要用在读取文件函数中。
ferror函数是用于检查文件读/写操作过程中是否发生了错误。
需要这两个函数的原因是:在文件读写的时候,遇到读写文件失败其实对应着多种情况,但是失败的多种情况对应的返回值是一样的,文件读到末尾和文件读取错误返回值有时候一样,就需要我们使用feof来判断是不是读取到文件结尾,区分究竟是哪种情况。
fgets/gets是从文件流中按行读取数据,需要我们手动定义一个缓冲区,用来存储每次读取到的数据。
fputs/puts是将一个字符串写入到指定的文件流中。
fgets函数有内存边界判断,更加安全,推荐使用,gets没有内存边界判断,不安全,不推荐使用。
通常我们配套使用fgets和fputs。使用fgets来将源文件按行写到自定义缓冲区中,使用fputs函数将缓冲区中的数据写入到目标文件中。
fread/fwrite函数是......
详细介绍在下一篇文章中
疑问:函数这里可以看到fgetc和fputc函数的返回值类型是int类型,但是我们函数返回的是实际上读取到的字符,那我们使用char类型作为返回值不就行了吗?为什么要使用int呢?
解释:因为这里在函数执行失败的时候会返回EOF(常常代表着-1),如果我们使用char类型的话存储负数会发生错误,所以使用了int类型。
现在我们要来用fgetc函数和fputc函数实现将一个文件中的内容复制到另一个文件。如果用getc和putc效果完全一样,但是不能使用getchar()和putchar()。
#include #include #include #include #include #include int main(int argc, char **argv){ if(argc!=3){ printf("你必须指定两个文件来保存程序的输出内容,这是一个复制文件内容程序\n"); exit(0); } //打开源文件 FILE *fp_src=fopen(argv[1],"r"); if(fp_src==NULL){ printf("打开%s文件失败%s\n",argv[1],strerror(errno)); exit(0); } //打开目标文件 FILE *fp_dst=fopen(argv[2],"w"); if(fp_dst==NULL){ printf("打开%s文件失败%s\n",argv[2],strerror(errno)); exit(0); } //复制文件内容 int c; while(1){ c=fgetc(fp_src); if(c==EOF){ if(feof(fp_src)){//到达文件尾 break; } if(ferror(fp_src)){//读取发生错误 perror("读取文件错误"); exit(0); } }else{ fputc(c,fp_dst); } } return 0; }
feof函数的全称是 file end-of-file
feof
和 ferror
是用于处理文件流错误和状态的函数,它们分别用于检查文件流的读取结束状态和错误状态,帮助我们在文件操作中更好地处理文件状态和错误,确保程序的健壮性。
feof
:用于检测文件读取时是否到达文件末尾。写操作的时候不用这个函数ferror
:用于检查文件读/写操作过程中是否发生了错误。对于读操作而言,读操作失败,此时对应有两种情况:文件读取结束和文件读取错误,为了区分这两种情况,我们需要使用feof函数和ferror函数帮助我们判断,需要传递的参数是文件指针。
如果 feof(fp) 为真,此时意味着读到了文件末尾,没有数据可读了。
如果 ferror(fp) 为真,此时意味着遇到了错误。
注意:
1. feof函数只会用在读操作中,ferror函数在读写操作中都会用到。
2.并不是只有那些返回 EOF
的读取函数才能使用 feof
进行判断,fgetc,fgets,fscanf,fread虽然返回值类型不太一样,但是都可以利用feof
来进行判断否到达文件末尾。也都可以利用ferror
来判断是否发生文件错误。都是传递文件指针就行。
feof
ferror
fgets() 和 gets() 都是按行读取文件数据,他们的区别是:
fputs() 和 puts() 都是按行将数据写入文件,他们的区别是:
我们在选择读写文件的函数时,推荐使用配对的函数,这样读取文件函数与写入文件函数对应的实现细节一样,不容易出现错误。比如推荐fgets和fputs函数一起使用,不推荐fgets和puts函数一起使用。
fgets/fputs这对函数在使用的时候和我们的系统I/O中的read/write有些相似。这里也需要我们来手动定义一个缓冲区,其他地方区别很多。
返回值 fgets函数返回值是指针类型,执行成功时返回指向我们缓冲区的指针。执行失败时返回NULL。如果在读取过程中到达文件的结束位置,fgets
会返回 NULL
。读取过程中发生错误(例如 I/O 错误),fgets
也会返回 NULL
。
这个时候我们如何来判断究竟是哪种呢?当然是用feof函数和ferror函数啦。只需要我们将文件指针传递给这两个函数,那么问题便迎刃而解了。
fputs函数返回值是int类型,主要用来指示函数调用的成功与否,而不是表示实际写入的字节数
成功时返回一个非负整数,失败时返回 EOF
(通常是 -1
)
函数参数 fgets函数有三个参数,缓冲区指针,缓冲区大小,源文件指针。
fputs函数只有两个参数,分别是缓冲区指针和目标文件指针。没有指定字节数的参数
//省略前面打开源文件和目标文件代码, //fp_src为源文件指针,fp_dst为目标文件指针 char buf[100]; while(1) { bzero(buf, 100); if(fgets(buf, 100, fp_src) == NULL) { if(feof(fp_src)) { break; } if(ferror(fp_src)) { perror("fgets() failed"); break; } } fputc(buf, fp_dst); }
fgets
是按行读取的,每次读取受到自定义缓冲区大小n的限制。每次最多会读取 n-1
个字符,因为fgets
函数会在读取的字符串末尾添加一个空字符 '\0',确保返回的是一个以 null 结尾的字符串。。一次读取结束的条件:读取到文件结束符/读取够了 n-1
个字符/遇到换行符,本次读取结束。前两个我们可以理解,我们主要来解释一下第三点:
如果 fgets
读取到了换行符,换行符会存储在缓冲区中,并且紧随其后是空字符 \0
作为字符串的结束标志。即使缓冲区还有剩余的空间,fgets
也不会继续读取数据。这样的话我们可以非常自然地处理文本行的结束,这个特点正好对应了fgets是按行读取文件函数。
说一下我学习到这里的一个很大的疑问,说好的标准I/O与系统I/O相比,会使用标准库的缓冲区来提高文件操作效率,但是学习了fgerc/fputc和fgers/fputs之后,我们貌似没有感受到缓冲区的存在和其发挥的作用(标准库缓冲区不是自定义缓冲区,不要混淆)。
我们首先要明确标准I/O中缓冲区并不会简化我们代码编写复杂程度,使用标准I/O和系统I/O代码量差不多。标准库缓冲区的作用是采取了在内存中缓存数据措施来减少我们从磁盘读写数据的次数,从而提高效率。
ps:磁盘 I/O 操作都涉及系统调用,这些系统调用具有较高的开销,缓冲区的使用可以将多个 I/O 操作合并成一次系统调用,减少了系统调用的次数和开销。
针对于读操作和写操作,我们系统缓冲区采用了不同措施
缓冲区的作用:
fgetc
、fgets
)时,数据首先从内存中的缓冲区中读取,而不是直接从源文件读取。如果缓冲区为空,标准库会从磁盘读取更多数据并填充缓冲区,然后再从缓冲区中返回数据。标准I/O每次读取会在缓冲区中提前放置很多源文件数据(即使这些数据可能还未被完全读取),方便后续fgetc
每次读取并不需要都调用系统I/O,本质上是减少了调用系统I/O的次数来提升效率。相当于你做饭需要用一根葱,但是你发现家里面没葱的时候会去菜市场买,你肯定不会只买一根,你肯定是买一把,因为每次跑过去很浪费时间,这样下次需要用葱的时候你就不需要跑到菜市场,综合下来我们是减少了跑到菜市场的次数,从而节省了时间。fputc
、fputs
)时,数据会首先被写入到标准库的缓冲区中,而不是立即写入目标文件。那什么时候写缓存区中的数据会被加载到目标文件呢?让我们来看写缓冲区的自动刷新机制1.当写缓冲区被填满时,标准库会自动将缓冲区中的数据写入到磁盘。2.调用 fflush
:显式调用 fflush
函数可以强制刷新写缓冲区,将缓冲区中的数据立即写入到磁盘,而不需要等待缓冲区满或文件关闭。3.文件关闭:当调用 fclose
函数关闭文件时,标准库会自动将缓冲区中的所有数据写入到磁盘,确保所有待写入的数据都被正确保存。上述机制可以减少实际的系统 I/O 调用次数,从而提高性能。
标准库文件缓冲区知识补充
标准库为文件流分配了缓冲区,通常这个缓冲区的大小是几 KB 到几十 KB。且每个文件流只有一个缓冲区。
那么上面显然读缓冲区的机制和写缓冲区的机制不一样,对一个文件采用读写方式打开岂不是会乱套?
通常来说我们对于一个文件只会使用只读或者只写的方式打开,当文件流处于只读模式时,缓冲区被用来存储从文件中读取的数据,被当作读缓冲区使用。当文件流处于只写模式时,缓冲区被用来缓存要写入该文件的数据,被当作写缓冲区使用。不推荐采用读写模式打开一个文件。
如果真的需要同时读取和写入同一个文件,推荐打开文件的两个独立的文件流,就有两个缓冲区了,一个用于读取,一个用于写入。这可以避免文件指针管理的复杂性,并减少潜在的错误。