apue 第五章 标准i/o库

时间:2022-12-26 10:38:24

       在很多操作系统中,都实现标准I/O库,它由ISO C说明。

      标准I/O库处理很多细节,例如缓存分配,以优化长度执行I/O等,用户不必担心如何选择正确的块长度。

   标准I/O库的操作是围绕流stream进行的,相对于posix I/O是围绕文件描述符,当然在标准I/O库实现简单的理解是在此之上的封装,我们可以用fileno函数获得文件描述符。

   标准的I/O文件流可用于单字节和多字节<wchar.h>,这里称为流的定向。使用fwide函数可以设置流的定向。freopen可以清除流的定向。

   使用标准I/O函数,会用到一个FILE类型的文件指针,该对象通常是一个内部结构,它包含了标准I/O库为管理该流所需要的所有信息:用于实际I/O的文件描述符,指向流缓存的指针,缓存的长度,当前在缓存中的字符数,出错标志,文件结束标志,出错信息等。我们称指向FILE对象的指针为文件指针。

    一个进程预定义了三个流 stdin,stdout,stderr 和STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO所引用的文件描述符相同,都定义在stdio.h中。

   关于缓冲,posixI/O和 标准I/O区别再次,底层内核维护一个内核缓冲区,标准I/O也有自己的缓冲区,而posix没有。

   说说标准I/O的三种模式。 全缓冲、行缓冲、无缓冲。 全缓冲指的是当标准I/O的缓冲区写满后才会把缓冲区的内容提交到内核缓冲区中,行缓冲是指当遇到换行符后才会提交内容到内核缓冲区。 

然而ISO C要求, 标注出错时不带缓冲的,为的的错误尽快的被打印出来。

        涉及到终端设备的其他流,则它们是行缓冲的,否则是全缓冲的。


1.更改缓存类型函数
void setbuf(FILE *fp, char *buf);
int setvbuf(FILE *fp, char *buf, int mode, size_t size);  成功返回0,出错返回非0。
    对于setbuf:为了带缓存进行I/O,参数buf必须指向一个长度为BUFSIZ的缓存(该常数定义在<stdio.h>文件中,通常为1024)。
在此调用之后,该流就是全缓存的,但如果该流与一个终端设备相关,则系统会将该流设置为行缓存。将buf设置为NULL,可以关闭缓存。
    对于setvbuf:
        1)参数mode的含义为_IOFBF 全缓存;_IOLBF 行缓存;_IONBF 无缓存。
        2)如果mode为_IONBF,设置fp为无缓存,并忽略buf和size参数。
        3)如果mode为_IOFBF,buf不为NULL,则设置fp为由buf指向的长度为size的全缓存。
           如果buf为NULL,则由标准I/O库自动分配合适长度的缓存
        4)如果mode为_IOLBF,buf不为NULL,则设置fp为由buf指向的长度为size的行缓存。
          如果buf为NULL,则由标准I/O库自动分配合适长度的缓存。
    一般而言,应由系统选择缓存的长度,并自动分配缓存,这样标准I/O库在关闭此流时将自动释放此缓存。
 
刷新一个流: int fflush( FILE *fp); //成功返回0,出错返回EOF。
fflush使流fp的所有未写数据都被传递至内核。如果fp为NULL,则fflush函数刷新所有输出流。


2.打开流函数
//成功返回文件指针,出错返回NULL。
FILE *fopen(const char *pathname, const char *type);  
FILE *freopen(const char *pathname, const char *type, FILE *fp);
在一个特定的流上打开文件,如果流已经打开则先关闭。如果已经定向,则freopen清除定向。 
FILE *fdopen(int filedes, const char *type);
  类unix特有属于posix并不是iso C,利用打开的文件描述和标准I/O结合。
常用语socket和管道返回的函数返回的文件描述符,因为这些特殊的类型不能用fopen打开。

TYPE:
w r a 
w+ r+ a+
w会把文件截短为0,如果追加方式应该用a. +代表流可读可写,但要跟着 w r a 一种之后。
b使标准I/O可以区分文本和二进制文件,因为unix内核并不区分两种文件,对于unix环境无作用。

当以读写类型打开一文件时(type中有+号),具有以下限制:
        a)如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。
        b)如果中间没有fseek、fsetpos或rewind,或者一个输出操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。

对于fdopen,因为该描述符已经被打开,所以fdopen为写而打开w和w+并不截短该文件,
同时,标准I/O添加方式a和a+也不能用于创建该文件。

3.关闭流
int fclose(FILE *fp);
在该文件被关闭之前,刷新缓存中的输出数据。
缓存中的输入数据被丢弃。如果标准I/O库已经为该流自动分配了一个缓存,则释放该缓存。


4读和写流
    有三种不同类型的非格式化I/O和几种格式化I/O可以用于对一个打开的流进行读写。
  每次一个字符的I/O
    int getc(FILE *fp);    
    int getchar(void);   
    int fgetc(FILE *fp);
    成功返回读到的字符,读到文件尾端或者出错返回EOF。
    1)函数getchar等同于getc(stdin)。
    2)这三个函数以unsigned char类型转换为int的方式返回读到的字符。说明为不带符号是为了,即使最高位为1,也不会使返回值为负。使用整型返回值是为了可以返回所有可能的字符值,再加上一个已发生错误或已到达文件尾端的指示值。EOF常数在stdio.h中定义为-1,这就意味着不能将这三个函数的返回值存放在一个字符变量中,因为以后还要用这些函数的返回值与常数EOF相比较。
    3)在FILE对象中,为每个流保持了两个标志:出错标志和文件结束标志。为了区分出错还是到达文件尾,需要调用ferror或者feof。
   函数clearerr清除这两个标志。
    int ferror(FILE *fp);  int feof(FILE *fp);  条件为真,返回非0,否则返回0。
    void clearerr(FILE *fp);
    4)从一个流读之后,可以调用ungetc将字符再送回流(先进后出)。EOF不能回送,但ungetc会清除该流的文件结束指示。 int ungetc(int c, FILE *fp); 成功返回c,出错返回EOF。
    5)每次一个字符的I/O输出,包括:
    int putc(int c, FILE *fp); int putchar(int c); int fputc(int c, FILE *fp);
putchar(c)等同于putc(c, stdout)。putc可以实现为宏,fputc则不可以。
 
  每次一行的I/O 
    输入函数:
    char *fgets(char *buf, int n, FILE *fp); 
    char *gets(char *buf);
    gets从标准输入读,fgets从指定的流中读。
    gets不推荐使用,因为它不指定缓存的长度,可能会造成缓存越界。fgets读到一个新行符为止,但不超过n-1个字符。
    输出函数:
    int fputs(char *buf, FILE *fp);  
    char *puts(char *buf);
 
   二进制I/O
    二进制文件中可能含有null字符和新行符,所以fputs和fgets不能正确工作,
    而getc和putc需要循环通过整个结构,一次读或写一个字符,效率比较低,
    所以使用fread和fwrite函数以执行二进制I/O操作。
    size_t fread(void *ptr, size_t size, size_t num, FILE *fp);//从fp中读num个size大小的结构到ptr所指的缓存中。
    size_t fwrite(void *ptr, size_t size, size_t num, FILE *fp);//从ptr所指缓存中取num个size大小的结构写到fp中。
    使用二进制I/O的基本问题是它只能用于读写在同一个系统中的数据。原因是:
    1)在一个结构中,同一成员的位移量可能随编译程序和系统的不同而异,因为不同的对齐要求。编译程序有选择项,
                  允许紧密包装结构(节省存储空间,但会降低运行性能),
                  或者准确对齐(耗费存储空间,但运行时易于存取结构的各个成员)。
    2)用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。
    3)在不同系统之间交换二进制数据的实际解决方法是使用较高层次的协议。
 
5.定位流
    有两种方法定位标准I/O流:
    1)ftell和fseek(V7系统),它们假定文件的位置可以存放在一个长整型中。
long ftell(FILE *fp);    成功返回当前文件位置指示,出错返回-1L。
int fseek(FILE *fp, long offset, int whence); //参数whence的值有:SEEK_SET、SEEK_CUR和SEEK_END
void rewind(FILE *fp);   将一个流设置到文件的起始位置。
    对于一个二进制文件,其位置指示器是从文件起始位置开始度量,并以字节为计量单位的,例如ftell。
    对于文本文件,它们的文件当前位置可能不是以简单的字节位移量来度量,在非UNIX系统中,它们可能以不同的格式存放文本文件,比如UTF-8格式,UNCODE格式等。为了定位一个文本文件,whence一定要是SEEK_SET,而且offset只能有两种值:0(反绕文件至其起始位置),或是对该文件的ftell所返回的值。
   2)fgetpos和fsetpos(ANSI C系统),
     它们引进了一个新的抽象数据类型fpos_t,它记录文件的位置。在非UNIX系统中,这种数据类型可以定义为记录一个文件的位置所需的长度。
int fgetpos(FILE *fp, fpos_t *pos);
int fsetpos(FILE *fp, fpos_t *pos);
fgetpos将文件位置指示器的当前值存入由pos指向的对象中,在以后调用fsetpos时,可以使用此值将流重新定位至该位置。
        需要移植到非UNIX系统的应用程序应该使用fgetpos和fsetpos。
 
6.格式化I/O
    格式化输出
  int printf(const char *format, ...);
int fprintf(FILE *fp, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
三个函数分别将格式化数据写到标准输出、指定的流fp和数组buf中,成功返回输出或者存入的字符数,出错返回负值。

三种printf族的变体类,可变参数表(...)变为了arg。
#include <stdarg.h>
#include <stdio.h>
int vprintf(const char *format, va_list arg);
int vfprintf(FILE *fp, const char *format, va_list arg);
int vsprintf(char *buf, const char *format, va_list arg); 
 
    格式化输入
执行格式化输入的三个scanf函数
int scanf(const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);
int sscanf(char *buf, const char *format, ...);
同样,它们也有可变长度参数表类型的变体函数:vscanf, vfscanf, vsscanf。
这些函数成功返回指定的输入项数,如果输入出错,或者在任意变换前以至文件尾端则为EOF。   

7.临时文件
      char *tmpnam(char *ptr);
    产生一个与现存文件名不同的一个有效路径名字符串,返回指向该唯一路径名的指针。
如果ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。
    下一次再调用tmpnam时,会重写该静态区。所以如果我们调用tmpnam多次,我们应该保存该路径名的副本,而不是指针的副本。
如果ptr不为NULL,则认为它指向长度至少是L_tmpnam个字符的数组。L_tmpnam定义在stdio.h文件中。
      FILE *tmpfile(void);    成功返回文件指针,出错返回NULL。
    tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件。
tmpfile函数的实现使用的标准UNIX技术就是先调用tmpnam产生一个唯一的路径名,然后立即unlink它。
对一个文件解除连接并不删除其内容,关闭该文件时才删除其内容。
     
      char *tempnam(const char *directory, const char *prefix);
tempnam函数是tmpnam的一个变体,它允许调用者为所产生的路径名指定目录和前缀。
对于目录有四种不同的选择,并且按顺序检查每个选择,使用第一个为真的作为目录。
该函数同时也检查相应的目录是否有意义,如果目录不存在或者没有写许可权,则跳过这个选择,检查下一个选择:
    1)检查是否定义了环境变量TMPDIR。
    2)检查参数directory是否不为NULL。
    3)将<stdio.h>中的字符串P_tmpdir作为目录。
    4)将本地目录,通常是/tmp,作为目录。
如果prefix不为NULL,则它应该是最多包含5个字符的字符串,用其作为文件名的头几个字符。
因为5个字符长的前缀,4个字符长的进程内唯一标识,再加上5个字符长的系统内唯一标识(进程ID号),
正好组成14位的UNIX传统文件长度限制。
tempnam函数内部调用malloc分配动态存储区,用其存放所构造的路径名。
所以当不再使用tempnam返回的路径名时,应该要手工调用free函数释放该存储区。


习题:
1.
int _setbuf(FILE *fp, char *buf)
{
if ( (buf == NULL)<span style="font-family: simsun;">||(fp==stderr) )</span>
return (setvbuf(fp, buf, _IONBF, BUFSIZ));
else
{
if( (fp==stdin)||(fp==stdout) )
return (setvbuf(fp, buf, _IOLBF, BUFSIZ));
else
return (setvbuf(fp, buf, _IOFBF, BUFSIZ));
} <span style="font-family: simsun;"></span><pre name="code" class="cpp" style="color: rgb(70, 70, 70); font-size: 14px; line-height: 20.66666603088379px;">}