如果你是在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:
- gets不检查缓冲区,所以是有安全风险的,极力不推荐使用,毕竟fgets可以完全替代它的功能,并且不会引入安全问题。
- 一般,这四个函数就推荐使用fgets和fputs,不推荐使用gets和puts。当然puts没有gets那样的安全风险,但是因为这四个函数对于换行符的不同处理,导致我们很容易在使用的时候产生混淆:gets和puts不管处理的数据里面有没有换行符,都会强制在后面加一个换行符。而fgets和fputs则不会在后面加换行符。所以只使用fgets和fputs我们也就只需要记住这两个函数不会加换行符就行了,不需要记其它的(当然,这四个函数都会在后面加结束符)。
- 因为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里面的三种类型的缓冲区:
- 无缓冲(unbuffered)——一有信息就马上写到目标文件或者显示到终端上面。
- 块缓冲(block buffered)——又称全缓冲,现将数据写到一个block里面,当block满了以后才写到文件或者终端。
- 行缓冲(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使用频繁的库了,虽然里面有很多的函数,但是使用都相对简单。
评论已关闭