标准I/O库由ISO C标准制定的。标准I/O库相对于系统I/O处理了很多细节,比如缓冲区分配、以优化的块大小执行I/O(这个数据存储在stat结构体中的st_blksize字段)等。

系统I/O是以文件描述符来作为基础展开的,而标准I/O是围绕流进行的,打开或创建文件时会将流和文件关联起来。

获取与流相关的文件描述符可以使用函数fileno()

流和文件相关联为文件流,流和内存关联起来为内存流。

流和FILE对象

首先,流是在标准C中定义的。

标准C想要使用统一的方式连接所有顺序数据源,如文件、键盘、显示器、打印机、socket等,因此设计了一个可以应用于所有数据源的接口,该接口拥有这些数据源的通用属性。为了方便称呼,将该接口称为流。

使用相同接口的好处在于可以使用同一套代码从不同的输入输出设备中读或写数据。

对于进程来说,打开文件则是将流连接到文件,此时流在该进程中是文件的逻辑对象,对该流写入数据即是写入数据到文件。因此在标准IO中通常将流指代为文件。

使用fopen函数打开一个流时,会返回一个指向FILE对象的指针,该指针是流的抽象类型。

该指针通常是一个结构,它包含了标准I/O库为管理该流所需要的信息,包括实际I/O的文件描述符、指向用于该缓冲区的指针、缓冲区的长度、当前在缓冲区的字符数以及出错标 志等。

不同的C标准实现比如GNN C、VC++等都必须包含标准I/O所需要的字段,其余字段可选。

下面是GNU C在/libio/bits/type/struct_FILE.h文件中对FILE结构体的定义:

/* The tag name of this struct is _IO_FILE to preserve historic
   C++ mangled names for functions taking FILE* arguments.
   That name should not be used in new code.  */
struct _IO_FILE
{
  int _flags;           /* High-order word is _IO_MAGIC; rest is flags. */

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;   /* Current read pointer */
  char *_IO_read_end;   /* End of get area. */
  char *_IO_read_base;  /* Start of putback+get area. */
  char *_IO_write_base; /* Start of put area. */
  char *_IO_write_ptr;  /* Current put pointer. */
  char *_IO_write_end;  /* End of put area. */
  char *_IO_buf_base;   /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */

  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

缓冲

标准I/O提供缓冲的目的是为了减少使用readwrite调用的次数。它对每个I/O流自动的进行缓冲管理。

缓冲类型参数说明
全缓冲_IOFBF填满标准缓冲区之后才进行实际的I/O操作。第一次I/O时获取缓冲区
行缓冲_IOLBF遇到换行符或填满了缓冲区执行I/O操作
无缓冲_IONBF标准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, 失败返回-1

参数说明如下图:

一般而言应由系统选择缓冲区的长度并自动分配缓冲区。(部分标准I/O实现会将流管理信息存放在缓冲区中,因此存放在缓冲区的实际数据小于size值)

缓冲区的写操作可由标准I/O自动冲洗,或者手动调用fflush函数冲洗。

#include <stdio.h>

int fflush(FILE* fp);
// 成功返回0, 失败返回-1

此函数会使该流所有未写的数据传送至内核,而不是直接写入到磁盘文件中。内核到磁盘之间还有一个页高速缓存。

如果参数为NULL,则所有输出流都会被冲洗。

下面的例子来验证缓冲区的相关功能:

#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fp = NULL;
    char data[] = "no buffer test data";
    char lineData[] = "line buffer data\n next line";
    char allData[] = "all buffer data\n next line\nlast data";
    /* 无缓冲 */
    if ((fp = fopen("./no_buffer.file", "w")) != NULL)
    {
        /* 使用setbuf函数设置无缓冲 */
        printf("start test setbuf no buffer\n");
        setbuf(fp, NULL);
        fwrite(data, 1, strlen(data), fp);
        printf("wrote data, please press any key continue\n");
        getchar();
        /* 此时查看./no_buffer.file文件的内容 会发现数据已写入 */
        fclose(fp);
    }

    if ((fp = fopen("./no_buffer.file1", "w")) != NULL)
    {
        /* 使用setvbuf设置无缓冲 */
        printf("start test setvbuf no buffer\n");
        if (setvbuf(fp, NULL, _IONBF, 0) == 0)
        {
            fwrite(data, 1, strlen(data), fp);
            printf("wrote data, please press any key continue\n");
            /* 此时查看./no_buffer.file1文件的内容 会发现数据已写入 */
            getchar();
        }
        fclose(fp);
    }
    /* 行缓冲 */
    if ((fp = fopen("./line_buffer.file", "w")) != NULL)
    {
        /* 使用setvbuf设置行缓冲 */
        printf("start set 128 bytes space for line buffer\n");
        char buf[128] = {0};
        if (setvbuf(fp, buf, _IOLBF, sizeof(buf)) == 0)
        {
            fwrite(lineData, 1, strlen(lineData), fp);
            printf("wrote lineData, please press any key continue\n");
            /* 此时查看./line_buffer.file文件的内容 会发现lineData中\n之前的数据已写入 */
            getchar();
        }
        fclose(fp);
        /* 此时查看./line_buffer.file文件的内容 会发现lineData中\n之后的数据已写入,因为关闭文件会冲洗流 */
    }
    /* 全缓冲 */
    if ((fp = fopen("./all_buffer.file", "w")) != NULL)
    {
        /* 使用setvbuf设置行缓冲 */
        printf("start set 128 bytes space for all buffer\n");
        char buf[128] = {0};
        if (setvbuf(fp, buf, _IOFBF, sizeof(buf)) == 0)
        {
            fwrite(allData, 1, strlen(allData), fp);
            printf("wrote allData, please press any key continue\n");
            /* 此时查看./all_buffer.file文件的内容 会发现allData数据没有写入 */
            getchar();
        }
        fclose(fp);
        /* 此时查看./all_buffer.file文件的内容 会发现allData数据才会写入 */
    }
}

下面是测试结果:

$ cat no_buffer.file
no buffer test data$ cat no_buffer.file1
no buffer test data$ cat line_buffer.file
line buffer data
$ cat line_buffer.file
line buffer data
 next line$ cat all_buffer.file
$ cat all_buffer.file
all buffer data
 next line
last data$

流的基本操作

打开流

#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);
/* 成功返回文件指针,失败返回-1 */
  • fopen函数打开pathname指定的文件,返回一个FILE指针。
  • freopen函数在一个指定的流上打开一个指定文件,若流已经打开,在先关闭流。一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出、标准错误。
  • fdopen函数取一个已有的文件描述符,并使一个标准的I/O流与该描述符关联。常用于创建管道和网络通信函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen打开。

type参数指定对该I/O流的读写方式,ISO C规定type参数可以由15中不同的值。分别是r, rbw, wba, abr+, r+b, rb+w+, w+b, wb+a+, a+b, ab+

当以读和写方式打开一个文件时,会有限制:

  • 如果中间没有fflushfseekfsetposrewind,则在输出的操作后面不能直接跟随输入。
  • 如果中间没有fseekfsetposrewind或者输入操作没有到达文件尾,在输入操作之后不能直接跟随输出。

POSIX.1规定创建权限位是666。进程可以通过umask来限制。

除非流连接终端设备,否则按系统默认,流被打开时是全缓冲的。

#include <stdio.h>
int fclose(FILE* fp);
/* 成功返回0, 失败返回-1 */

文件被关闭之前,冲洗缓冲区的输出数据。缓冲区的任何输入数据被丢弃。之后释放缓冲区。进程终止时执行相同操作。

如果有多个进程用标准I/O追加写方式打开同一文件,那么来自每个进程的数据都将正确地写入到文件中。这是因为每次在执行写操作时会先获取文件i节点的内容长度,写入到文件表项中,然后再写入。

下面使用代码验证

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    if (argc > 1)
    {
        FILE* fp = fopen("./multi_thread.file", "a");
        if (fp != NULL)
        {
            while (1)
            {
                fwrite(argv[1], 1, strlen(argv[1]), fp);
                fflush(fp);
                sleep(1);
            }
            fclose(fp);
        }
    }
}
/* 分别执行./multi_thread_write_file process1和./multi_thread_write_file process2*/
/* 执行结果如下: */
/* process1process1process1process1process1process1process1process2process1process2process1process2process1process2process1process2 */

定位流

有三种方法定位流,分别是:ftellfseek函数、ftellofseeko函数、fgetposfsetpos函数。

#include <stdio.h>

long ftell(FILE* fp);
/* ftell成功返回文件当前位置,失败返回-1L */
int fseek(FILE* fp long offset, int whence);
/* 成功返回0,失败返回-1*/
void rewind(FILE* fp);

二进制文件的文件位置从起始位置度量,以字节为单位。ftell用于二进制文件时,返回的就是字节位置。

fseek的参数whence用来设置解释offset的类型,取值分别为SEEK_SET、SEEK_CUR和SEEK_END。

rewind函数将流设置到文件的起始位置。

下面验证这三个函数的功能:

#include <stdio.h>

int main()
{
    FILE* fp = NULL;
    long curPos = 0;
    if ((fp = fopen("./fseek_test.file", "r")) != NULL)
    {
        /* 当前在文件头 */
        curPos = ftell(fp);
        printf("curPos=%ld\n", curPos);
        /* fseek 设置在文件头 */
        fseek(fp, 0, SEEK_SET);
        curPos = ftell(fp);
        printf("curPos=%ld\n", curPos);
        /* fseek 设置在文件尾 */
        fseek(fp, 0, SEEK_END);
        curPos = ftell(fp);
        printf("curPos=%ld\n", curPos);

        /* rewind设置到文件头 */
        rewind(fp);
        curPos = ftell(fp);
        printf("curPos=%ld\n", curPos);

        fclose(fp);
    }
}

下面是测试结果

$ cat fseek_test.file
Hello, world!
$ ./fseek_test
curPos=0
curPos=0
curPos=14 # Linux默认文本文件都会以\n结尾
curPos=0
#include <stdio.h>

off_t ftello(FILE* fp);
/* 成功返回当前文件位置,失败返回-1 */
int fseeko(FILE* fp, off_t offset, int whence);
/* 成功返回0,失败返回-1 */

ftello函数和ftell函数的唯一区别就是返回类型变为off_t,其他的完全一致,这是因为off_t可以根据架构不同设置为不同的位数。

在一些计算机架构中off_tlong均为32位,但是可以通过定义_FILE_OFFSET_BITS为64位,将off_t也设置位64位。

下面验证ftellofseeko的用法:

#include <stdio.h>
#include <unistd.h>

int main()
{
    FILE* fp = NULL;
    off_t curPos;

    if ((fp = fopen("./fseeko_test.file", "r")) != NULL)
    {
        /* 刚打开文件查看当前位置 */
        curPos = ftello(fp);
        printf("curPos=%ld\n", curPos);
        /* 使用fseeko定位 */
        if (fseek(fp, 0, SEEK_END) == 0)
        {
            curPos = ftello(fp);
            printf("curPos=%ld\n", curPos);
        }

        fclose(fp);
    }
}
/*
curPos=0
curPos=14
*/
#include <stdio.h>

int fgetpos(FILE *stream, fpos_t *pos);

int fsetpos(FILE *stream, const fpos_t *pos);
/* 成功返回0 失败返回-1 */

fgetpos函数将文件位置存放在pos指向的对象中,以后调用fsetpos函数时可以使用此值将流重新定位到该位置。

fsetpos函数和whence=SEEK_SET时的fseek函数等效。

下面是这两个函数的使用范例:

#include <stdio.h>
#include <string.h>

int main()
{
    FILE* fp = NULL;
    fpos_t curPos;
    char data[] = "append data";
    char data1[] = "again";
    if ((fp = fopen("./fsetpos_test.file", "r+")) != NULL)
    {
        /* 保存当前文件位置 */
        if (fgetpos(fp, &curPos) == 0)
        {
            /* 写入数据 */
            fwrite(data, 1, strlen(data), fp);
            fflush(fp);
            /* 恢复到之前的位置 */
            if (fsetpos(fp, &curPos) == 0)
            {
                /* 重新写入数据, 会覆盖之前的 */
                fwrite(data1, 1, strlen(data1), fp);
                fflush(fp);

                /* 重新恢复到之前的位置 */
                if (fsetpos(fp, &curPos) == 0)
                {
                    /* 读取文件内容并打印 */
                    char buf[1024] = {0};
                    if (fgets(buf, sizeof(buf), fp) != NULL)
                    {
                        printf("%s\n", buf);
                    }
                }
            }
        }
        fclose(fp);
    }
}

./fsetpos_test.file初始为空的情况下,输出结果是:

$ ./fsetpos_test
againd data

冲洗流

#include <stdio.h>

int flush(FILE* fp);
/* 成功返回0,失败返回-1 */

其他操作

类Unix系统与每个标准I/O流都有一个相对应的文件描述符,因为标准I/O流底层调用的是read,write函数。

#include <stdio.h>

int fileno(FILE* fp);
/* 返回与流相关的文件描述符 */
#include <stdio.h>

int main()
{
    printf("%d, %d, %d\n", fileno(stdin), fileno(stdout), fileno(stderr));
    FILE* fp = NULL;
    if ((fp = fopen("./fileno_test.file", "r")) != NULL)
    {
        printf("%d\n", fileno(fp));
        fclose(fp);
    }
}
/* 运行结果如下 */
// 0,1,2
// 3

下面查看3个标准流以及一个与普通文件关联的流的的缓冲的状态信息

#include <stdio.h>
/* 下面这些宏定义来自于glibc-2.35源码中的libio.h 文件 */
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000
static int is_unbuffered(FILE* fp)
{
    return (fp->_flags & _IO_UNBUFFERED);
}

static int is_linebuffered(FILE* fp)
{
    return (fp->_flags & _IO_LINE_BUF);
}

static int get_buffer_size_of_stream(FILE* fp)
{
    return (fp->_IO_buf_end - fp->_IO_buf_base);
}

static void print_buffer_stat(const char* streamName, FILE* fp)
{
    printf("stream=%s, ", streamName);
    if (is_unbuffered(fp))
        printf("unbuffered");
    else if (is_linebuffered(fp))
        printf("line buffered");
    else
        printf("full buffered");
    printf(", buffer size=%d\n", get_buffer_size_of_stream(fp));
}

int main()
{
    /* 在流上第一次执行I/O操作时会分配缓冲区 */
    FILE* fp = NULL;

    /* 在标准输出流第一次执行I/O操作 */
    fputs("Enter any character\n", stdout);

    /* 在标准输入流第一次执行I/O操作 */
    if (getchar() == EOF)
        printf("Input error\n");

    /* 在标准错误流第一次执行I/O操作 */
    fputs("Output one line standard error message\n", stderr);
    print_buffer_stat("stdin", stdin);
    print_buffer_stat("stdout", stdout);
    print_buffer_stat("stderr", stderr);

    if ((fp = fopen("/etc/passwd", "r")) != NULL)
    {
        /* 对流fp进行第一次I/O操作 */
        if (getc(fp) != EOF)
        {
            print_buffer_stat("/etc/passwd", fp);
        }
        fclose(fp);
    }
}

执行结果如下:

$ ./stream_buffer_stat
Enter any character

Output one line standard error message
stream=stdin, line buffered, buffer size=1024
stream=stdout, line buffered, buffer size=1024
stream=stderr, unbuffered, buffer size=1
stream=/etc/passwd, full buffered, buffer size=4096

$ ./stream_buffer_stat  </etc/group > std.out 2>std.err
$ cat std.out
Enter any character
stream=stdin, full buffered, buffer size=4096
stream=stdout, full buffered, buffer size=4096
stream=stderr, unbuffered, buffer size=1
stream=/etc/passwd, full buffered, buffer size=4096

根据上面的测试结果可以发现,Ubuntu20.04的系统默认是:当标准输入、标准输出连接至终端时,采用的是行缓冲,并且行缓冲的大小是1024字节。当连接到文件时,采用的是全缓冲,缓冲区大小是stat结构中的st_blocksize值。

非格式化I/O

非格式化I/O支持3中方式:

  • 每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
  • 每次一行的I/O。一次读写一行,每行以遇到\n终止。fgets需要指定能处理的最大长度。
  • 直接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)

这3个函数返回整型的理由是可以返回所有可能的字符值再加上一个已出错或已到达文件尾端的指示值。EOF常量被要求是一个负值,通常为-1。

但是无论是出错还是到达文件尾端,返回的都是同样的值。为了区分这两种情况,可以使用下面的函数:

#include <stdio.h>
int ferror(FILE* fp);
int feof(FILE* fp);
/* 条件为真返回非零值,条件为假返回0 */
void clearerr(FILE* fp);

大多数实现中,为每个流在FILE对象中维护了两个标志:出错标志和文件结束标志。 clearerr函数可以清除这两个标志。

从流中读取数据之后,可以调用ungetc将字符在压送回流中。

#include <stdio.h>

int ungetc(int c, FILE* fp);
/* 成功返回c, 失败返回EOF */

压送回流中的字符以后又可以从流中读出,但读出字符的顺序与压送回的顺序相反,类似栈一样后进先出。

ISO C允许实现任意次数的回送,但是一次只能会送一个字符。会送的字符不一定是上一次读出的字符。不能会送EOF。当到达文件尾端时,仍可以会送一个字符,下次读将返回这个字符,再读返回EOF。ungetc会清除文件结束标志。

单字符输出函数:

#include <stdio.h>
int putc(int c, FILE* fp);
int fputc(int c, FILE* fp);
int putchar(int c);
/* 成功返回c, 失败返回-1*/

下面是读写函数验证功能:

#include <stdio.h>
#include <string.h>
int main()
{
    int c;
    FILE* fp = NULL;
    if ((fp = fopen("./signal_character.file", "r+")) != NULL)
    {
        while ((c = fgetc(fp)) != EOF)
        {
            printf("%c", c);
        }
        printf("read end\n");
        /* 出错或已读到文件尾 */
        if (feof(fp))
        {
            printf("end file\n");
        }
        if (ferror(fp))
        {
            printf("read file error\n");
        }

        /* 压送字符重新读取,后进先出 */
        char data[] = "abcdefg";
        for (int i = 0; i < strlen(data); i++)
        {
            if (ungetc(data[i], fp) != EOF)
            {
                printf("put %c ok\n", data[i]);
            }
        }
        printf("read again\n");
        while ((c = fgetc(fp)) != EOF)
        {
            printf("%c", c);
        }
        printf("\nread end\n");

        /* 验证读完之后写入字符 */
        char data1[] = "ABCDEFG";
        for (int i = 0; i < strlen(data1); i++)
        {
            if (fputc(data1[i], fp) != EOF)
            {
                printf("%c wrote\n", data1[i]);
            }
        }

        /* 验证写入一半重新读 */
        char data2[] = "1234567890";
        for (int i = 0; i < strlen(data2) / 2; i++)
        {
            if (fputc(data2[i], fp) != EOF)
            {
                printf("%c wrote\n", data2[i]);
            }
        }
        printf("wrote %ld characters\n", strlen(data2) / 2);
        printf("start read character\n");

        printf("\n");
        fflush(fp);
        /* 需要重定位之后才可以读 */
        fseek(fp, 0, SEEK_SET);
        c = fgetc(fp);
        printf("%c", c);
        fclose(fp);
    }
}

执行结果如下:

$ ./signal_character_read_and_write
Hello, world
ABCDEFG12345ABCDEFG12345read end
end file
put a ok
put b ok
put c ok
put d ok
put e ok
put f ok
put g ok
read again
gfedcba
read end
A wrote
B wrote
C wrote
D wrote
E wrote
F wrote
G wrote
1 wrote
2 wrote
3 wrote
4 wrote
5 wrote
wrote 5 characters
start read character

H

行I/O

#include <stdio.h>

char* fgets(char* restrict buf, int n, FILE* restrict fp);
char* gets(char* buf);
/* 成功返回buf, 到达文件尾或出错返回NULL */

fgets函数需要指定缓冲的长度n

此函数会一直读到下一个换行符为止,会将读到的字符(包含换行符)写入缓冲中,但是如果包含换行符在内的字符数超过n-1,将会返回一个不完整的行。缓冲会以NUL字节结尾。

gets函数不推荐使用,因为不能指定缓冲区长度,容易造成缓冲区溢出。

#include <stdio.h>

int fputs(const char* restrict str, FILE* fp);
int puts(const char* str);
/* 成功返回非负值, 失败返回EOF */

函数fputs将一个以NUL字节结尾的字符串写入指定的流中,NUL之前的字符不一定必须是\n

putsfputs的区别在于将str字符串写到标准输出之后,又会再将换行符写到标准输出。

使用fgetsfputs需要牢记一点,需要自己手动处理换行符。

直接I/O

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
/* 返回读或写的对象数 */

常用于读写二进制数组和结构体。

fread如果出错或者到达文件尾端,返回数字可能小于nmemb,需要使用feof或者ferror进一步判断是出错还是到达文件尾端。

freadfwrite只能读写同一操作系统的数据,因为不同系统结构体的偏移量可能不同。

格式化I/O

格式化输出是由5个printf函数来处理的

#include <stdio.h>

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
/* 成功返回输出的字符数, 出错返回负值 */
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
/* 成功返回存入数组的字节数,出错返回负值 */

printf将格式化数据写到标准输出。

fprintf写到指定的流。

dprintf写至指定的文件描述符。

sprintf将格式化的字符送入数组strsnprintfsprintf相同。

sprintfsnprintf都会自动追加NUL字符,如果输入字符加上NUL结尾字符大于缓冲区长度,会触发编译警告并将输入字符截断。此时实际上能够写入的字符数为size-1

printf函数的va_list相关变体:

#include <stdarg.h>

int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vdprintf(int fd, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

执行格式化输入由3个函数:

#include <stdio.h>

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

#include <stdarg.h>

int vscanf(const char *format, va_list ap);
int vsscanf(const char *str, const char *format, va_list ap);
int vfscanf(FILE *stream, const char *format, va_list ap);

下面是这些函数的简单用例

#include <stdarg.h>
#include <stdio.h>

static void print(const char* format, ...)
{
    va_list vaList;
    va_start(vaList, format);
    vprintf(format, vaList);
    va_end(vaList);

    va_start(vaList, format);
    FILE* fp = NULL;
    if ((fp = fopen("./print_test.file", "w")) != NULL)
    {
        vfprintf(fp, format, vaList);
        fclose(fp);
    }

    va_end(vaList);
}

static void myscanf(int count, const char* format, ...)
{
    va_list vaList;
    va_start(vaList, format);

    int rc = vscanf(format, vaList);
    if (rc != count)
        printf("param count incorrect\n");
    va_end(vaList);
}

int main()
{
    print("%s:%d\n", __FUNCTION__, __LINE__);
    int val = 12;
    /* 从标准输入读取数据保存到va_list中 */
    myscanf(1, "%d", &val);
}

临时文件

标准库提供两个函数来创建临时文件:

#include <stdio.h>

char* tmpnam(char* ptr);
/* 返回指向唯一路径名的指针 */
FILE* tmpfile(void);
/* 成功返回流指针,失败返回NULL */

tmpnam函数每次调用时都会产生一个和现有文件名不同的有效路径字符串,最大调用TMP_MAX次。

ptr为NULL时产生的有效路径字符串保存在静态区中返回,此时需要保存该路径的副本,否则第二次调用会被覆盖。

当前编译时,tmpnam会触发警告,建议尽可能少的使用warning: the use of tmpnam’ is dangerous, better use mkstemp'

tmpfile会创建一个临时二进制文件,在关闭该文件或程序结束时自动被删除。

下面来验证上面两个函数的功能:

#include <stdio.h>

int main()
{
    printf("%d\n", TMP_MAX);
    char name[L_tmpnam] = {0};
    FILE* fp = NULL;

    /* 第一次调用tmpnam */
    printf("%s\n", tmpnam(NULL));
    tmpnam(name);
    printf("%s\n", name);

    if ((fp = tmpfile()) != NULL)
    {
        fputs("one line of output\n", fp);
        rewind(fp);
        char buf[1024] = {0};
        if (fgets(buf, sizeof(buf), fp) != NULL)
        {
            fputs(buf, stdout);
        }
        else
        {
            perror("fgets");
        }
        fclose(fp);
    }
    else
    {
        perror("tmpfile");
    }
}

执行结果如下:

$ ./tmp_test
238328
/tmp/fileMQQIzW
/tmp/fileuc02VA
/tmp/filemrx19B
one line of output

tmpfile函数使用的技术经常是先tmpnam产生一个路径名,然后用该路径名创建一个文件,操作符是w+b,然后unlink该文件。因为对文件解除连接并不会删除它,关闭该文件时才会删除其内容。

使用tmpnam的缺陷之一是在返回路径名和创建文件之间存在时间差,在时间差之中其他进程可能会使用该路径名创建文件。 为了避免这个问题要尽量使用tmpfile函数。

下面两个函数是SUS定义的处理临时文件:

#include <stdlib.h>

char* mkdtemp(char* template);
/* 成功返回指向目录名的指针,出错返回NULL */
int mkstemp(char* template);
/* 成功返回文件描述符 */

mkdtemp函数创建一个具有唯一名称的文件夹;mkstemp函数创建一个具有唯一名称的文件。

名称受参数template限制,template参数的最后六位是占位符XXXXXX,函数将占位符替换为不同的字符组成唯一路径名。

mkdtemp创建的临时目录具有权限S_IRUSR|S_IWUSR|S_IXUSR,调用进程可以使用文件模式创建屏蔽字进一步限制权限,创建成功mkdtemp返回目录名称。

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

mkstemp创建的临时文件并不会自动删除,如果想要关闭文件或退出进程删除的话,需要手动解除链接才行。

下面对这两个函数进行验证

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char template[] = "/tmp/dirXXXXXX";
    printf("Create temp directory\n");
    char* pDIR = NULL;
    if ((pDIR = mkdtemp(template)) != NULL)
    {
        printf("%s\n", pDIR);
    }

    char template1[] = "/tmp/fileXXXXXX";
    int fd = mkstemp(template1);
    if (fd == -1)
        perror("mkstemp");

    return 0;
}

执行结果如下所示:

$ ./mkdtemp
Create temp directory
/tmp/diriyKyd2
$ ls -l /tmp/
drwx------ 2 blduan blduan     4096 Apr  4 19:20 diriyKyd2
-rw------- 1 blduan blduan        0 Apr  4 19:20 filerAsdEK

由结果可以看出,进程退出创建的临时目录和文件也不会删除。

内存流

内存流指的是所有的I/O都是通过缓冲区与主存之间来回传送字节来完成,虽然仍使用FILE指针进行访问,但是并没有底层文件。

有3个函数可用于内存流的创建:

#include <stdio.h>

FILE* fmemopen(void* reestrict buf, size_t size, const char* restrict type);
/* 成功返回流指针,失败返回NULL */

参数说明:buf参数指向缓冲区的开始位置,size参数指定缓冲区的字节大小,type参数指定流的打开方式。

当以追加方式打开内存流时,当前文件位置设为缓冲区中的第一个null字节,如果流中不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流不是以追加方式打开,当前位置设为缓冲区的开始位置。因为追加写模式通过第一个null字节确定数据的尾端,内存流不适合存储二进制数据。

如果buf参数为空,打开流执行读或写操作没有任何意义。因为这种情况下缓冲区是由fmemopen函数分配的,没有办法找到缓冲区地址。

任何时候向流缓冲区中增加数据以及调用fclose, fflush, fseek, fseekos, fsetpos时都会在当前位置写入一个null字节,表示结尾。

#include <stdio.h>

int main()
{
    char buf[1024] = {0};
    FILE* fp = NULL;
    if ((fp = fmemopen(buf, sizeof(buf), "w+")) != NULL)
    {
        printf("init buf=%s\n", buf);
        fprintf(fp, "Hello,world");
        fflush(fp);
        printf("buf=%s\n",buf);

        fclose(fp);
    }
    else
        perror("fmemopen failed");
}

执行结果如下:

$ ./fmemopen_test
init buf=
buf=Hello,world