标准I/O库是由C语言标准ISO C定义的,不仅在Unix上,还有很多系统都实现了此库,与第三章介绍的常用文件I/O函数的区别:后者是POSIX.1标准定义的,且都是不带缓冲的I/O函数,可以看作是系统调用,而前者都是带缓冲的I/O函数,是在系统调用的基础上实现的。
1. 系统调用和库函数的区别
系统调用是操作系统提供的一组特殊接口,通过这组接口用户程序可以使用操作系统内核提供的各种功能,例如分配内存、创建进程、实现进程之间的通信等。
库函数可以说是对系统调用的一种封装,如果直接使用系统调用,会影响程序的移植性,所以才有了各种库函数,比如C库。
2. 流和FILE对象
文件I/O函数是针对文件描述符进行操作的,而标准I/O库则是围绕流进行的,当用标准I/O函数打开文件时,就使流与一个文件相关联。标准I/O库使用FILE对象来管理流的各种信息,包括:用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。
流的定向决定了所读、写的字符是单字节的还是多字节的,当流最初创建时,并没有定向,第一次在未定向的流上使用多字节I/O函数还是单字节I/O函数,决定了该流的定向。
操作系统对一个进程预定义的三个流,分别是:标准输入、标准输出和标准出错,通过定义在<stdio.h>
中的FILE指针:stdin
、stdout
和stderr
加以引用。它们引用的文件已第三章中文件描述符:STDIN_FILENO
、STDOUT_FILENO
和STDERR_FILENO
引用的文件相同。
3. 标准I/O库提供的缓冲
标准I/O库提供缓冲的目的是尽可能的减少使用read
和write
系统调用的次数。提供了三种类型的缓冲:
(1)全缓冲。这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作,在一个流上执行第一个I/O操作时,相关标准I/O函数会调用malloc获得所需的缓冲区。
(2) 行缓冲。当在输入和输出中遇到换行符时,标准I/O库执行I/O操作,这允许一个输出一个字符,但只有在写了一行后才进行实际的I/O操作。当流涉及一个终端(如标准输入和标准输出)时,通常使用行缓冲。行缓冲有两个限制:只要填满了缓冲区,即使没有遇见换行符,也进行I/O操作;当标准I/O库要求从内核中得到数据时,需要将缓冲区中的所有待输出数据进行冲洗。冲洗(flush)指缓冲区的写操作。
(3) 不带缓冲。不对字符进行缓冲存储,例如若使用不带缓冲的流进行I/O写操作,则标准I/O函数可能直接调用write系统调用,将字符立即写入相关文件。标准出错stderr通常是不带缓冲的,这就使得出错信息会尽快显示出来。
很多操作系统默认使用下列类型的缓冲:
* 标准出错是不带缓冲的;
* 若是涉及终端设备的其它流,则是行缓冲的;否则是全缓冲的。
4. 打开流
以下三个函数打开一个标准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 filedes, const char* type);
返回值:若成功返回文件指针,若出错返回NULL
三者的区别:fopen
打开一个指定的文件;freopen
在一个指定的流上打开一个文件,若该流已打开,则先关闭该流,若已定向,则清除定向;fdopen
使一个流与文件描述符所引用的文件相关联,适用于不能通过fopen
打开的特殊文件。
type参数指定对该I/O流的读、写方式,可取以下值:
type | 说明 |
---|---|
r或rb | 为读打开 |
w或wb | 为写打开并将文件截短至0 |
a或ab | 在文件尾端添加或为写创建 |
r+或r+b或rb+ | 为读和写打开 |
w+或w+b或wb+ | 为读和写打开并将文件截短至0 |
a+或a+b或ab+ | 为在文件尾读和写打开或创建 |
b用于区分文件是文本文件还是二进制文件。当以a或w创建一个新文件时,无法说明该文件的访问权限位
#include <stdio.h>
int fclose(FILE *fp);
关闭一个打开的流
返回值:成功返回0,出错返回EOF
文件关闭前,冲洗缓冲区中的输出数据,丢弃任何输入数据。当进程正常退出(直接调用exit或从main函数返回),所有打开的流会被自动关闭,所有缓冲区中未写的数据会被冲洗。
5. 读和写流 :非格式化I/O函数
非格式化I/O函数包括三种:每次一个字符的I/O;每次一行的I/O;直接I/O,每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。
以下三个函数用于一次读一个字符:
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
返回值: 成功返回下一个字符,到文件尾或出错返回EOF
函数getchar
可以视为getc(stdin)
,getc
和fgetc
区别是前者可被实现为宏。这三个函数在返回下一个字符时,会将其unsigned char转换为int类型,之所以转换为整型,是需要一个负值表示EOF(常用-1表示),所以不能用char来表示返回值;之所以unsigned,是因为即使最高位为1也不会使返回值为负。
出错和到达文件尾都返回EOF,所以需要函数来判断是哪种情况,每个流在FILE对象中维持了两个标志:出错标志和文件结束标志。
#include <stdio.h>
int ferror(FILE * fp);
int feof(FILE * fp);
返回值:条件为真返回非0,为假返回0;
void clearerr(FILE *fp);
清楚这两个标志
ungetc
将字符回压入流中,当到达文件尾时,调用ungetc
会清除文件结束标志:
#include <stdio.h>
int ungetc(int c, FILE* fp);
返回值:成功返回c,出错返回EOF
对应的输出函数:
#include <stdio.h>
int putc(int c, FILE* fp);
int fputc(int c, FILE* fp);
int putchar(void);
返回值:成功返回c,出错返回EOF
putc可实现为宏
6. 每次一行的I/O
#include <stdio.h>
char * fgets(char *restrict buf, int n, FILE* fp);
char * gets(char * buf);
返回值:成功返回buf,出错或到达文件尾返回NULL
gets从标准输入读,且不能指定缓冲区长度,不将换行符存入缓冲区;
fgets从指定的流读且必须指定缓冲区长度n,此函数一直读取一行,但是不超过n-1个字符,如果该行超过n-1个字符,则返回一个不完整的行,但缓冲区的结尾总是NULL字符。
gets不将换行符输入缓冲区,fgets将换行符输入缓冲区。
#include<stdio.h>
int fputs(const char* restrict str, FILE *fp);
int puts(const char* str);
返回值:成功返回非负值,出错返回EOF
fputs
输出以NULL字符结尾的字符串到指定的流,尾端的NULL不写出,并不一定输出一行,因为NULL字符前不一定是换行符。
puts
将以NULL字符结尾的字符串写到标准输出,NULL字符不写出,但puts
又将 一个换行符输出。
fgets
和fputs
都不处理换行符。
7. 二进制I/O
#include <stdio.h>
size_t fread(void * restrict ptr, size_t size, size_t nobj, FILE* fp);
size_t fwrite(void *restrict ptr, size_t size, size_t nobj, FILE* fp);
返回值:读或写的对象数
这两个函数常用于读或写数组或结构,只能用于在同一系统上读写数据,因为不同系统中,同一个结构,大小可能不一样。
8. 定位流
#include <stdio.h>
long ftell(FILE *fp);
返回值:成功则返回当前文件位置指示,失败则返回-1L;
int fseek(FILE *fp, long offset, int wherece);
返回值: 成功则返回0,出错返回非0值;
void rewind(FILE* fp);
wherece参数与第三章中lseek
函数的相同:SEEK_SET
,SEEK_CUR
,SEEK_END
rewind使一个流返回到文件的起始位置。
9. 格式化I/O
#include <stdio.h>
int printf(const char* restrict format, ...);
int fprintf(FILE* fp, 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, ...);
返回值:成功返回输入数组的字符数,出错返回负值。
sprintf在数组的尾端自动添加一个NULL字节,但该字节不包含在返回个数中。
10. 临时文件
#include <stdio.h>
char * tmpnam(char* ptr);
返回值:指向唯一路径名的指针;
FILE * tmpfile(void);
返回值:成功则返回文件指针,出错返回NULL
int mkstemp(char * template);
返回值:成功则返回文件描述符,出错返回-1。
tmpnam
产生一个与现有文件路径名不同的有效路径名字字符串,最多可调用TMP_MAX次。若ptr是NULL,则产生的字符串存在静态内存区中,若ptr不是NULL,则它至少指向长度是L_tmpnam个字符的数组,产生的路径名存放在该数组中,ptr作为返回值返回。
tmpfile
函数先调用tmpnam
产生一个唯一的路径名,如何创建临时文件,并立即unlink它。
mkstemp
与tmpfile
类似,但它并不自动unlink临时文件,需要手动unlink。
tmpnam
的不足之处:在返回唯一路径名和应用程序用该路径名创建临时文件的空隙,另一个进程可能产生一个同名文件,所以应当避免使用它。