UNIX环境高级编程学习笔记(六)标准I/O库

时间:2022-07-20 22:17:23

1.流和FILE对象。

  • 文件I/O中诸如read、write的函数都是围绕文件描述符的,而标准I/O库中的I/O函数是围绕流进行的。当标准I/O库打开或创建一个文件时,我们使一个流与一个文件相关联。
  • 当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了I/O库为管理该流所需要的所有信息:用于实际I/O的文件描述符,指向流缓存的指针,缓存的长度,当前在缓存中的字符数,出错标志等。
  • 应用程序没有必要检验FILE对象。为了引用一个流,需将FILE指针作为参数传递给每个标准I/O函数。指向FILE对象的指针(类型为FILE*)称为文件指针。
  • 标准I/O文件流可用于单字节或多(宽)字节字符集。流的定向决定了所读或者写的字符是单字节或多字节的。流被创建时,它是未被定向的。若在一个未定向的流上使用一个多字节函数,则该流的定向设置为宽定向的。若在未定向流上使用一个单字节函数,则该流的定向设置为字节定向。
  • 只有两个函数能改变流的定向:freopen和fwide。前者清除流的定向,后者设置流的定向。

2.标准输入、标准输出和标准错误

  • 对一个进程预定义了3个流,这3个流可以自动被进程使用,它们是:标准输入、标准输出和标准错误。这3个标准I/O流通过预定义文件指针stdin、stdout和stderr引用,它们的定义在stdio.h头文件中。

3.缓冲

标准I/O提供3种类型缓冲:

  • 全缓冲:标准I/O缓冲区填满后才进行实际的I/O操作,对于驻在磁盘上的文件通常是由标准I/O库实施全缓存的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓存。可以调用fflush函数冲洗一个流。
  • 行缓冲:当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许一次输出一个字符(用标准I/O的fputc函数),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(例如标准输入和标准输出),典型地使用行缓存。缓冲区满时还没遇到换行符时也进行I/O。任何时候只要通过标准I/O库要求从(a)一个不带缓存的流,或者(b)一个行缓存的流(它预先要求从内核得到数据)得到输入数据,那么就会造成刷新所有行缓存输出流。
  • 不带缓冲:标准I/O库不对字符进行缓存,相当于用read、write系统调用进行I/O操作。

可以对I/O缓冲的类型:

#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);

这些函数一定要在流被打开后调用。

5.打开流
下列3个函数打开一个标准I/O流:

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);
  • fopen打开路径名由pathname指示的一个文件。
  • freopen在一个特定的流上(由fp指示)打开一个指定的文件(其路径名由pathname指示),如若该流已经打开,则先关闭该流。此函数一般用于将一个指定的文件打开为一个预定义的流: 标准输入、标准输出或标准出错。
  • fdopen取一个现存的文件描述符(我们可能从open,dup,dup2,fcntl或pipe函数得到此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通 道函数获得的插述符。因为这些特殊类型的文件不能用标准I/Ofopen函数打开,首先必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。

type参数指定对该I/O流的读、写方式,ISO C规定type参数有15种不同值:

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系统可以区分文本文件和二进制文件,但内核对这两种文件并不区分。
调用fclose关闭一个打开的流。

6.读和写流
一旦打开了流,则可在三种不同类型的非格式化I/O中进行选择,对其进行读、写操作,(printf和scanf为格式化I/O函数):

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

输入函数:

#include <stdio.h>
int getc(FILE* fp);
int fgetc(FILE* fp);
int getchar(void);

函数getchar等同于getc(stdin)。前两个函数的区别是getc可被实现为宏,而fgetc则不能实现为宏。

  • getc的参数不应当是具有副作用的表达式,因为它可能被计算多次。
  • 因为fgetc一定是个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传送给另一个函数。
  • 调用fgetc所需时间很可能长于调用getc,因为调用函数通常所需的时间长于调用宏。检验一下stdio.h头文件的大多数实现,从中可见getc是一个宏,其编码具有较高的工作效率。

这3个函数在出错或到达文件尾端时,都返回同样的值,为了区分不同情况,必须调用ferror或feof。

int ferror(FILE *fp);
int feof(FILE *fp);
void clearerr(FILE *fp);

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

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

调用clearerr可以清除这两个标志。
从流中读取数据后,可以调用ungetc将字符再压送回流中:

int ungetc(int c, FILE *fp);

当正在读一个输入流,并进行某种形式的分字或分记号操作时,会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将刚查看的字符送回,以便下一次调用getc时返回该字符。ungetc压送回字符时,并没有将它们写入到文件或设备,只是将它们写回到流的缓冲区。

输出函数:

int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);

与输入函数一样,putchar(c) 等同于putc(c, stdout),putc可被实现为宏,而fputc则不能实现为宏。

7.每次一行I/O

#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);

fgets和gets:

  • 对于fgets,必须指定缓存的长度n。此函数一直读到下一个换行符为止,但是不超过n-1个字符,读入的字符被送入缓存。该缓存以null字符结尾。如若该行,包括最后一个换行符的字符数超过n-1,则只返回一个不完整的行,而且缓存总是以null字符结尾。对fgets的下一次调用会继续读该行。
  • gets是一个不推荐使用的函数。问题是调用者在使用gets时不能指定缓存的长度。这样就可能造成缓存越界(如若该行长于缓存长度),写到缓存之后的存储空间中,从而产生不可预料的后果。
  • gets与fgets的另一个区别是,gets并不将换行符存入缓存中。

fputs和puts:

  • 函数fputs将一个以null符终止的字符串写到指定的流,终止符null不写出。注意,这并不一定是每次输出一行,因为它并不要求在null符之前一定是换行符。通常,在null符之前是一个换行符,但并不要求总是如此。
  • puts将一个以null符终止的字符串写到标准输出,终止符不写出。但是,puts然后又将一个换行符写到标准输出。
  • puts并不像它所对应的gets那样不安全。但是我们还是应避免使用它,以免需要记住它在最后又加上了一个换行符。如果总是使用fgets和fputs,那么就会熟知在每行终止处我们必须自己加一个换行符。

8.二进制I/O
二进制I/O一次读写若干个完整的结构,比起fputs和fgets,有些场景更加适合。

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 nobj, FILE *restrict fp);

这些函数有两个常见的用法:

  • 读或写一个二进制数组。例如,将一个浮点数组的第2~5个元素写至一个文件上,可以写作:
float data[10];
if(fwrite(&data[2], sizeof(float), 4, fp) != 4)
    err_sys("fwrite error");

其中,指定size为每个数组元素的长度,nobj为欲写的元素数。

  • 读或写一个结构。例如,可以写作:
struct{
    short count;
    long total;
    char name[NAMESIZE];
}item;

if(fwrite(&item, sizeof(item), 1, fp) != 1)
    err_sys("fwriteerror");

其中,指定size为结构的长度,nobj为1(要写的对象数)。

  • 将这两个例子结合起来就可读或写一个结构数组。为了做到这一点,size应当是该结构的sizeof,nobj应是该数组中的元素数。
  • fread和fwrite返回读或写的对象数。对于读,如果出错或到达文件尾端,则此数字可以少于nobj。此时,应调用ferror或feof以判断究竟是那一种情况。对于写,如果返回值少于所要求的nobj,则出错。

二进制I/O的基本问题是:它只能用于读在同一系统上已写的数据。原因是:

  • 同一成员的偏移量可能随编译程序和系统的不同而不同
  • 内存对齐方式不同
  • 用来存储多字节整数和浮点值得二进制格式在不同的系统结构间可能不同。

9.定位流

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

  • ftell和fseek。这两个函数都假定文件的位置可以存放在一个长整型中。
  • ftello和fseeko。它们使得文件偏移量可以不一定使用长整形,使用off_t数据类型代替了长整形。
  • fgetpos和fsetpos。这两个函数是新由ANSIC引入的。它们引进了一个新的抽象数据类型fpos_t,它记录文件的位置。在非UNIX系统中,这种数据类型可以定义为记录一个文件的位置所需的长度。需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos。

10.格式化I/O

格式化输出由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, ...);
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字节,但该字节不包括在返回值中。
  • sprintf可能造成缓冲区溢出。调用者应确保缓冲区足够大。
  • 若缓冲区足够大,snprintf返回写入缓冲区的字符数,不包括尾部null。若snprintf函数返回小于缓冲区长度n的正值,没有截断输出。若发生编码错误,返回负值。

格式说明控制其余参数如何编写和显示,每个参数按照转换说明编写,转换说明以%开始,除转换说明外的其他字符按原样输出。一个转换说明有4个可选部分:

%[flags][fldwidth][precision][lenmodifier]convtype

convtype不是可选的,它控制如何解释参数。
下列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);
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);

格式化输入处理的函数有3个:

#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, ...);

scanf族用于分析输入字符串,并将字符序列转换成指定类型的变量。在格式之后的各参数包含了变量的地址,用转换结果对这些变量赋值。

格式说明控制如何转换参数,以便对它们赋值。转换说明以%开始。除转换说明和空白字符外,格式字符串中的其他字符必须与输入匹配。若有一个字符不匹配,则停止后续处理,不再读输入的其余部分。一个转换说明有3个可选部分:

%[*][fldwidth][m][lenmodifier]convtype

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

11.实现细节

标准I/O库最终都要调用文件I/O的系统调用。每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数获得其描述符。fileno不是ISO C标准部分,而是POSIX.1支持的扩展。

#include <stdio.h>
int fileno(FILE *fp);

12.临时文件

标准I/O库提供两个函数以帮助创建临时文件:

#include <stdio.h>
char *tmpnam(char *ptr);
FILE *tmpfile(void);

tmpnam产生一个与现在文件名不同的一个有效路径名字符串。每次调用它时,它都产生一个不同的路径名,最多调用次数是TMP_MAX。
tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件。注意,UNIX对二进制文件不作特殊区分。tmpfile函数经常使用的标准UNIX技术是先调用tmpnam产生一个唯一的路径名,然后立即unlink它。

Single UNIX Specification为处理临时文件定义另外两个函数:

#include <stdlib.h>
char *mkdtemp(char *template);
int mkstemp(char *template);

13.内存流
内存流通过FILE指针进行访问,但是并没有底层文件。所有的I/O都是通过在缓冲区与主存之间传送字节完成。
有3个函数可以用于创建内存流:

#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
FILE *open_memstream(char **bufp, size_t *sizep);
#include <wchar.h>
FILE *open_wmemstream(wchar **bufp, size_t *sizep);