标准I/O库

标准I/O库:

流和FILE对象:
    在文件I/O函数都是围绕这文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的I/O
    操作。而对于标准I/O库,它们的操作时围绕流进行的。当用标准I/O库打开或者创建一个文件时,我们已使一个流与一个文件
    相关联。
    对于ASCLL字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于单字节
    或者多字节(”宽“)字符集。流的定向决定了所读、写的字符是单字节或多字节的。当一个流最初被创建是,它并没有定向。
    如若在未定向的流上使用一个多字节I/O函数(<wchar.h>),则将该流的定向设置为宽定向的。若在为定向的流上使用一个
    单字节I/O函数,则将该流的定向设为单字节定向的。只有两个函数可改变流的定向。
    fopen函数清除一个流的定向;fwide函数可用于设置流的定向。
    #include <stdio.h>
    #include <wchar.h>
    int fwide(FILE *fp,int mode);
    根据mode参数的不同值,fwide函数执行不同的工作:
    1、如若mode参数值为负,fwide将试图使指定的流是字节定向的
    2、如若mode参数值为正,fwide将试图使指定的流是宽定向的
    3、如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值

    注意,fwide并不改变已定向流的定向。还应注意的是,fwide无出错返回。如若流是无效的,那么会发生什么?
    我们唯一可依靠是,在调用fwide前先清除errno,从fwide返回时检查errno值。
    当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象通常时一个结构,它包含了标准I/O库
    为管理该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在
    缓冲区的资附属以及出错标志等。

缓冲:
    标准I/O库提供缓冲的目的时尽可能减少使用read和write调用的次数。它也对每个I/O流自动地进行缓冲管理,从
    而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的时,标准I/O库最令人迷惑的也是它的缓冲。
    标准I/O提供了3种缓冲:
    1、全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。对于留在磁盘上的文件通常是由标准
    I/O库实施全缓冲的。在一个流上执行第一次I/O操作是,相关标准I/O函数通常调用malloc获得需使用的缓冲区。

    术语flush说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗(例如当填满一个缓冲区),或者
    可以调用函数fflush冲洗一个流。值得注意的是,在UNIX环境种,flush由两种意思。在标准I/O库方面,flush
    (冲洗)意味这将缓冲区中的内容写道磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面,flush(刷清)
    表示丢弃已存储在缓冲区中的数据。

    2、行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符
    (用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时,通常使用行缓冲。
    它有两个限制:
    1、因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,
    也进行I/O操作。

    2、任何时候只要通过标准I/O库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要的数据)
    得到输入数据,那么就会冲洗所有行缓冲输出的流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已
    在缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲区的流中输入(a)需要从内核获得数据。

    3、不带缓冲。标准I/O库不对字符进行缓冲储存。例如,若用标准I/O函数fputs写15个字符到不带缓冲的流中,我们
    就期望这15个字符能立即输出,很可能使用write函数将这些字符写道相关联的打开文件中。

    标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。

    很多系统默认:
    1、标准错误是不带缓冲的
    2、若是指向终端设备的流,是行缓冲的,否则是全缓冲的

    对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更改缓冲类型:
    #include <stdio.h>
    void setbuf(FILE *restrict fp,char *restrict buf);
    int setvbuf(FILE *restrict fp,char *restrict buf,int mode, size_t size);
    这些函数一定要在流已被打开后调用,而且也应在该流执行任何一个其他操作之前调用。

    可以使用setbuf函数打开或者关闭缓冲机制。为了带缓冲进行I/O,参数buf必须指向一个长度为
    BUFSIZ的缓冲区(该常量定义在<stdio.h>中)。通常在此之后该流就是全缓冲的,但是如果该流
    与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关闭缓冲,将buf设置为NULL。

    使用setvbuf,我们可以精确地说明所需的缓冲类型。这是用mode参数实现的:
    _IOFBF  全缓冲
    _IOLBF  行缓冲
    _IONBF  不带缓冲

    如果指定一个不带缓冲的流,则忽略buf和size参数。如果指定全缓冲或行缓冲,则buf和size可选择地指定一个缓冲
    区及其长度。如果该流是带缓冲的,而buf是NULL,则标准I/O库将自动地为该分配适当长度的缓冲区。适当长度指的是
    由常量BUFSIZ所指定的值。

    函数      mode    buf 缓冲区及长度          缓冲类型
    setbuf          非空  长度为BUFSIZ的用户缓冲区buf  全缓冲或行缓冲
                NULL    无缓冲区                不带缓冲区

    setvbuf     _IOFBF  非空  长度为size的用户缓冲区buf        全缓冲
                NULL    适合长度的系统缓冲区buf       全缓冲
            _IOLBF  非空  长度为size的用户缓冲区buf        行缓冲
                NULL    适合长度的系统缓冲区buf       行缓冲
            _IONBF  忽略  无缓冲区                不带缓冲区

    如果一个函数内分配一个自动变量类的标准I/O缓冲区,则从该函数返回之前,必须关闭该流。另外,其些实现将缓冲区的
    一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于size。一般而言,应由系统选择
    缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。
    任何时候,我们都可以强制冲洗一个流:
    #include <stdio.h>
    int fflush(FILE *fp);
    此函数使流所有未写的数据都被传送至内核。作为一种特殊情形,如若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);
    3个函数的返回值:若成功,返回文件指针,若出错,返回NULL。

    3个函数的区别:
    1、fopen函数打开路径名为pathname的一个指定的文件
    2、freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用
    freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出、标准错误。
    3、fdopen函数取一个已有的文件描述符(我们可能从open、dup、dup2、fcntl、pipe、socket、socketpair或
    accept函数得到此文件描述符),并是一个标准的I/O流与该描述符相结合。此函数用于由创建管道和网络通信函数返回
    的描述符。因为这些特殊型的文件后用fdopen使一个标准I/O流与该描述符相结合。

    type参数指定对该I/O流的读、写方式:
    type        说明                  open标志
    r或rb        为读而打开               O_RDONLY
    w或wb        把文件截断至0长,或为写而创建     O_WRONLY | O_CREAT | O_TRUNC
    a或ab        追加;为在文件尾写而打开,或为写而创建 O_WRONLY | O_CREAT | O_APPEND
    r++或r+b或rb+ 为读和写而打开             O_RDWR  
    w++或w+b或wb+ 把文件截断至0长,或为读和写而打开       O_RDWR | O_CREAT | O_TRUNC
    a++或a+b或ab+ 为在文件尾读和写而打开或创建      O_RDWR | O_CREAT | O_APPEND

    使用字符b作为type的一部分,这使得标准I/O系统可以区分文本文件和二进制文件。因为UNIX内核并不对这两种文件
    进行区分,所以在UINX系统环境下指定字符b作为type的一部分实际上并无作用。
    对于fdopen,type参数的意义有点区别。因为该描述符已被打开,所以fdopen为写而打开并不截断该文件。例如,若
    该描述符原来是由open函数创建的,而且该文件已经存在,则其O_TRUNC标志将决定是否截断该文件。fdopen函数不能
    截断它为写而打开的任一文件。另外,标准I/O追加写方式也不能用于创建文件(因为如果一个描述符引用一个文件,则
    该文件一定已经存在)。

    注意为了正确处理追加写方式,该文件必须用O_APPEND标志打开。
    当以读和写类型打开一个文件时(type中+号),具有下列限制:
    1、如果中间没有fflush、fseek、fsetpos或者rewind,则在输出的后面不能直接跟随输入
    2、如果中间没有gseek、fsetpos或rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接
    跟随输出。

    注意,在指定w或者a类型创建一个新文件时,我们无法说明该文件的访问权限位(open函数和creat函数则能做到)
    POSIX.1要求使用如下的权限位集来创建文件:
    S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
    我们也可以使用umask值来限制这些权限。
    除非流引用终端设备,否则按系统默认,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了
    流,那么在对该流执行任何操作之前,如果希望,则可使用前节所说的setbuf和setvbuf改变缓冲区的类型。
    调用fclose关闭一个打开的流:
    #include <stdio.h>
    int fclose(FILE *fp);
    在该文件被关闭之前,冲洗缓冲区中的输出数据。缓冲区的任何输入数据被丢弃。如果标准I/O库已经为该流自动分配了
    一个缓冲区,则释放此缓冲区。
    当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有
    打开的标准I/O流都被关闭。

读和写流:
    一旦打开了流,则在这3种不同类型的非格式化I/O种进行选择,对其进行读、写操作。
    1、每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
    2、每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,
    应说明能处理的最大行长。
    3、直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或者写某种数量的对象,而每个对象具有指定
    的长度。这两个函数常用于从二进制文件中每次读或写一个结构。

1、输入函数
    以下3个函数可用于一次读一个字符:
    #include <stdio.h>
    int getc(FILE *fp);
    int fgetc(FILE *fp);
    int getchar(void);
    函数getchar等同于getc(stdin)。前两个函数的区别是,getc可被实现为宏,而fgetc不能实现宏。
    1、getc的参数不应当是具有副作用的表达式,因为它可能会被计算多次
    2、因为fgetc一定是个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传送给另外一个函数。
    3、调用fgetc所需时间很可能比调用getc要长,因为调用函数所需的时间通常长于调用宏。

    判断出错还是到达文件尾端了:
    #include <stdio.h>
    int ferror(FILE *fp);
    int feof(FILE *fp);
    void clearerr(FILE *fp);
    在大多数实现中,为每个流在FILE对象中维护了两个标志:
    1、出错标志
    2、文件结束标志
    调用clearerr可以清除这两个标志。
    从流中读取数据之后,可以调用ungetc将字符再压送回流中。
    #include <stdio.h>
    int ungetc(int c,FILE *fp);

    压送回到流中的字符以后又可从流中读出,但读出字符的顺序与压送回的顺序相反。
    实际上,每次提供一次只回送一个字符,我们不能期望一次能回送多个字符。
    回送的字符,不一定必须是上一次读到的字读。不能回送EOF。但是当已经到达文件尾端时,仍可以回送一个字符。
    下次读将返回该字符,再读则返回EOF。之所以能这样做的原因时,一次成功的ungetc调用会清除该流的文件结束
    标志。

2、输出函数:
    #include <stdio.h>
    int putc(int c,FILE *fp);
    int fputc(int c, FILE *fp);
    int putchar(int c);
    与输出函数一样,putchar(c)等同于putc(stdin),putc可被实现为宏,而fputc不能

每次一行I/O:
    #include <stdio.h>
    char *fgets(char *restrict buf,int n,FILE *restrict fp);
    char *gets(char *buf);
    这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标出输入读,而fgets则从指定的流读。

    对于fgets,必须指定缓冲的长度n。此函数一直读到下一个换行符为止,但是不超过n-1个字符,读入的字符将
    被送入缓冲区。该缓冲区以null字节结尾。如若该行包括最后一个换行符的字符数超过n-1,则fgets只返回一个
    不完整的行,但是,缓冲区总是以null字节结尾。对fgets的下一次调用会继续读该行。

    gets是一个不推荐使用的函数。其问题时调用者再使用gets时不能指定缓冲区的长度。这样就可能赵成缓冲区溢出
    (如若改行长于缓冲区长度),写道缓冲区之后的存储空间中,从而产生不可预料的结果。

    gets与fgets的另一个区别是,gets并不将换行符存入缓冲区中。

    fputs和puts函数提供了每次输出一行的功能:
    #include <stdio.h>
    int fputs(const char *restrict str,FILE *restrict fp);
    int puts(const char *str);
    函数fputs函数将一个null字节终止的字符串写道指定的流,尾端的终止符null不写出。
    注意,这并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非null字节。通常,在null字节之前是一个换行符
    但并不要求总是如此。
    puts将一个null字节终止的字符串写到标准输出,终止符不写出。但是,puts随后又将一个换行符写到标准输出。

标准I/O的效率:
    使用getc和putc函数将标准输入复制到标准输出。
        #include <stdio.h>
        #include <stdlib.h>

        int main()
        {
            int c;
            while((c=getc(stdin))!=EOF)
            {
                if(putc(c,stdout)==EOF)
                        printf("output error");
            }

            if(ferror(stdin))
                printf("input error");
            exit(0);
        }
    可以使用fgetc和fputc函数改写该程序,这两个一定是函数,而不是宏。
    最后编写一个读、写行的版本。
        #include <stdio.h>
        #include <stdlib.h>

        #define MAXLINE 100
        int main()
        {
            char buf[MAXLINE];

            while(fgets(buf,MAXLINE,stdin)!=NULL)
            {
                if(fputs(buf,stdout)==EOF)
                        printf("out error");
            }
            if(ferror(stdin))
                printf("input error");
            exit(0);
        }
    实际上,每次一行的版本会更慢一些。而在本测试中所用的每次一行函数使用memccpy实现的,通常为了提高效率,
    memccpy用汇编实现而不是c语言实现的。

二进制I/O:
    如果进行二进制I/O操作,那么我们更愿意一次读或写一个完整的结构。如果使用getc或putc读、写一个结构,那么
    必须循环通过整个结构,每次循环处理一个字节,一次读或写一个字节,这会非常麻烦而且费时。如果使用fgets和
    fputs,那么因为fputs再遇到null字节时就停止,而在结构中可能包含null字节,所有不能使用它实现读结构的
    要求。同样,如果输入数据中包含null字节或换行符,则fgets也不能正确工作。因此,提供了下列函数:

    #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 nojb,FILE *restrict fp);
    返回值:读或者写的对象数。   

    这些函数由以下两种常见的用法:
    1、读或者写一个二进制数组。例如,为了将一个浮点数组的第2~5个元素写至一文件上,编写如下:
        float data[10];
        if(fwrite(&data[2],sizeof(float),4,fp)!=4)
            printf("fwrite error");

    2、读或者写一个结构。例如编写如下:
        struct {
            short count;
            long total;
            char name[NAMESIZE];
        }item;
        if(fwrite(&item,sizeof(item),1,fp)!=1)
            printf("fwrite error");
    其中size为结构的长度,nobj为1(要写的对象个数)。

    对于读,如果出错或到达文件尾端,则此数字可以少于nobj。在这种情况下,应调用ferror或者feof一判断究竟是哪一种情
    况。
    对于写,如果返回值少于所要求的nobj,则出错。

    但是它们只能用于读在同一系统上已写的数据。在另外一个系统上,可能不能正常工作,原因是:
    1、在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同。
    2、用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。


定位流:
    1、ftell和fseek函数。但是它们都假定文件的位置可以存放在一个长整型中。
    2、ftello和fseeko函数。使文件偏移量可以不必一定使用长整型。它们使用off_t数据类型代替了长整型。
    3、fgetpos和fsetpos函数。它们使用一个抽象数据类型fpos_t记录文件的位置。这种数据类型可以根据需要定义
    一个足够大的数,用以记录文件位置。

    需要在非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos。
    #include <stdio.h>
    long ftell(FILE *fp);   成功:返回当前文件位置指示,出错,返回-1L
    int fseek(FILE *fp,long offset,int whence); 成功:返回0,出错,返回-1
    void rewind(FILE *fp);
    对于一个二进制文件,其文件位置指示器就是从文件起始位置开始度量,并以字节为度量单位的。
    ftell用于二进制文件时,其返回值就是这种字节位置。
    为了用fseek定位一个二进制文件,必须指定一个字节offset,以及解释这种偏移量的方式。
    whence的值:SEEK_SET表示从文件的起始位置开始,
        SEEK_CUR从当前位置开始,
        SEEK_END从文件尾端开始。


    为了定位一个文本文件,whence一定要时SEEK_SET,而且offset只能有两种值:0(后退到文件的起始位置)
    或是对该文件的ftell所返回的值,使用rewind也可将一个流设置到文件的起始位置。

    除了偏移量的类型是off_t而非long外,ftello函数与ftell相同,fseeko函数与fseek相同。

    #include <stdio.h>
    off_t ftello(FILE *fp);成功:返回当前文件位置,出错,返回(off_t)-1
    int fseeko(FILE *fp,off_t offset,int whence);成功:返回0,出错,返回-1

    fgetpos和fsetpos函数:
    #include <stdio.h>
    int fgetpos(FILE *restrict fp,fpos_t *restrict pos);
    int fsetpos(FILE *fp,const fpos_t *pos);
    fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在之后使用fsetpos时,可以使用此值将流
    重新定位至该位置。

格式化I/O:
    1、格式化输出
    格式化输出是由5个printf函数来处理的:
    #include <stdio.h>
    int printf(const char *restrict format,...);
    int fprintf(FILE *restrict fp,const char *restrict format,...);
    int dprintf(int fd,const char *restrict fromat,...);
    3个函数返回值:成功,返回输出字符数,错误,返回负值
    int sprintf(char *restrict buf,const char *restrict format,...);
    成功:返回存入数组的字符数,出错,返回负值
    int snprintf(char *restrict buf,size_t n,const char *restrict format,...);
    返回值:若缓冲区足够大,返回将要存入数组的字符数,出错,返回负值

    printf将格式化数据写到标准输出,fprintf写至指定的流,dprintf写至指定的文件描述符,
    sprintf将格式化的字符送入数组buf中。sprintf在该数组的尾端自动加入一个null字节
    但该字符不包括在返回值中。

    虽然dprintf不处理文件指针,但我们仍然把它包括在处理格式化输出的函数中。注意,使用dprintf
    不需要使用fdopen将文件描述符转换为文件指针(fprintf需要)。

    fldwidth说明最小字段宽度。转换后参数字符数若小于宽度,则多余字符位置用空格填充。字段宽度是一个
    非负十进制数,或是一个星号(*).

    precision说明整形转换后最小输出数字位数、浮点数转换后小数点后的最少位数、字符串转换后最大字节数。
    精度是一个点(.),其后跟随一个可选的非负十进制数或者一个星号(*).

    宽度和精度字段两者皆可为*.此时,一个整形参数指定宽度或精度的值。该整形参数正好位于被转换的参数之前。
    lenmodifier说明参数长度。其可能的值:

    convtype不是可选的。它控制如何解释参数。

    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);
    3个函数的返回值:成功,返回输出字符数,出错,返回负值
    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);
    函数返回值:缓冲区足够大,返回存入数组的字符数,出错,返回负值。

格式化输入:
    #include <stdio.h>
    int scanf(const char *restrict format,...);
    int fscanf(FILE *restrict fp,const char *restrict format,...);
    int sscanf(const char *restrict buf,const char *restrict format,...);
    返回值:赋值的输入项数;出错或到达文件尾端,返回EOF

    scanf族的变体类型:
    #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_list arg);

实现细节:
    每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数以获得其描述符:
    #include <stdio.h>
    int fileno(FILE *fp);
    返回值:与该流相关联的文件描述符。

    如果要调用dup或者fcntl函数,则需要此函数。

临时文件:
    标准I/O库提供了两个函数以帮助创建临时文件:
    #include <stdio.h>
    char *tmpnam(char *ptr);
    FILE *tmpfile(void);

    tmpnam函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它,都产生一个不同的路径名,最多
    调用此书时TMP_MAX。TMP_MAX定义在<stdio.h>中

    若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。后续调用temnam时,
    会重写该静态区(这意味着,如果我们调用此函数多次,而且想保存路径名,则我们应当保存该路径名的副本,而不是
    指针的副本)。如若ptr不是NULL,则认为它应该是指向长度至少是L_tmpnam个字符的数组(常量L_tmpnam定义在文件
    <stdio.h>中)。所产生的路径名存放在该数组中,ptr也作为函数值返回。

    tmpfile创建一个临时的二进制文件(类型wb+),在关闭该文件或者程序结束时自动删除这个文件。注意UNIX对二进制文件
    不进行特殊区分。
        #include <stdio.h>
        #include <stdlib.h>

        #define MAXLINE 100

        int main()
        {
            char name[L_tmpnam],line[MAXLINE];
            FILE *fp;

            printf("%s\n",tmpnam(NULL));

            tmpnam(name);
            printf("%s\n",name);

            if((fp=tmpfile())==NULL)
                printf("tmpfile error");
            fputs("one line of output\n",fp);
            rewind(fp);
            if(fgets(line,sizeof(line),fp)==NULL)
                printf("fgets error");
            fputs(line,stdout);
            exit(0);
        }
        执行程序:
        ./a.out

    tmpfile函数经常使用的标准UNIX技术时先调用tmpnam产生一个唯一的路径名,然后,用该路径名创建一个文件,
    并立即unlink它。对一个文件解除链接并不删除其内容,关闭该文件时才删除其内容。而关闭文件可以是显式的,
    也可以在程序终止时自动进行。

    处理林是文件定义了另外两个函数:mkdtemp和mkstemp:
    #include <stdlib.h>
    char *mkdtemp(char *template);
    返回值:若成功,返回指向目录名的指针,出错,返回NULL
    int mkstemp(char *template);
    返回值:若成功,返回文件描述符,出错,返回-1

    mkdtemp函数创建一个目录,该目录由一个唯一的名字;mkstemp函数创建一个文件,该文件有一个唯一的名字。
    名字时通过template字符串进行选择的。这个字符串时后6为设置为XXXXX的路径名。函数将这些占位符替换成不
    同的字符来构建一个唯一的路径名。如果成功的话,这两个函数将修改template字符串反映临时文件的名字。

    由mkdtemp函数创建的目录使用下列访问权限位集:S_IRUSR | S_IWUSR | S_IXUSR。注意,调用进程的文件
    模式创建屏蔽字可以进一步限制这些权限。如果目录创建成功,mkdtemp返回新目录的名字。

    mkstemp函数以唯一的名字创建一个普通文件并且打开该文件,该函数返回的文件描述符以读写方式打开。由mkstemp
    创建的文件使用访问权限位S_IRUSR | S_IWUSR。

    与tempfile不同,mkstemp创建的临时文件并不会自动删除。如果希望从文件系统命名空间中删除该文件,必须自己对
    它解除链接。

    使用tmpnam和tempnam至少有一个缺点:在返回唯一的路径名和用该名字创建文件之间存在一个时间窗口,在这个时间
    窗口中,另一个进程可以用相同的名字创建文件。以此应该使用它们tmpfile和mkstemp函数,因为它们不存在这个问题。

        #include <stdio.h>
        #include <stdlib.h>
        #include <errno.h>
        #include <sys/stat.h>

        void make_temp(char *template);

        int main()
        {
            char good_template[]="/tmp/dirXXXXXX";
            char *bad_template="/tmp/dirXXXXXX";

            printf("trying to create first temp file...\n");
            make_temp(good_template);
            printf("trying to crreate second temp file ...\n");
            make_temp(bad_template);
            exit(0);
        }

        void make_temp(char *template)
        {
            int fd;
            struct stat sbuf;

            if((fd=mkstemp(template))<0)
                printf("can't create temp file");
            printf("temp name = %s\n",template);

            close(fd);

            if(stat(template,&sbuf)<0){
                if(errno==ENOENT)
                        printf("file doesn't exist\n");
                else
                        printf("stat failed");

            }
            else{
                printf("file exists\n");
                unlink(template);
            }
        }

        执行就会发现:这两个模板字符串声明方式的不同带来了不同的结果。
        对于第一个模板,因为使用了数组,名字是在栈上分配的。
        而第二个使用的是指针,只有指针自身驻留在栈上。
        编译器把字符串存放在可执行文件的只读段,当mkstemp函数试图修改字符串时,出现了
        段错误。

内存流:
    标准I/O库是数据缓存在内存中,因此每次一字符和每次一行的I/O更有效。
    我们也可以通过调用setbuf或者setvbuf函数让I/O库使用我们自己的缓冲区。
    在SUSv4中支持了内存流。这就是标准I/O流,虽然仍使用FILE指针进行访问,但起始并没有底层
    文件。所有的I/O都是通过在缓冲区与主存之间来回传送字节来完成的。我们将看到,即使这些流
    看起来像文件流,我们的某些特征使其更适合用于字符串操作。

    3个函数可用于内存流的创建,第一个是fmemopen函数:
    #include <stdio.h>
    FILE *fmemopen(void *restrict buf,size_t size,const char *restrict type);
    返回值:成功,返回流指针,出错,返回NULL

    fmemopen函数允许调用者提供缓冲区用于内存流:buf参数指向缓冲区的开始位置,size参数指向了缓冲区大小
    的字节数。如果buf参数为空,fmemopen函数分配size字节数的缓冲区。
    在这种情况下,当流关闭时缓冲区会被释放。
    type参数控制如何使用流:
    type            说明
    r或rb            为读而打开
    w或wb            为写而打开
    a或ab            追加:为在第一个null字节处写而打开
    r+或者r+b或rb+     为读和写而打开
    w+或w+b或wb+      把文件截断至0长,为读和写而打开
    a+或a+b或ab+      追加:为在第一个null字节处读和写而打开

    注意,这些取值对应于基于文件的标准I/O流的type参数取值,但其中有些微小差别。
    1、无论何时以追加写方式打开内存流时,当前文件位置设为缓冲区中的第一个null字节。如果缓冲区中不存在null
    字节,则当前位置就设为缓冲区结尾的后一个字节。当流并不是以追加写方式打开时,当前位置设为缓冲区的开始位置
    因为追加写模式通过第一个null字节确定数据的尾端,内存流并不适合存储二进制数据(二进制数据在数据尾端之前
    就可能包含多个null字节)。
    2、如果buf参数是一个null指针,打开流进行读或者写都没有任何意义。因为在这种情况下缓冲区是通过fmemopen进行
    分配的,没有办法找到缓冲区的地址,只写方式打开流意味者无法读取已写入的数据,同样,以读方式打开意味着只能
    读取那些我们写入的缓冲区的中的数据
    3、任何时候需要增加流缓冲区中数据量以及调用fclose、fflush、fseek、fseeko以及fsetpos是都会在当前位置写入
    一个null字节。

    实例:
    有必要看一下对内存流的写入是如何在我们自己提供的恶缓冲区上进行操作的:
        #include <stdio.h>
        #include <stdlib.h>

        #define BSZ 48

        int main()
        {
            FILE *fp;
            char buf[BSZ];

            memset(buf,'a',BSZ-2);
            buf[BSZ-2]='\0';
            buf[BSZ-1]='X';

            if((fp=fmemopen(buf,BSZ,"w+"))==NULL)
                printf("fmemopen failed");
            printf("inital buffer contents: %s\n",buf);
            fprintf(fp,"hello world");
            printf("before flush: %s\n",buf);
            fflush(fp);
            printf("after flush: %s\n",buf);
            printf("len of string in buf= %ld\n",(long)strlen(buf));

            memset(buf,'b',BSZ-2);
            buf[BSZ-2]='\0';
            buf[BSZ-1]='X';
            fprintf(fp,"hello wordld");
            fseek(fp,0,SEEK_SET);
            printf("after fseek: %s\n",buf);
            printf("len of string in buf= %ld\n",(long)strlen(buf));

            memset(buf,'c',BSZ-2);
            buf[BSZ-2]='\0';
            buf[BSZ-1]='X';
            fprintf(fp,"hello world");
            fclose(fp);
            printf("after fclose: %s\n",buf);
            printf("len of string in buf=%ld\n",(long)strlen(buf));

            return(0);


        }
        执行:
        ./beuffered
    这个例子给出了冲洗内存流和追加写null字节的策略。写入内存流以及推进流的内容大小(项对缓冲区大小而言。该
    大小是固定的)这个概念时,null字节会自动追加写。流内容大小是由写入多少来确定的。

    用于创建内存流的其他两个函数分别是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

    open_memstream函数创建的流是面向字节的,open_wmemstream函数创建的流是面向宽字节的。与fmemopen函数不同:
    1、创建的流只能写打开
    2、不能指定自己的缓冲区,但可以分别通过bufp和sizep参数访问缓冲区地址和大小。
    3、关闭流后需要自行释放缓冲区
    4、对流添加字节会增加缓冲区大小
    但是在缓冲区地址和大小的使用上必须遵循一些原则。
    1、缓冲区地址和长度只有在调用fclose或fflush后才有效
    2、这些值只有在下一次流写入或调用fclose前才有效。因为缓冲区可以增长,可能需要重新分配。如果出现这种情况,我们
    会发现缓冲区的内存地址值在下一次调用fclose或fflush是会改变。

猜你喜欢

转载自blog.csdn.net/lv_yong/article/details/79515865