六、读和写流
一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对其进行读、写操作:1)每次一个字符的I/O,一次读或写一个字符,如果刘时代缓冲的,则标准I/O函数处理所有缓冲;2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大行长。3)直接I/O(这个术语来自ISO C标准,有时也被称为二进制I/O,一次一个对象的I/O、面向记录的I/O或面向结构的I/O)。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数据的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件每次读或写一个结构。
1.输入函数
#include <stdio.h> int get(FILE *fp); int fgetc(FILE *fp); int getchar(void); //若成功,返回下一个字符;若已经到达文件尾端或出错,返回EOF
函数getchar等同于fgetc(stdin)。前两个函数的区别是getc可被实现为宏,而fgetc不能被事先未宏,这意味着以下几点区别:1)getc的参数不应当是具有副作用的表达式,因为他可能会被计算多次;2)因为fgetc一定是个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传送给另一个函数;3)调用fgetc所需时间很可能比调用getc要长,因为调用函数所需的时间通常长于调用宏。
需要注意的是不管是出错还是达到文件的尾端,这三个函数都返回同样的值,为了区分这两种不同的情况,必须调用ferror或feof,这是因为大多数实现中,为每个流在FILE对象中维护了两个标志:出错标志;文件结束标志。clearerr可以清除这两个标志。
#include <stdio.h> int gerror(FILE *fp); int feof(FILE *fp); //若条件为真,返回非0;否则,返回0 void clearerr(FILE *fp);
从流中读取数据以后,可以调用ungetc将字符再压送回流中。压送回流中的字符以后又可从流中读出,但读出字符的顺序与压送回的顺序相反。不能回送EOF,。但是当已经到达文件尾端时,仍可以回送一个字符。下次读将返回该自负,再读则返回EOF。之所以能这样做的原因是:一次成功的ungetc调用会清除该流的文件结束标志。用ungetc压送回字符时,并没有将他们写到底层文件中或设备上,知识将它们写会标准I/O库的流缓冲区中。
#include <stdio.h> int ungetc(int c, FILE *fp); //若成功,返回c;若出错,返回EOF
2、输出函数
对应于上面所述的每个输入函数都有一个输出函数:
#include <stdio.h> int putc(int c, FILE *fp); int fputc(int c, FILE *fp); int putchar(int c);
七、每次一行I/O
#include <stdio.h> char *fgets(char *restrict buf, int n, FILE *restrict fp); char *gets(char *restrict buf);
对于fgets,必须指定缓冲的长度n,次函数一直读到下一个换行符为止,但是不超过n-1个字符,读入的字符被送入缓冲去。该缓冲区以null字节结尾。如该行包括最后一个换行符的字符数超过n-1,则只返回一个不完整的行,但是,缓冲区总是yinull字节结尾。对fgets的下一次调用会继续该执行。gets不推荐使用,可能造成缓冲区溢出。
#include <stdio.h> int fputs(const char *restrict str, FILE *restrict fp); int puts(const char *str); //若成功,返回非负值;若出错,返回EOF
fputs讲一个以null字节终止的字符串写到指定的流,尾端的终止符null不写出。puts并不像它所对应的gets那样不安全,但是我们还是应避免使用它,以免需要记住它最后是否添加一个换行符。
八、标准I/O的效率
#include <apue.h> #include <my_err.h> int main(void) { int c; while((c = getc(stdin)) != EOF) { if(putc(c, stdout) == EOF) { err_sys("output error!"); } } if(ferror(stdin)) { err_sys("input error!"); } exit(0); }
5-4 用getc和putc将标准输入复制到标准输出
#include <apue.h> #include <my_err.h> int main(void) { char buf[MAXLINE]; while(fgets(buf, MAXLINE, stdin) != NULL) { if(fputs(buf, stdout) == EOF) { err_sys("output error!"); } } if(ferror(stdin)) { err_sys("input error!"); } exit(0); }
5-5 用fgets和fputs将标准输入复制到标准输出
函数 |
用户cpu |
系统cpu |
时钟时间 |
程序正文字节数 |
图3-6最佳时间 |
0.05 |
0.29 |
3.18 |
|
fgets/fputs |
2.27 |
0.30 |
3.49 |
143 |
getc/putc |
8.45 |
0.29 |
10.33 |
114 |
fgetc/fputc |
8.16 |
0.40 |
10.18 |
114 |
图3-6单字节时间 |
134.61 |
249.94 |
394.95 |
|
5-6 使用标准I/O例程得到的时间结果
1、系统CPU时间几乎相同,原因是因为所有这些程序对内核提出的读写请求数基本相同
2、fgets优于fgetc,是因为fgets底层拷贝使用memccpy,并且减少了函数调用次数
3、fgetc和getc没有明显区别的原因是getc是宏简单地扩充为函数调用
4、fgetc、fputc大大优于单字节时间是因为虽然函数调用次数差不多,但是fgetc/fputc是普通的函数调用,而单字节是系统调用read。系统调用花费的时间大大超过普通函数调用。
九、二进制I/O
当我们一次需要处理一个完整的结构,我们就需要二进制I/O。典型的应用场景如读写一个二进制数组;读或写一个结构。
#include <stdio.h> size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp); size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp); //两个函数的返回值:读或写的对象数
fread和fwrite返回读或写的对象数。对于读、如果出错或到达文件尾端,则此数字可以少于nobj、在这种情况,应调用ferror或feof以判断究竟是哪一种情况。对于写,如果返回值少于所要求的nobj,则出错。使用二进制I/O还需要注意两个问题:1)在一个结构中,同意成员的偏移量可能随编译程序和系统的不同而不同;2)用来储存多字节整数和复电纸的二进制格式在不同的系统结构间也可能不同。