在之前提到的所有I/O都是系统提供的I/O都是围绕文件描述符的,而本章的标准I/O都是基于流(stream)的。
当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。 对于ASCII字符集,一个字符用一个字节表示,对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于但字节或多字节(“宽”)字符集。流的定向决定了所读所写的字符是单字节还是多字节的。当一个流最初被创建时,它没有定向。如若在未定向的流上使用一个多字节I/O函数,则将该流的定向设置为宽定向的。如若在未定向的流上使用单字节的流则使用单字节的I/O函数,则将该流的定向设置为字节定向的。只有两个函数可改变流的定向。freopen函数清除一个流的定向;fwide用于设置流的定向。
函数原型: #include <stdio.h> #include <wchar.h> int fwide(FILE *fp, int mode); 返回值:若流时宽定向的,返回正值,若流是字节定向的,返回负值,若流是未定向的,返回0。 mode参数值:
负值,fwide试图将指定的流设置为字节定向的正值,fwide试图将指定的流设置为宽字节定向的若为0,fwide将不试图设置流的定向,但返回标识该流的值。这三个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用。这三个文件指针定义在头文件<stdio.h>中。
标准I/O提供了以下三种类型的缓冲
全缓冲。在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘文件上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需要使用缓冲区。 fflush说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动的冲洗(例如,当填满一个缓冲区时),或者可以调用函数fflush冲洗一个流。值得注意的是,在UNIX环境中,flush有两种意思。在标准I/O库方面,flush意味着将缓冲区中的内容写到磁盘(该缓冲区可能只是部分填满的)。在终端驱动程序方面,flush表示丢弃已存储在缓冲区中的数据。行缓冲。当输入输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时如标准输入和标准输出,通常使用行缓冲。不带缓冲。标准I/O库不对字符进行缓冲存储。标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。对任何一个给定的流,如果我们并不喜欢这些系统默认,可以调用下列两个函数中的一个更改缓冲类型。
函数原型: #include <stdio.h> void setbuf(FILE *restrict fp, char *restrict buf); int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size); 返回值:成功返回0,失败返回非0。 这些函数一定要在流已被打开后调用,而且也应在对流执行任何一个其他操作之前调用。 mode参数: _IOFBF 全缓冲 _IOLBF 行缓冲 _IONBF 不带缓冲 如果指定一个不带缓冲的流,则setvbuf函数忽略buf和size_t参数。如果指定全缓冲或行缓冲,则buf和size可选择的指定一个缓冲区及长度。如果该流是带缓冲的,而buf是NULL,则标准I/O库将自动的为该流分配适当的长度的缓冲区。适当长度指的是由常量BUFSIZE所指定的值。
任何时刻,我们都可以使用一下函数强制冲洗一个流:
函数原型: #include <stdio.h> int fflush(FILE *fp); 返回值:成功返回0,失败返回EOF。 此函数使该流所有未写的数据都被传送到内核。作为一种特殊情形,如若fp是NULL,则此函数将导致所有输出流被冲洗。
下列3个函数打开一个标准I/O流。
函数原型: #include <stdio.h> FILE *fopen(const char *restrict pathname, const char *restrict type); FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp); FILE *fdopen(int fd, const char *type); 成功返回文件指针,失败返回NULL。
调用如下函数可关闭一个打开的流。
函数原型: #include <stdio.h> int fclose(FILE *fp); 成功返回0,失败返回EOF。 在文件关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被抛弃。如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。当一个进程正常关闭时(调用exit或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。
函数原型: #include <stdio.h> int getc(FILE *fp); int fgetc(FILE * fp); int getchar(void); 三个函数,成功返回下一个字节,若已到达文件尾端或出错,返回EOF。 getchar等同于getc(stdin)。getc可被实现为宏,而fgetc不能被实现为宏。
不管是出错还是到达文件尾端,这三个函数都返回相同的值。为了区分这两种不同的情况,必须调用ferror或feof。
函数原型: #include <stdio.h> int ferror(FILE *fp); int feof(FILE *fp); 两个函数成功返回非0,失败返回0 void clearerr(FILE *fp);\
大多数实现中,为每个流在FILE对象中维护两个标志:
出错标志文件结束标志调用clearerr可以清除这两个标志。 从流中读取数据后,可以调用ungetc将字符再压回流中。
函数原型: #include <stdio.h> int ungetc(int c, FILE *fp); 成功返回c,失败返回EOF。 压送回流中的字符以后又可以从流中读出,但读出字符的顺序与压入的顺序相反。虽然ISOC运行任何次数的压入,但是它每次只回送一个字符。我们不能期望一次能送回多个字符。
输出函数函数原型: int putc(int c, FILE *fp); int fputc(int c, FILE *fp); int putchar(int c); 三个函数成功返回c,失败返回EOF。 putchar©等同于putc(c, stdout),putc可被实现为宏,而fputc不能实现为宏。
下面两个函数提供了每次输入一行的功能。
函数原型: #include <stdio.h> char *fgets(char *restrict buf, int n, FILE *restrict fp); char *gets(char *buf); 两个函数:成功返回buf,失败或达到文件尾返回NULL。
fputs和puts提供每次输出一行的功能。
函数原型: #include <stdio.h> int fputs(const char *restrict str, FILE *restrict fp); int puts(const char *str); 两个函数成功返回非负值,失败返回EOF。
使用上述函数,我们能对标准I/O系统的效率有所了解。 使用每次获取一字节的I/O:
#include <stdio.h> #include <stdlib.h> int main() { int c; while ((c = getc(stdin)) != EOF) { if (putc(c, stdout) == EOF) { fprintf(stderr,"output error\n"); exit(-1); } } if (ferror(stdin)) { fprintf(stderr, "input error\n"); exit(-1); } return 0; }使用每次获取一行数据的I/O:
#include <stdio.h> #include <stdlib.h> int main() { char buf[BUFSIZ]; while (fgets(buf,BUFSIZ,stdin) != NULL) { if (fputs(buf,stdout) == EOF) { fprintf(stderr, "output error\n"); exit(-1); } } if (ferror(stdin)) { fprintf(stderr, "input error\n"); exit(-1); } return 0; }两个程序执行风别得到的结果如下图:
函数原型: 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); 两个函数的返回值:读或写的对象数 ptr:写入的对象或元素 size:对象的大小或元素的大小 nboj:写入的对象的个数或元素的个数 fp:写入的文件的流指针或终端设备的流指针
有三种方法定位标准I/O流
ftell和fseek函数。ftello和fseeko函数。fgetpos和fsetpos函数。需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos。
函数原型: #include <stdio.h> logn ftell(FILE *fp); 返回值:成功返回当前文件为准指示,出错返回-1L int fseek(FILE *fp, long offset, int whence); 返回值:成功返回0,失败返回-1 void rewind(FILE *fp);
对于一个二进制文件,其文件位置指示器是从文件起始位置开始度量,并以字节为度量单位的。ftell用于二进制文件时,其返回值就是这种字节位置。为了用fseek定位一个二进制文件,必须指定一个字节offset,以及解释这种偏移量的方式。whence的参数值和lseek函数的whence参数值是一样的。 SEEK_SET文件开始位置,SEEK_CUR文件当前位置,SEEK_END文件尾端。
函数原型: #include <stdio.h> off_t ftello(FILE *fp); 成功返回当前文件位置,出错返回(off_t)-1 int fseeko(FILE *fp,off_t offset, int whence); 成功返回0,失败返回-1 int fgetpos(FILE *restrict fp, fpos_t *restrict pos); int fsetpos(FILE *restrict fp, const fpos_t *pos); 两个函数成功返回0,失败返回非0
1.格式化输出 格式化输出是由5个printf函数来处理的。
函数原型: #inlcude <stdio.h> int printf(const char *restrict format, …); int fprintf(FILE *restrict fp, const char *restrict format, …); int dprintf(int fd, const char *restrict format, …); 三个函数成功返回输出字符数,失败返回负数 int sprintf(char *restrict buf, const char *restrict format, …); 成功返回存入数组的字符数,若编码出错,返回负数 int snprintf(char *restrict buf, size_t n, const char *restrict format, …); 若缓冲区足够大,返回将要存入数组的字符数,若编码出错,返回负数
格式说明控制其余参数如何编写,以后又如何显示。每个参数按照装换说明编写,装换说明以%开始,除转换说明外,格式字符串中的其它字符将按原样,不经任何修改被复制输出。一个转换说明有4个可选择的部分,下面将它们都示与方括号中: %[flags][fldwidth][precision][lenmodifier]convtype fldwidth说明最小字段宽度。转换后参数字符数若小于宽度,则多余字符位置用空格填充。字段宽度是一个非负十进制,或是一个星号()。 precision说明整形转换后最少输出数字位数、浮点数转换后小数点后的最少位数、字符串转换后最大字节数。精度是一个点(.),其后跟随一个可选的非负十进制数或一个星号()。 宽度和精度字段两者皆可为*,此时,一个整型参数指定宽度或精度的值。该整型参数正好位于被转换的参数之前。 lenmodifier说明参数长度。其可能的值如下图。 convtype不是可选的。它控制如何解释参数。 下列5种printf族的变体类似于上面5种,当可变参数表(…)被替换成了arg。
函数原型: #include <stdarg.h> #include <stdio.h> int vprintf(const char *restrict format, va_list arg); int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg); int vdprintf(int fd, const char *restrict format, va_list arg); 三个函数成功返回输出字符数,失败返回负数 int vsprintf(char *restrict buf, const char *restrict format, va_list arg); 成功返回存入数组的字符数,若编码失败,返回负数 int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg); 若缓冲区够大,返回存入数组的字符数,若编码失败,返回负数
2.格式化输入
函数原型: int scanf(const char *restrict format, …); int fscanf(FILE *restrict); int sscanf(const char *restrict buf, const char *restrict format, …); 三个函数成功返回赋值的输入项数,若输入出错或在任一转换前已到达文件尾端,返回EOF。 与printf族相同,scanf族也使用由<stdarg.h>说明的可变长度参数表。 函数原型: #include <stdarg.h> #include <stdio.h> int vscanf(const char *restrict format, va_list arg); int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg); int vsscanf(const char *restrict buf, const char *restrict format, va_lst arg); 三个函数成功返回赋值的输入项数,若输入出错或在任一转换前文件结束,返回EOF。
正如前述,在UNIX中,标准I/O库最终都要调用第三章中说明的I/O例程。每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数以获得其描述符。 fileno不是ISO C标准部分,而是POSIX.1支持的扩展。
函数原型: #include <stdio.h> int fileno(FILE *fp); 返回值:与该流相关联的文件描述符
ISO C标准I/O库提供了两个函数以帮助创建临时文件。
#include <stdio.h> char *tmpnam(char *ptr); 返回值:指向唯一路径名的指针 FILE *tmpfile(void); 返回值:成功返回文件指针,失败返回NULL 实例:使用两个函数创建临时文件
#include <stdio.h> #include <stdlib.h> int main() { char name[1024],line[BUFSIZ]; FILE *fp; printf("%s\n",tmpnam(NULL)); tmpnam(name); printf("%s\n",name); if ((fp = tmpfile()) == NULL) { perror("tmpfile error: "); exit(-1); } fputs("one line of output\n",fp); rewind(fp); if (fgets(line, sizeof(line), fp) == NULL) { perror("fgets error: "); exit(-1); } fputs(line,stdout); return 0; }程序运行效果: Sigle UNIX Specification为处理临时文件定义了另外两个函数:mkdtemp和mkstemp。
函数原型: #include <stdlib.h> char *mkdtemp(char *template); 成功返回路径名指针,失败返回NULL int mkstemp(char *template); 成功返回文件描述符,失败返回-1
mkdtemp函数创建了一个目录,该目录有一个唯一的名字,mkstemp函数创建了一个文件,该文件有一个唯一的名字。名字是通过template字符串进行选择的。这个字符串是后6位设置为XXXXXX的路径名。函数将这些占位符替换成不同的字符老构建一个唯一的路径名。如果成功的话,这两个函数将修改template字符串反映临时文件的名字。 由mkdtemp函数创建的目录使用下列访问权限位集:S_IRUSR | S_IWUSR | S_IXUSR。注意,调用进程的文件模式创建屏蔽字可以进一步限制这些权限。如果目录创建成功,mkdtemp返回新的目录的名字。 mkstemp函数以唯一的名字创建一个普通文件且打开该文件,该函数返回的文件描述符以读写方式打开。由mkstemp创建的文件使用访问权限位S_IRUSR | S_IWUSR。 与tempfile不同,mkstemp创建的临时文件并不会自动删除。如果希望从文件系统命名空间中删除该文件,必须自己对它解除链接。 使用tmpnae和mkdtemp至少有一个缺点:在返回唯一的路径名和用该名字创建文件之间存在一个时间窗口,在这个时间窗口中,另一个进程可以用相同的名字创建文件。因此应该使用tmpfile和mkstemp函数,因为他们不存在这个问题。 实例:如何使用mkstemp函数
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/stat.h> void make_temp(char *tempname); int main() { char good_tempname[] = "/tmp/dirXXXXXX"; char *bad_tempname = "/tmp/dirXXXXXX"; printf("trying to create first temp file...\n"); make_temp(good_tempname); printf("trying to create second temp file...\n"); make_temp(bad_tempname); return 0; } void make_temp(char *tempname) { int fd; struct stat sbuf; if ((fd = mkstemp(tempname)) < 0) { perror("cna't create temp file"); exit(-1); } printf("temp name = %s\n", tempname); close(fd); if (stat(tempname, &sbuf) < 0) { if (ENOENT == errno) { printf("file doesn't exist\n"); } else { perror("stat failed: "); exit(-1); } } else { printf("file exists\n"); unlink(tempname); } }程序运行结果:
标准I/O把数据缓存在内存中,因此每次一字符和每次一行的I/O更有效。我们可以通过调用setbuf或setvbuf函数让I/O库使我们自己的缓冲区。在SUSv4中支持了内存流。这就是标准I/O流,虽然仍使用FILE指针进行访问,但其实并没有底层文件。所有的I/O都是通过在缓冲区与主存之间来回传递字节来完成的。我们将看到,即便这些流看起来像文件流,它们的某些特征使其更适用于字符串操作。 第一个用于创建内存流的函数fmemopen函数:
函数原型: #include <stdio.h> FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type); 成功返回流指针,失败返回NULL。 该函数允许调用者提供缓冲区用于内存流,buf参数指向缓冲区的开始位置,size参数指定了缓冲区大小的字节数。如果buf参数为空,fmemopen函数分配size字节数的缓冲区,在这种情况下,当流关闭时缓冲区会被释放。 type参数控制如何使用流。type的取值如下图。
函数open_memstream和open_wmemstream
函数原型: #include <stdio.h> FILE *open_memstream(char **bufp, size_t *sizep); #include <wchar.h> FILE *open_wmemstream(wchar_t **bufp, size_t *sizep); 两个函数成功返回流指针,失败返回NULL。 第一个函数是面向字节流的,第二个是面向宽字节流的。