> 文章列表 > UNIX环境高级编程——标准I/O库

UNIX环境高级编程——标准I/O库

UNIX环境高级编程——标准I/O库

5.1 引言

本章讲述标准I/O库,这个库由ISO C标准说明。

5.2 流和FILE对象

  • 对于标准I/O库,它们的操作是围绕(stream)进行的,当用标准I/O库打开或创建一个文件时,就使一个流与一个文件关联;
  • 标准I/O文件流可用于单字节或多字节(“宽”)字符集,流的定向决定了所读、写的字符是单字节还是多字节的,当一个流最初被创建时,它并没有定向。

fwide函数可用于设置流的定向:

#include <stdio.h>
#include <wchar.h>int fwide(FILE *fp, int mode);// 返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;若流是未定向的,返回0 
  • 如若mode参数值为负,fwide将试图使指定的流是字节定向的;
  • 如若mode参数值为正,fwide将试图使指定的流是宽定向的;
  • 如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值。

注意:

  • fwide不改变已定向流的定向;
  • fwide无出错返回,在调用fwide前先清除errno,返回时检查errno的值。

当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流缓冲区的指针加粗样式缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。

5.3 标准输入、标准输出和标准错误

对一个进程预定义了3个流,并且这3个流可以自动地被进程使用,它们分别是:标准输入标准输出标准错误,分别通过预定义文件指针stdinstdoutstderr加以引用。

5.4 缓冲

标准I/O提供了以下3种类型的缓冲:

  • 全缓冲:在填满标准I/O缓冲区后才进行实际I/O操作;
  • 行缓冲:在输入和输出中遇到换行符时,标准I/O库执行I/O操作;
  • 不带缓冲:标准I/O库不对字符进行缓冲存储。

可以调用setbufsetvbuf来更改缓冲类型:

#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;若出错,返回非0
  • 可使用setbuf函数打开或关闭缓冲机制,参数buf必须指向一个长度位BUFSIZ的缓冲区;为了关闭缓冲,将buf设置位NULL
  • setvbuf可以根据mode精确说明所需的缓冲类型:
mode 缓冲
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 不带缓冲

UNIX环境高级编程——标准I/O库
可以使用fflush强制冲洗一个流:

#include <stdio.h>int fflush(FILE *fp);// 返回值:若成功,返回0;若出错,返回EOF

此函数使该流所有未写的数据都被传送至内核,特别的,如若fpNULL,则此函数将导致所有输出流被冲洗。

5.5 打开流

下列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个函数的区别如下:

  • fopen函数打开路径名为pathname的一个指定的文件;
  • freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流;若该流已经定向,则使用freopen清除该定向;此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误;
  • fdopen函数取一个已有的文件描述符,并使一个标准的I/O流与该描述符相结合;此函数常用于由创建管道和网络通信通道函数返回的描述符。

type参数指定对该I/O流的读、写方式,共有15种不同的值:
UNIX环境高级编程——标准I/O库

  • 除非流引用终端设备,否则按系统默认,流被打开时是全缓冲的;
  • 若流引用终端设备,则该流是行缓冲的。

调用fclose关闭一个打开的流:

#include <stdio.h>int fclose(FILE *fp);// 返回值:若成功,返回0;若出错,返回EOF
  • 在该文件被关闭之前,冲洗缓冲中的输出数据;
  • 缓冲区中的任何输入数据被丢弃;
  • 如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区;
  • 当一个进程正常终止时,则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。

5.6 读和写流

一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对其进行读、写操作:

  • 每次一个字符的I/O:一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲;
  • 每次一行的I/O:如果想要一次读或写一行,则使用fgetsfputs,每行都以一个换行符终止,当调用fgets时,应说明能处理的最大行长;
  • 直接I/Ofreadfwrite函数支持这种类型的I/O,每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度,这两个函数常用于从二进制文件中每次读或写一个结构。

1. 输入函数

以下3个函数可用于一次读一个字符:

#include <stdio.h>int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);// 3个函数的返回值:若成功,返回下一个字符;若已到达文件尾端或出错,返回EOF
  • 函数getchar等同于getc(stdin)
  • getc可被实现为宏,而fgetc不能实现为宏;
  • 在<stdio.h>中的常量EOF被要求是一个负值,其值通常是-1。

不管是出错还是到达文件尾端,这3个函数都返回同样的值,为了区分这两种不同的情况,必须调用ferrorfeof

#include <stdio.h>int ferror(FILE *fp);
int feof(FILE *fp);// 两个函数的返回值:若条件为真,返回非0(真);否则,返回0(假)

在大多数实现中,为每个流在FILE对象中维护了两个标志:

  • 出错标志;
  • 文件结束标志。

调用clearerr可以清除这两个标志:

#include <stdio.h>void clearerr(FILE *fp);

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

#include <stdio.h>int ungetc(int c, FILE *fp);// 返回值:若成功,返回c;若出错,返回EOF

2. 输出函数

#include <stdio.h>int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);// 3个函数返回值:若成功,返回c;若出错,返回EOF
  • putchar(c)等同于putc(c, stdout)
  • putc可被实现为宏,而fputc不能实现为宏。

5.7 每次一行I/O

下面两个函数提供每次输入一行的功能:

#include <stdio.h>char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);// 两个函数返回值:若成功,返回buf;若已到达文件尾端或出错,返回NULL
  • gets从标准输入读,fgets从指定的流读;
  • fgets必须指定缓冲区的长度n,此函数一直读到下一个换行符为止,但不超过n-1个字符,读入的字符被送入缓冲区;
  • 读缓冲区以null字节结尾。

fputsputs提供每次输出一行的功能:

#include <stdio.h>int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);// 两个函数返回值:若成功,返回非负值;若出错,返回EOF
  • fputs将一个以null字节终止的字符串写到指定的流,尾端的终止符null不写出;
  • puts将一个以null字节终止的字符串写到标准输出,终止符不写出,但puts随后又将一个换行符写到标准输出。

5.8 标准I/O的效率

  • exit函数将会冲洗任何未写的数据,然后关闭所有打开的流;
  • 系统调用与普通的函数调用相比需要花费更多的时间;
  • 标准I/O库与直接调用readwrite函数相比并不慢很多。

5.9 二进制I/O

二进制I/O操作,一次读或写一个完整的结构,下列两个函数用于执行I/O操作:

#include <stdio.h>size_t fread(void *restrict buf, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);// 两个函数的返回值:读或写的对象数
  • 对于读,如果出错或到达文件尾端,则返回值可以少于nobj,在这种情况下,应调用ferrorfeof以判断究竟是哪一种情况;
  • 对于写,如果返回值少于所要求的nobj,则报错。

5.10 定位流

有3种方法定位标准I/O流:

  • ftellfseek函数;
  • ftellofseeko函数;
  • fgetposfsetpos函数。
#include <stdio.h>long ftell(FILE *fp);// 返回值:若成功,返回当前文件位置指示;若出错,返回-1L
int fseek(FILE *fp, long offset, int whence);// 若成功,返回0;若出错,返回-1
void rewind(FILE *fp);
  • 对于一个二进制文件,其文件位置指示器是从文件起始位置开始度量,并以字节为度量单位的;
  • fseekwhence参数的可选值:SEEK_SET表示从文件的起始位置开始,SEEK_CUR表示从当前文件位置开始,SEEK_END表示从文件的尾端开始;
  • 为了定位一个文本文件,whence一定要是SEEK_SET,而且offset只能有两种值:0(后退到文件的起始位置)或是对该文件的ftell所返回的值;
  • rewind函数将一个流设置到文件的起始位置。
#include <stdio.h>off_t ftello(FILE *fp);// 返回值:若成功,返回当前文件位置;若出错,返回(off_t)-1
int fseeko(FILE *fp, off_t offset, int whence)// 返回值:若成功,返回0;若出错,返回-1
#include <stdio.h>int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos)// 两个函数返回值:若成功,返回0;若出错,返回非0
  • fgetpos将文件位置指示器的当前值存入由pos指向的对象中,在以后调用fsetpos时,可以使用此值将流重新定位至该位置。

5.11 格式化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 format, ...);// 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中,在该数组的尾端自动添加一个null字节,但该字符不包括在返回值中
  • snprintfsprintf多了一个参数n,它指明了缓冲区长度,超过缓冲区尾端写的所有字符都被丢弃。

每个参数按照转换说明编写,转换说明以百分号%开始,除转换说明外,格式字符串中的其他字符将按原样,不经任何修改被复制输出。一个转换说明有4个可选择的部分,下面将它们都展示于方括号中:
%[flags][fldwidth][precision][lenmodifier]convtype

  • flags标志如下图所示:
    UNIX环境高级编程——标准I/O库
  • fldwidth说明最小字段宽度,转换后参数字符数若小于宽度,则多余字符位置用空格填充,字段宽度是一个非负十进制数,或是一个星号(*);
  • precision说明整型转换后最少输出数字位数、浮点数转换后小数点后的最少位数、字符串转后最大字节数,精度是一个点(.),其后跟随一个可选的非负十进制数或一个星号(*);
  • lenmodifier说明参数长度,其可能值如下图:
    UNIX环境高级编程——标准I/O库
  • convtype不是可选的,它控制如何解释参数,下图列出了各种转换类型字符:
    UNIX环境高级编程——标准I/O库
    下列5种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);// 函数返回值:若缓冲区足够大,返回存入数组的字符数;若编码出错,返回负值

2. 格式化输入

执行格式化输入处理的是3个scanf函数:

#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, ...);// 3个函数返回值:赋值的输入项数;若输入出错或在任一转换前已到达文件尾端,返回EOF
  • scanf族用于分析输入字符串,并将字符序列转换成指定类型的变量;
  • 在格式之后的各参数包含了变量的地址,用转换结果对这些变量赋值;

格式说明控制如何转换参数,以便对它们赋值,转换说明以百分号%字符开始,除转换说明和空白字符外,格式字符串中的其他字符必须于输入匹配。一个转换说明有3个可选择的部分,下面将它们都示于方括号中:
%[*][fldwidth][m][lenmodifier]convtype

  • 可选择的星号(*)用于抑制转换,按照转换说明的其余部分对输入进行转换,但转换结果并不存放在参数中;
  • fldwidth说明最大宽度(即最大字符数);
  • lenmodifier说明要用转换结果赋值的参数大小,由printf函数族支持的长度修饰符同样得到scanf族函数的支持;
  • convtype说明转换类型,scanf族函数支持的转换类型如下图:
    UNIX环境高级编程——标准I/O库
    printf族相同,scanf族也使用由<stdarg.h>说明的可变长度参数表:
#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);// 3个函数返回值:指定的输入项目数;若输入出错或在任一转换前文件结束,返回EOF

5.12 实现细节

每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数以获得其描述符:

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

5.13 临时文件

ISO C标准I/O库提供了两个函数来创建临时文件:

#include <stdio.h>char *tmpnam(char *ptr);// 返回值:指向唯一路径名的指针
FILE *tmpfile(void);// 返回值:若成功,返回文件指针;若出错,返回NULL
  • tmpnam函数产生一个与现有文件名不同的一个有效路径名字符串,每次调用它时,都产生一个不同的路径名;
  • ptrNULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回,后续调用tmpnam时,会重写该静态区;
  • ptr不是NULL,则认为它应该是指向长度至少是L_tmpnam个字符的数组,所产生的路径名存放在该数组中,ptr也作为函数值返回;
  • tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件。

Single UNIX Specification为处理临时文件定义了另外两个函数,即mkdtempmkstemp

#include <stdlib.h>char *mkdtemp(char *template);// 返回值:若成功,返回指向目录名的指针;若出错,返回NULL
int mkstemp(char *template);// 返回值:若成功,返回文件描述符;若出错,返回-1
  • mkdtemp函数创建了一个目录,该目录有一个唯一的名字;
  • mkstemp函数创建了一个文件,该文件有一个唯一的名字;
  • 名字通过template字符串进行选择,这个字符串是后6位设置为XXXXXX的路径名,函数将这些占位符替换成不同的字符来构建一个唯一的路径名,如果成功的话,这两个函数将修改template字符串反映临时文件的名字;
  • mkdtemp函数创建的目录使用下列访问权限位集:S_IRUSR | S_IWUSR | S_IXUSR
  • mkstemp函数创建的文件使用访问权限位:S_IRUSR | S_IWUSR
  • mkstemp创建的临时文件并不会自动删除。

5.14 内存流

有3个函数可用于内存流的创建,第一个是fmemopen函数:

#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);// 返回值:若成功,返回流指针;若错误,返回NULL
  • buf参数指向缓冲区的开始位置,size参数指定了缓冲区大小的字节数,如果buf参数为空,fmemopen函数分配size字节的缓冲区,在这种情况下,当流关闭时缓冲区会被释放;
  • type参数控制如何使用流,可能取值如下:
    UNIX环境高级编程——标准I/O库

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

  • 创建的流只能写打开;
  • 不能指定自己的缓冲区,但可以分别通过bufpsizep参数访问缓冲区地址和大小;
  • 关闭流后需要自行释放缓冲区;
  • 对流添加字节会增加缓冲区大小。

5.15 标准I/O的替代软件

5.16 实例代码

chapter5