在学习和分析标准I/O库的同时, 可以重点与Linux的I/O系统调用进行比较。
stdin、 stdout和stderr都是FILE类型的文件指针, 是由C库静态定义的, 直接与文件描述符0、 1和2相关联, 所以应用程序可以直接使用它们。其中,stdin是不可写的, stdout是不可读的, 而stderr不仅不可读, 且没有缓存。
I/O的缓存
C库的I/O接口对文件I/O进行了封装, 为了提高性能, 其引入了缓存机制, 共有三种缓存机制: 全缓存、 行缓存及无缓存。
(1)全缓存一般用于访问真正的磁盘文件。 C库会为文件访问申请一块内存, 只有当文件内容将缓存填满或执行冲刷函数flush时, C库才会将缓存内容写入内核中。
(2)行缓存一般用于访问终端。 当遇到一个换行符时, 就会引发真正的I/O操作。 需要注意的是, C库的行缓存也是固定大小的。 因此, 当缓存已满, 即使没有换行符时也会引发I/O操作。
(3)无缓存, 顾名思义, C库没有进行任何的缓存。 任何C库的I/O调用都会引发实际的I/O操作。
下面看一个行缓存的例子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 int main(void) 5 { 6 printf("Hello "); 7 if (0 == fork()) { 8 printf("child "); 9 return 0; 10 } 11 printf("parent "); 12 return 0; 13 }
执行结果如下:
之所以是这样的结果, 就是因为背后的行缓存。 执行printf( "Hello") 时, 因为printf是向标准输出打印的, 因此使用的是行缓存。 字符串Hello没有换行符, 所以并没有真正的I/O输出。 当执行fork时,子进程会完全复制父进程的内存空间, 因此字符串Hello也存在于子进程的行缓存中。 故而最后的输出结果中, 无论是父进程还是子进程都有Hello字符串。
fopen函数
1 # include<stdio.h> 2 FILE * fopen(const char * path,const char * mode)
参数说明:
(1)path:需要打开的文件路径
(2)mode:文件打开方式,主要对应下表
mode | function |
---|---|
r | 以只读方式打开文件,该文件必须存在。 |
r+ | 以可读写方式打开文件,该文件必须存在。 |
rb+ | 读写打开一个二进制文件,允许读数据。 |
rt+ | 读写打开一个文本文件,允许读和写。 |
w | 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。 |
w+ | 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。 |
a | 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留) |
a+ | 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留) |
wb | 只写打开或新建一个二进制文件;只允许写数据。 |
wb+ | 读写打开或建立一个二进制文件,允许读和写。 |
wt+ | 读写打开或着建立一个文本文件;允许读写。 |
at+ | 读写打开一个文本文件,允许读或在文本末追加数据 |
ab+ | 读写打开一个二进制文件,允许读或在文件末追加数据。 |
fdopen函数
1 #include <stdio.h> 2 FILE *fdopen(int fd, const char *mode);
fdopen用于从文件描述符fd生成一个文件流FILE
fileno函数
1 #include <stdio.h> 2 int fileno(FILE *stream);
fileno则用于从文件流FILE得到对应的文件描述符
无论是fdopen还是fileno, 关闭文件时, 都要使用fclose来关闭文件, 而不是用close。 因为只有采用此方式, fclose作为C库函数, 才会释放文件流FILE占用的内存。
fread函数
1 size_t fread(void *buffer, size_t size, size_t count, FILE *file);
参数说明:
(1)buffer是读取数据后存放地址
(2)size是的块长度
(3)count是块的数量,实际读取长度为size*count,返回值为块成功读取块的count数量
fwrite函数
1 size_t fwrite(const void *buffer, size_t size, size_t count, FILE *file);
参数说明:
(1)buffer是写入数据后存放地址
(2)size是的块长度
(3)count是块的数量,实际读取长度为size*count,返回值为块成功写入块的count数量
下面看一个读写文件的例子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 int main(void) 5 { 6 char buf[20]; 7 int ret; 8 FILE *fp = fopen("./tmp.txt", "w+"); 9 if (!fp) { 10 printf("Fail to open file "); 11 return -1; 12 } 13 ret = fwrite("123", sizeof("123"), 1, fp); 14 printf("we write %d member ", ret); 15 memset(buf, 0, sizeof(buf)); 16 ret = fread(buf, 1, 1, fp); 17 printf("We read %s, ret is %d ", buf, ret); 18 fwrite("456", sizeof("456"), 1, fp); 19 fclose(fp); 20 return 0; 21 }
运行结果:
为什么fread什么都没有读取到, 返回值是0呢? 因为每一次系统调用的read和write成功返回后, 文件的偏移量都会被更新 。fwrite和fread操作的是同一个文件指针fp, 也就是对应的是同一个文件描述符。 第一次fwrite后, 在tmp.txt中写入了字符串“123”, 同时文件偏移为3, 也就是到了文件尾。 进行fread操作时, 既然操作的是同一个文件描述符, 自然会共享同一个文件偏移, 那么, 从文件尾自然读取不到任何数据。
ferror函数
1 int ferror(FILE *stream);
若文件流出错则返回非0,否则返回0。ferror()函数常与clearerr()函数一起使用,如果ferror(fp)发现错误,返回一个非0值,那么调用clearerr(fp)后,ferror(fp)的值变为0。
当文件流读到文件尾时, 文件流会被设置上EOF标志。 即使不使用clearerr清除EOF标志, 有新的数据, 也可以读取成功,这是因为:文件流FILE的错误标志位只有在打开IO_DEBUG的情况下才会对后面的I/O调用产生影响: 在有错误标志位的时候, 后面的I/O调用都会直接返回EOF。 而一般情况下, IO_DEBUG这个宏是没有定义的。
getc函数
1 #include <stdio.h> 2 int fgetc(FILE *stream); 3 int getc(FILE *stream);
正常的情况下返回一个char,可以吗?
不可以,这些函数返回的字符是以unsigned char形式保存,然后转换为int的。因此,在int的值域里,有效的返回值总是一个正整数。例如,遇到文件结束时,返回宏EOF。通常EOF定义成-1,在int域里,-1是没有歧义的,我们不会迷惑到底是读进来了一个字符'0xff',还是遇到了EOF。而在char域里,这样的歧义是存在的。