如果你是在Linux环境下使用标准C的I/O,并且想了解一下标准I/O背后的细节,可以先看一下我的另外一篇文章《Linux文件I/O》。

1. 类型

ANSI C标准库包括了一些与流相关的标准I/O函数,这些函数都定义在<stdio.h>头文件中,同时,也定义了一些流相关的类型。本文一一介绍。

1.1 FILE文件类型

FILE是stdio.h中定义的一种派生类型,这个类型会记录一些打开的文件的相关信息,比如缓冲区信息等。当我们用标准I/O打开或创建一个文件时,标准I/O函数就返回一个指向FILE对象的指针(FILE *),它包含了标准I/O库为管理该流所需要的所有信息:用于实际I/O文件的描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。该FILE对象会将文件和一个流(stream)关联起来,我们后续的操作也都是围绕这个流/FILE对象进行的。虽然FILE结构比较复杂,但这对于我们来说是透明的,我们不需要太关注FILE对象内部的细节。下面是FILE结构在Linux上面的一种实现:

struct _IO_FILE {
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  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;
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

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

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
};

typedef struct _IO_FILE FILE;

1.2 文件结束符EOF

EOF是一个特殊的值,一般定义为-1,一般用来表示文件的结尾。

1.3 标准输入/标准输出/标准错误

stdio.h文件把3个文件指针(stdin、stdout、stderr)与3个C程序自动打开的标准文件进行了关联,这些指针都是FILE指针类型,所以可以被用作标准I/O的参数。

2. 函数

2.1 错误相关类函数

函数原型:

void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);
int fileno(FILE *stream);

void perror(const char *s);

函数功能:

  • clearerr清除文件结尾(EOF)和错误指示器。
  • feof测试文件结尾,如果设置了文件结尾,返回非0.只有clearerr可以清除文件结尾指示器。
  • ferror测试错误指示器,如果设置了设置了指示器,返回非0.clearerr可以清除错误指示器。
  • fileno返回stream流的整数描述符。
  • perror把系统错误信息写到标准错误中

2.2 读写类函数

I/O主要就是读写,所以包含了非常多的读写类函数,这里简要列举,不做一一的详细介绍。 函数原型:

int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);
int ungetc(int c, FILE *stream);

int fputc(int c, FILE *stream);
int fputs(const char *s, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
int puts(const char *s);

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

#include <stdarg.h>

int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, 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);

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);

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);

函数功能:

  • fgetc从流中读取下一个字符(unsigned char),然后转换为int类型返回。遇到EOF或者错误时停止。
  • getc和fgetc相同,不过getc一般都实现为宏,而不是函数。
  • getchar从标准输入中获取一个字符,等价于getc(stdin)。
  • gets从标准输入读取一行,存储到s指向的内存里面去,遇到换行或者EOF时停止,并给字符串后面加上字符串结束符。但是gets函数不检查s缓冲区是否可以容纳读取到的字符串,不推荐使用。
  • fgets与gets类似,不过他指定了要读取的字符数目,比较安全。
  • ungetc将字符c加到stream流的后面。
  • fputc把字符c写到指定的流中。
  • fputs把字符串s写到指定的流中,不包括字符串结束符。
  • putc和fputc等价,不过putc一般都被实现为宏。
  • putchar将字符c输出到标准输出流中,等价于putc( c, stdout ).
  • puts把字符串写到标准输出中,并且会在字符串后面加换行符。
  • printf族的函数把格式化的输出写到标准输出中;printf和vprintf把输出写到标准输出中;fprintf和vfprintf把输出写到指定的输出流中;sprintf、snprintf、vsprintf和vsnprintf把输出写到字符串str中。snprintf和vsnprintf指定了要输出到字符串str的字节数(包括字符串结束符)。v开头的printf族函数与其他printf族函数功能一样,惟一的差别是他们使用va_list类型作为参数,而不是可变数量的参数;它们会调用va_arg宏来获取va_list中的每个变量,调玩之后ap的值是未定义的。但是这些函数都不会调用va_end宏。
  • scanf族的函数读取格式化的输入;各个函数之前的区别于printf函数族对应,这里不再赘述。
  • fread从指定的流中读取二进制数据。
  • fwrite把二进制数据写到指定的流中。

NB:上面虽然有很多,但是经常使用的只有几个。这里想重点强调一下四个函数:gets、fgets、puts、fputs:

  1. gets不检查缓冲区,所以是有安全风险的,极力不推荐使用,毕竟fgets可以完全替代它的功能,并且不会引入安全问题。
  2. 一般,这四个函数就推荐使用fgets和fputs,不推荐使用gets和puts。当然puts没有gets那样的安全风险,但是因为这四个函数对于换行符的不同处理,导致我们很容易在使用的时候产生混淆:gets和puts不管处理的数据里面有没有换行符,都会强制在后面加一个换行符。而fgets和fputs则不会在后面加换行符。所以只使用fgets和fputs我们也就只需要记住这两个函数不会加换行符就行了,不需要记其它的(当然,这四个函数都会在后面加结束符)。
  3. 因为fgets使用比较多,再强调一下:fgets从文件结构体指针stream中读取数据,每次读取一行。读取的数据保存在buf指向的字符数组中,每次最多读取bufsize-1个字符(第bufsize个字符赋''),如果文件中的该行,不足bufsize个字符,则读完该行就结束。如若该行(包括最后一个换行符)的字符数超过bufsize-1,则fgets只返回一个不完整的行,但是,缓冲区总是以NULL字符结尾,对fgets的下一次调用会继续读该行。函数成功将返回buf,失败或读到文件结尾返回NULL。因此我们不能直接通过fgets的返回值来判断函数是否是出错而终止的,应该借助feof函数或者ferror函数来判断。

2.3 文件操作类函数

函数原型:

/* 文件打开、关闭 */
FILE *fopen(const char *path, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);

int fclose(FILE *fp);

int fflush(FILE *stream);

int rename(const char *old, const char *new);

FILE *tmpfile(void);
char *tmpnam(char *s);

int remove(const char *pathname);

函数功能:

  • fopen打开path指向的文件,并将其与一个流联系在一起。mode可选的值有:r(只读)、r+(从文件开头写,文件必须已经存在)、w(先把文件清空再写,文件若不存在则创建)、w+(打开文件进行读写,其他与w相同)、a(打开文件进行追加写,文件不存在则创建)、a+(打开文件进行读或者追加写,文件不存在则创建;读的时候从文件开始读,写的时候追加写在文件后面)。fdopen将一个流和一个已经存在的文件描述符绑定到一起。对于该函数,有两点必须注意:(1)流的模式(mode)必须和文件描述符的模式兼容。(2)该函数会将错误指示器和文件结尾指示器(EOF)清除掉,所以w和w+模式都不会将文件情况。freopen函数打开path指向的文件并将其与stream流绑定起来,原来的stream流将被关闭(如果存在的话),mode与fopen相同。
  • fclose关闭指定的文件。
  • fflush刷新指定的文件:(1)对于输出流,fflush函数调用流底层的写函数将用户空间缓冲区的数据进行一次强制写。(2)对于输入流,fflush函数将丢弃从底层获取到的存在缓冲区中还未被程序使用的数据。流的打开状态不受影响。
  • rename为指定的文件重命名。
  • tmpfile函数以二进制读写模式(w+b)创建一个惟一的临时文件。当文件关闭或者程序结束时文件自动被删除。
  • tmpnam函数为临时文件产生一个惟一的名字。
  • remove函数删除一个文件或者目录:remove调用unlink删除文件,调用rmdir删除目录。

2.4 定位文件位置类函数

函数原型:

int fseek(FILE *stream, long offset, int whence);

long ftell(FILE *stream);

void rewind(FILE *stream);

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

函数功能:

  • fseek把文件位置指针设置为指定的值。这个指定的值这样计算:whence指定的位置加上偏移量offset,都是以字节为单位进行计算的。而whence可取三个值:_SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件结尾)_。函数调用成功后将清除文件结束符(EOF)。
  • ftell获取当前的流的文件位置。
  • rewind把文件位置指针设置到文件开始,相当于(void) fseek(stream, 0L, SEEK_SET)。
  • fgetpos和fsetpos是等价于ftell和fseek的两个可选的函数接口(whence设置为SEEK_SET)。

2.5 缓冲区设置相关

函数原型:

void setbuf(FILE *stream, char *buf);

void setbuffer(FILE *stream, char *buf, size_t size);

void setlinebuf(FILE *stream);

int setvbuf(FILE *stream, char *buf, int mode, size_t size);

函数功能: 介绍这些函数之前先介绍一下标准I/O里面的三种类型的缓冲区

  1. 无缓冲(unbuffered)——一有信息就马上写到目标文件或者显示到终端上面。
  2. 块缓冲(block buffered)——又称全缓冲,现将数据写到一个block里面,当block满了以后才写到文件或者终端。
  3. 行缓冲(line buffered)——将数据缓冲下来直到遇到新行(换行符),比如标准输入就是典型的行缓冲。

一般情况下,所有的文件操作都使用块缓冲;当某个流与终端联系在一起的时候(一般是标准输出stdout),使用行缓冲。标准出错流stderr默认总是无缓冲。下面我们来介绍函数:

  • setvbuf函数用于改变打开的流的缓冲区类型,mode有三种选项:(1)_IONBF:无缓冲 (2)IO_LBF:行缓冲 (2)IOFBF:块缓冲/全缓冲(fully buffered). 除了不缓冲(unbuffered)的文件以外,buf指向一个至少size字节大小的空间,这个空间将用来代替原来的缓冲区。如果buf为NULL的话,只有mode起作用,新的缓冲区将在下一次读写操作时由malloc分配。setvbuf函数使用的时机一般是流被打开但还没有任何操作作用在流上面。
  • setbuf函数设置缓冲区的位置和大小,等价于setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
  • setbuffer函数和setbuf类似,区别在于缓冲区的大小由调用者设置,而不是使用默认值BUFSIZ。
  • setlinebuf函数等价于setvbuf(stream, NULL, _IOLBF, 0);

3. 总结

标准I/O库应该使我们使用C使用频繁的库了,虽然里面有很多的函数,但是使用都相对简单。