《Unix环境高级编程》之 标准I/O库

时间:2022-10-21 22:16:38

  标准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指针:stdinstdoutstderr加以引用。它们引用的文件已第三章中文件描述符:STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO 引用的文件相同。
  
3. 标准I/O库提供的缓冲
  标准I/O库提供缓冲的目的是尽可能的减少使用readwrite系统调用的次数。提供了三种类型的缓冲:
  (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)getcfgetc区别是前者可被实现为宏。这三个函数在返回下一个字符时,会将其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又将 一个换行符输出。
  fgetsfputs都不处理换行符。
  
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_SETSEEK_CURSEEK_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它
  mkstemptmpfile类似,但它并不自动unlink临时文件,需要手动unlink。
  tmpnam不足之处:在返回唯一路径名和应用程序用该路径名创建临时文件的空隙,另一个进程可能产生一个同名文件,所以应当避免使用它。