Unix之标准IO库(五)

本文主体内容来自《UNIX环境高级编程第三版》。

标准I/O库是由Dennis Ritchie在1975年左右编写的。令人惊讶的是,35年来,几乎没有对标准I/O库进行修改。


流和FILE对象

在第3章中,所有I/O函数都是围绕文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的I/O操作。而对于标准I/O库,它们的操作是围绕流进行的。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。

对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于单字节或多字节(“宽”)字符集。流的定向(stream’s orientation)决定了所读、写的字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节 I/O 函数(见<wchar.h>),则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流的定向设为字节定向的。只有两个函数可改变流的定向。 freopen 函数清除一个流的定向;fwide 函数可用于设置流的定向。

1
2
3
4
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
// 返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;若流是未定向的,返回0

根据mode参数的不同值,fwide 函数执行不同的工作。

  • 如若mode参数值为负,fwide 将试图使指定的流是字节定向的。

  • 如若mode参数值为正,fwide 将试图使指定的流是宽定向的。

  • 如若mode参数值为0,fwide 将不试图设置流的定向,但返回标识该流定向的值。

注意,fwide 并不改变已定向流的定向。还应注意的是,fwide 无出错返回。试想,如若流是无效的,那么将发生什么呢?我们唯一可依靠的是,在调用 fwide 前先清除 errno,从 fwide 返回时检查errno的值。

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


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

对一个进程预定义了 3 个流,并且这 3 个流可以自动地被进程使用,它们是:标准输入、标准输出和标准错误。这些流引用的文件与文件描述符 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO 所引用的相同。

这3个标准I/O流通过预定义文件指针 stdinstdoutstderr 加以引用。这3个文件指针定义在头文件<stdio.h>中。


缓冲

标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。

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

(1)全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用 malloc 获得需使用的缓冲区。

术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在 UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上。在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中的数据。

(2)行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。

(3)不带缓冲。标准I/O库不对字符进行缓冲存储。例如,若用标准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符能立即输出。

标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来。

对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更改缓冲类型。

1
2
3
4
#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 函数打开或关闭缓冲机制。为了带缓冲进行 I/O,参数buf必须指向一个长度为 BUFSIZ 的缓冲区(该常量定义在<stdio.h>中)。通常在此之后该流就是全缓冲的,但是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关闭缓冲,将buf设置为NULL。

使用 setvbuf,我们可以精确地说明所需的缓冲类型。这是用mode参数实现的:

_IOFBF 全缓冲

_IOLBF 行缓冲

_IONBF 不带缓冲

如果指定一个不带缓冲的流,则忽略buf和size参数。如果指定全缓冲或行缓冲,则buf和size可选择地指定一个缓冲区及其长度。如果该流是带缓冲的,而buf是NULL,则标准I/O库将自动地为该流分配适当长度的缓冲区。适当长度指的是由常量 BUFSIZ 所指定的值。

下图列出了这两个函数的动作,以及它们的各个选项。

img

任何时候,我们都可强制冲洗一个流。

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

此函数使该流所有未写的数据都被传送至内核。作为一种特殊情形,如若fp是NULL,则此函数将导致所有输出流被冲洗。


打开流

下列3个函数打开一个标准I/O流。

1
2
3
4
5
#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个函数的区别如下。

(1)fopen 函数打开路径名为 pathname 的一个指定的文件。

(2)freopen 函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用 freopen 清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。

(3)fdopen 函数取一个已有的文件描述符,并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用 fdopen 使一个标准I/O流与该描述符相结合。

type 参数指定对该I/O流的读、写方式,ISO C规定type参数可以有15种不同的值,如图所示。

img

使用字符b作为type的一部分,这使得标准I/O系统可以区分文本文件和二进制文件。因为UNIX内核并不对这两种文件进行区分,所以在UNIX系统环境下指定字符b作为type的一部分实际上并无作用。

对于 fdopen,type参数的意义稍有区别。因为该描述符已被打开,所以fdopen为写而打开并不截断该文件。(例如,若该描述符原来是由open函数创建的,而且该文件已经存在,则其O_TRUNC标志将决定是否截断该文件。fdopen函数不能截断它为写而打开的任一文件。)另外,标准I/O追加写方式也不能用于创建该文件(因为如果一个描述符引用一个文件,则该文件一定已经存在)。

当以读和写类型打开一个文件时(type中+号),具有下列限制。

  • 如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。

  • 如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。

下图列出了打开一个流的6种不同的方式。

img

注意,在指定w或a类型创建一个新文件时,无法说明该文件的访问权限位(open函数和creat函数则能做到这一点)。POSIX.1要求实现使用如下的权限位集来创建文件:

S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH

可以通过调整umask值来限制这些权限。

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

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

在该文件被关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。

当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。


读和写流

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

(1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。

(2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大行长。

(3)直接 I/O。freadfwrite 函数支持这种类型的I/O。每次 I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构

格式化I/O函数,如 printfscanf

每次一个字符的I/O

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

1
2
3
4
5
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
// 3个函数的返回值:若成功,返回下一个字符;若已到达文件尾端或出错,返回EOF

函数 getchar 等同于 getc(stdin) 。前两个函数的区别是,getc 可被实现为宏,而 fgetc 不能实现为宏。这意味着以下几点。

(1)getc的参数不应当是具有副作用的表达式,因为它可能会被计算多次。

(2)因为fgetc一定是个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传送给另一个函数。

(3)调用fgetc所需时间很可能比调用getc要长,因为调用函数所需的时间通常长于调用宏。

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

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

void clearerr(FILE *fp);

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

  • 出错标志;

  • 文件结束标志。

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

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

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

压送回到流中的字符以后又可从流中读出,但读出字符的顺序与压送回的顺序相反。应当了解,虽然ISO C允许实现支持任何次数的回送,但是它要求实现提供一次只回送一个字符。我们不能期望一次能回送多个字符。

回送的字符,不一定必须是上一次读到的字符。不能回送EOF。但是当已经到达文件尾端时,仍可以回送一个字符。下次读将返回该字符,再读则返回EOF。之所以能这样做的原因是,一次成功的 ungetc 调用会清除该流的文件结束标志。

当正在读一个输入流,并进行某种形式的切词或记号切分操作时,会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将刚查看的字符回送,以便下一次调用getc时返回该字符。

ungetc 压送回字符时,并没有将它们写到底层文件中或设备上,只是将它们写回标准I/O库的流缓冲区中。

对应于上面所述的每个输入函数都有一个输出函数。

1
2
3
4
5
#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 不能实现为宏。

每次一行I/O

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

1
2
3
4
#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字节结尾。如若该行包括最后一个换行符的字符数超过n− 1,则fgets只返回一个不完整的行,但是,缓冲区总是以null字节结尾。对fgets的下一次调用会继续读该行。

gets 删除换行符,而 fgets 则保留换行符。尽量只用 fgets

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

1
2
3
4
#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
// 两个函数返回值:若成功,返回非负值;若出错,返回EOF

函数 fputs 将一个以null字节终止的字符串写到指定的流,尾端的终止符null不写出。注意,这并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非null字节。通常,在null字节之前是一个换行符,但并不要求总是如此。

puts 将一个以null字节终止的字符串写到标准输出,终止符不写出。但是,puts 随后又将一个换行符写到标准输出。

puts 并不像它所对应的 gets 那样不安全。但是我们还是应避免使用它,以免需要记住它在最后是否添加了一个换行符。如果总是使用 fgetsfputs, 那么就会熟知在每行终止处我们必须自己处理换行符。

二进制I/O

如果进行二进制I/O操作,那么我们更愿意一次读或写一个完整的结构。

1
2
3
4
#include <stdio.h>
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);
// 两个函数的返回值:读或写的对象数

size 是结构的大小,nobj是元素个数。

fread和fwrite返回读或写的对象数。对于读,如果出错或到达文件尾端,则此数字可以少于nobj。在这种情况,应调用 ferrorfeof 以判断究竟是那一种情况。对于写,如果返回值少于所要求的 nobj,则出错。


定位流

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

(1)ftellfseek 函数。假定文件的位置可以存放在一个长整型中。

(2)ftellofseeko 函数。使用 off_t数据类型代替了长整型。

(3)fgetposfsetpos 函数。使用抽象数据类型 fpos_t 记录文件的位置。这种数据类型可以根据需要定义为一个足够大的数,用以记录文件位置。

1
2
3
4
5
6
#include <stdio.h>
long ftell(FILE *fp);
// 返回值:若成功,返回当前文件位置指示;若出错,返回-1L
int fseek(FILE *fp, long offset, int whence);
// 返回值:若成功,返回0;若出错,返回−1
void rewind(FILE *fp);

对于二进制文件,其文件位置指示器是从文件起始位置开始度量,并以字节为度量单位的。ftell 用于二进制文件时,其返回值就是这种字节位置。为了用fseek 定位一个二进制文件,必须指定一个字节 offset,以及解释这种偏移量的方式。whence 的值与 lseek 函数的相同:SEEK_SET 表示从文件的起始位置开始,SEEK_CUR 表示从当前文件位置开始,SEEK_END 表示从文件的尾端开始。

对于文本文件,它们的文件当前位置可能不以简单的字节偏移量来度量。这主要也是在非UNIX系统中,它们可能以不同的格式存放文本文件。为了定位一个文本文件,whence一定要是 SEEK_SET,而且 offset 只能有两种值:0(后退到文件的起始位置),或是对该文件的 ftell 所返回的值。使用 rewind函数也可将一个流设置到文件的起始位置。

除了偏移量的类型是 off_t 而非 long 以外,ftello 函数与 ftell 相同,fseeko函数与 fseek相同。

1
2
3
4
5
#include <stdio.h>
off_t ftello(FILE *fp);
// 返回值:若成功,返回当前文件位置;若出错,返回(off_t)-1
int fseeko(FILE *fp, off_t offset, int whence);
// 返回值:若成功,返回0;若出错,返回−1

fgetposfsetpos 两个函数是ISO C标准引入的。

1
2
3
4
#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 时,可以使用此值将流重新定位至该位置。


格式化I/O

格式化输出

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

1
2
3
4
5
6
7
8
9
10
11
#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中。sprintf 在该数组的尾端自动加一个 null字节,但该字符不包括在返回值中。

注意,sprintf 函数可能会造成由buf指向的缓冲区的溢出。调用者有责任确保该缓冲区足够大。因为缓冲区溢出会造成程序不稳定甚至安全隐患,为了解决这种缓冲区溢出问题,引入了 snprintf 函数。在该函数中,缓冲区长度是一个显式参数,超过缓冲区尾端写的所有字符都被丢弃。如果缓冲区足够大,snprintf函数就会返回写入缓冲区的字符数。与sprintf相同,该返回值不包括结尾的null字节。若 snprintf 函数返回小于缓冲区长度n的正值,那么没有截断输出。若发生了一个编码的错误,snprintf 返回负值。

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

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

下图总结了各种 flags 标志。

img

fldwidth 说明最小字段宽度。转换后参数字符数若小于宽度,则多余字符位置用空格填充。字段宽度是一个非负十进制数,或是一个星号(*)。

precision 说明整型转换后最少输出数字位数、浮点数转换后小数点后的最少位数、字符串转换后最大字节数。精度是一个点(.),其后跟随一个可选的非负十进制数或一个星号(*)。

lenmodifier 说明参数长度。其可能的值示于下图中。

img

convtype不是可选的。它控制如何解释参数。下图列出了各种转换类型字符。

img

下列5种 printf族的变体类似于上面的5种,但是可变参数表(…)替换成了 arg

1
2
3
4
5
6
7
8
9
10
11
12
#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);
// 函数返回值:若缓冲区足够大,返回存入数组的字符数;若编码出错,返回负值

格式化输入

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

1
2
3
4
5
#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 说明要用转换结果赋值的参数大小。

convtype 字段类似于 printf 族的转换类型字段,但两者之间还有些差别。一个差别是,作为一种选项,输入中带符号的可赋予无符号类型。例如,输入流中的-1可被转换成4 294 967 295赋予无符号整型变量。下图总结了 scanf 族函数支持的转换类型。

在字段宽度和长度修饰符之间的可选项m是赋值分配符。它可以用于%c、%s以及%[转换符,迫使内存缓冲区分配空间以接纳转换字符串。在这种情况下,相关的参数必须是指针地址,分配的缓冲区地址必须复制给该指针。如果调用成功,该缓冲区不再使用时,由调用者负责通过调用free函数来释放该缓冲区。

img

printf 族相同,scanf 族也使用由 <stdarg.h> 说明的可变长度参数表。

1
2
3
4
5
6
#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

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

注意,fileno不是ISO C标准部分,而是POSIX.1支持的扩展。

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

临时文件

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

1
2
3
4
5
6
#include<stdio.h>
char *tmpnam(char *ptr);
// 返回值:指向唯一路径名的指针

FILE *tmpfile(void);
// 返回值:若成功,返回文件指针;若出错,返回NULL

tmpnam 函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它时,都产生一个不同的路径名,最多调用次数是 TMP_MAXTMP_MAX 定义在<stdio.h>中。

tmpnam 函数在 SUSv4 中被标记为弃用,但是 ISO C 标准还继续支持它。

ptr 是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。后续调用 tmpnam 时,会重写该静态区(这意味着,如果我们调用此函数多次,而且想保存路径名,则我们应当保存该路径名的副本,而不是指针的副本)。如若 ptr 不是NULL,则认为它应该是指向长度至少是L_tmpnam 个字符的数组(常量 L_tmpnam 定义在头文件 <stdio.h> 中)。所产生的路径名存放在该数组中,ptr 也作为函数值返回。

tmpfile 创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件。

Single UNIX Specification为处理临时文件定义了另外两个函数:mkdtempmkstemp,它们是XSI的扩展部分。

1
2
3
4
5
6
#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。注意,调用进程的文件模式创建屏蔽字可以进一步限制这些权限。如果目录创建成功,mkdtemp 返回新目录的名字。

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

tempfile不同,mkstemp 创建的临时文件并不会自动删除。如果希望从文件系统命名空间中删除该文件,必须自己对它解除链接。


内存流

标准I/O库把数据缓存在内存中,因此每次一字符和每次一行的I/O更有效。也可以通过调用 setbufsetvbuf 函数让I/O库使用我们自己的缓冲区。在SUSv4中支持了内存流。这就是标准I/O流,虽然仍使用FILE指针进行访问,但其实并没有底层文件。所有的I/O都是通过在缓冲区与主存之间来回传送字节来完成的。我们将看到,即便这些流看起来像文件流,它们的某些特征使其更适用于字符串操作

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

1
2
3
#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
// 返回值:若成功,返回流指针;若错误,返回NULL

fmemopen函数允许调用者提供缓冲区用于内存流:buf 参数指向缓冲区的开始位置,size 参数指定了缓冲区大小的字节数。如果buf 参数为空,fmemopen 函数分配 size 字节数的缓冲区。在这种情况下,当流关闭时缓冲区会被释放。

type 参数控制如何使用流。type 可能的取值如图所示。

image-20250227214831301

注意,这些取值对应于基于文件的标准I/O流的type参数取值,但其中有些微小差别。第一,无论何时以追加写方式打开内存流时,当前文件位置设为缓冲区中的第一个null字节。如果缓冲区中不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流并不是以追加写方式打开时,当前位置设为缓冲区的开始位置。因为追加写模式通过第一个null字节确定数据的尾端,内存流并不适合存储二进制数据(二进制数据在数据尾端之前就可能包含多个null字节)。

第二,如果buf参数是一个null指针,打开流进行读或者写都没有任何意义。因为在这种情况下缓冲区是通过fmemopen进行分配的,没有办法找到缓冲区的地址,只写方式打开流意味着无法读取已写入的数据,同样,以读方式打开流意味着只能读取那些我们无法写入的缓冲区中的数据。

第三,任何时候需要增加流缓冲区中数据量以及调用fclose、fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节。

用于创建内存流的其他两个函数分别是 open_memstreamopen_wmemstream

1
2
3
4
5
#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 参数访问缓冲区地址和大小;

  • 关闭流后需要自行释放缓冲区;

  • 对流添加字节会增加缓冲区大小。

但是在缓冲区地址和大小的使用上必须遵循一些原则。第一,缓冲区地址和长度只有在调用 fclosefflush 后才有效;第二,这些值只有在下一次流写入或调用 fclose 前才有效。因为缓冲区可以增长,可能需要重新分配。如果出现这种情况,我们会发现缓冲区的内存地址值在下一次调用 fclosefflush 时会改变。

因为避免了缓冲区溢出,内存流非常适用于创建字符串。因为内存流只访问主存,不访问磁盘上的文件,所以对于把标准I/O流作为参数用于临时文件的函数来说,会有很大的性能提升。


习题

5.1 用 setvbuf 实现 setbuf

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

void my_setbuf(FILE *stream, char *buffer) {
if (buffer == NULL) {
// 如果 buffer 为 NULL,设置为无缓冲
setvbuf(stream, NULL, _IONBF, 0);
} else {
// 否则,设置为全缓冲,缓冲区大小为 BUFSIZ
setvbuf(stream, buffer, _IOFBF, BUFSIZ);
}
}

5.3 printf返回0值表示什么?

当printf没有输出任何字符时,如 printf("");,函数调用返回0。

5.4 下面的代码在一些机器上运行正确,而在另外一些机器运行时出错,解释问题所在。

1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
char c;
while ((c = getchar()) != EOF)
putchar(c);
}

这是一个比较常见的错误。 getc 以及 getchar 的返回值是int类型,而不是char类型。

由于EOF经常定义为−1,那么如果系统使用的是有符号的字符类型,程序还可以正常工作。但如果使用的是无符号字符类型,那么返回的EOF被保存到字符c后将不再是−1,所以,程序会进入死循环。本书说明的4种平台都使用带符号字符,所以实例代码都能工作。

5.5 对标准I/O流如何使用 fsync 函数?

使用方法为:先调用 fflush 后调用 fsyncfsync 所使用的参数由 fileno 函数获得。

如果不调用 fflush,所有的数据仍然在内存缓冲区中,此时调用 fsync 将没有任何效果。