APUE第五章 标准I/O库

第五章 标准I/O库

5.1 引言

本章讲述标准I/O库。不仅是UNIX,很多其他操作系统都实现了标 准I/O库,所以这个库由ISO C标准说明。Single UNIX Specification对ISO C标准进行了扩充,定义了另外一些接口。

标准I/O库处理很多细节,如缓冲区分配、以优化的块长度执行 I/O等。这些处理使用户不必担心如何选择使用正确的块长度(如3.9节 中所述)。这使得它便于用户使用,但是如果我们不深入地了解I/O库 函数的操作,也会带来一些问题。

标准I/O库是由Dennis Ritchie在1975年左右编写的。它是Mike Lesk编写的可移植I/O库的主要修改版本。令人惊讶的是,35年来,几 乎没有对标准I/O库进行修改。

5.2 流和FILE对象

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

对于ASCII字符集,一个字符用一个字节表示。对于国际字符集, 一个字符可用多个字节表示。标准I/O文件流可用于单字节或多字节 (“宽”)字符集。

流的定向(stream's orientation)决定了所读、写的 字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。 如若在未定向的流上使用一个多字节 I/O 函数(见<wchar.h>),则将 该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函 数,则将该流的定向设为字节定向的。只有两个函数可改变流的定向。 freopen函数(稍后讨论)清除一个流的定向;fwide函数可用于设置流的 定向。

#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(参考5.5节)返回一个指向 FILE对象的指针。该对象通常是一个结构,它包含了标准I/O库为管理 该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流 缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志 等。

5.4 缓冲

标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次 数(见图3-6,其中显示了在不同缓冲区长度情况下,执行I/O所需的 CPU时间量)。它也对每个I/O流自动地进行缓冲管理,从而避免了应 用程序需要考虑这一点所带来的麻烦。遗憾的是,标准I/O库最令人迷 惑的也是它的缓冲。

应用程序没有必要检验FILE对象。为了引用一个流,需将FILE指 针作为参数传递给每个标准I/O函数。在本书中,我们称指向FILE对象 的指针(类型为FILE*)为文件指针。

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

对一个进程预定义了 3 个流,并且这 3 个流可以自动地被进程使 用,它们是:标准输入、标准输出和标准错误。这些流引用的文件与在 3.2 节中提到文件描述符 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO所引用的相同。
这3个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引 用。这3个文件指针定义在头文件<stdio.h>中。

5.4缓冲

标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次 数(见图3-6,其中显示了在不同缓冲区长度情况下,执行I/O所需的 CPU时间量)。它也对每个I/O流自动地进行缓冲管理,从而避免了应 用程序需要考虑这一点所带来的麻烦。遗憾的是,标准I/O库最令人迷 惑的也是它的缓冲。
标准I/O提供了以下3种类型的缓冲。
(1)全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际 I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲 的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用 malloc(见7.8节)获得需使用的缓冲区。
术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准 I/O例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函 数 fflush 冲洗一个流。值得注意的是,在 UNIX环境中,flush有两种意 思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁 盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面(例如, 在第18章中所述的tcflush函数),flush(刷清)表示丢弃已存储在缓冲 区中的数据。
(2)行缓冲。在这种情况下,当在输入和输出中遇到换行符时, 标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函 数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个 终端时(如标准输入和标准输出),通常使用行缓冲。
对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的

缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一 个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O 库要求 从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求 需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。

3)不带缓冲。标准I/O库不对字符进行缓冲存储。例如,若用标 准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符 能立即输出,很可能使用3.8节的write函数将这些字符写到相关联的打开 文件中。

标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快 显示出来,而不管它们是否含有一个换行符。
ISO C要求下列缓冲特征。
•当且仅当标准输入和标准输出并不指向交互式设备时,它们才是 全缓冲的。
•标准错误决不会是全缓冲的。
但是,这并没有告诉我们如果标准输入和标准输出指向交互式设备 时,它们是不带缓冲的还是行缓冲的;以及标准错误是不带缓冲的还是 行缓冲的。很多系统默认使用下列类型的缓冲:
•标准错误是不带缓冲的。 •若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。 本书讨论的4种平台都遵从标准I/O缓冲的这些惯例,标准错误是不
带缓冲的,打开至终端设备的流是行缓冲的,其他流是全缓冲的。
我们将在5.12节和图5-1对标准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);

返回值:若成功,返回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所指定的值。

4064394-5e84671906b3e12f.png
image.png

要了解,如果在一个函数内分配一个自动变量类的标准I/O缓冲 区,则从该函数返回之前,必须关闭该流(7.8节将对此做更多讨论)。 另外,其些实现将缓冲区的一部分用于存放它自己的管理操作信息,所 以可以存放在缓冲区中的实际数据字节数少于 size。一般而言,应由系 统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时, 标准I/O库将自动释放缓冲区。

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

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

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

5.5 打开6流

下列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个函数的区别如下。 (1)fopen函数打开路径名为pathname的一个指定的文件。 (2)freopen 函数在一个指定的流上打开一个指定的文件,如若该
流已经打开,则先关闭该流。若该流已经定向,则使用 freopen 清除该 定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准 输入、标准输出或标准错误。
(3)fdopen函数取一个已有的文件描述符(我们可能从open、 dup、dup2、fcntl、pipe、socket、socketpair或accept函数得到此文件描述 符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建 管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能 用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一 个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。
fopen和freopen是ISO C的所属部分。而ISO C并不涉及文件描述 符,所以仅有POSIX.1具有fdopen。

4064394-f7064e68bfe0edee.png
image.png

当以读和写类型打开一个文件时(type中+号),具有下列限制。
•如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能 直接跟随输入。
•如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达 文件尾端,则在输入操作之后不能直接跟随输出。

注意,在指定w或a类型创建一个新文件时,我们无法说明该文件的 访问权限位(第3章中所述的open函数和creat函数则能做到这一点)。 POSIX.1要求实现使用如下的权限位集来创建文件:
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
回忆4.8节,我们可以通过调整umask值来限制这些权限。

调用fclose关闭一个打开的流。
#include <stdio.h> int fclose(FILE *fp);

在该文件被关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输 入数据被丢弃。如果标准I/O库已经为该流自动分配了一个缓冲区,则 释放此缓冲区。
当一个进程正常终止时(直接调用exit函数,或从main函数返 回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准 I/O流都被关闭。

5.6 读和写流

一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对 其进行读、写操作。
(1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲 的,则标准I/O函数处理所有缓冲。
(2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和 fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大 行长。5.7节将说明这两个函数。

(3)直接 I/O。fread和fwrite函数支持这种类型的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不能实现为宏。这意味着以下几点。 (1)getc的参数不应当是具有副作用的表达式,因为它可能会被计
算多次。 (2)因为fgetc一定是个函数,所以可以得到其地址。这就允许将
fgetc的地址作为一个参数传送给另一个函数。 (3)调用fgetc所需时间很可能比调用getc要长,因为调用函数所需
的时间通常长于调用宏。

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

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

两个函数返回值:若条件为真,返回非0(真);否则,返回0(假)

void clearerr(FILE *fp);

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

#include <stdio.h>
int ungetc(int c, FILE *fp);

返回值:若成功,返回c;若出错,返回EOF 压送回到流中的字符以后又可从流中读出,但读出字符的顺序与压
送回的顺序相反。应当了解,虽然ISO C允许实现支持任何次数的回 送,但是它要求实现提供一次只回送一个字符。我们不能期望一次能回 送多个字符。
回送的字符,不一定必须是上一次读到的字符。不能回送EOF。但 是当已经到达文件尾端时,仍可以回送一个字符。下次读将返回该字 符,再读则返回EOF。之所以能这样做的原因是,一次成功的ungetc调 用会清除该流的文件结束标志。
当正在读一个输入流,并进行某种形式的切词或记号切分操作时, 会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何 处理当前字符。然后就需要方便地将刚查看的字符回送,以便下一次调 用getc时返回该字符。如果标准I/O库不提供回送能力,就需将该字符 存放到一个我们自己的变量中,并设置一个标志以便判别在下一次需要 一个字符时是调用 getc,还是从我们自己的变量中取用这个字符。

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

#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc, char* argv[]) {
    printf("test putc\n");
    FILE *open_file = fopen(argv[1], "r+");
    char c;
    while(!feof(open_file)) {
        c = getc(open_file);
        // printf("char %c\n", c);
        putc(c, stdout);
        fflush(open_file);
    }
}

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字节结尾。如若该行包括最后一个换行符的字符数超过n−1,则fgets
只返回一个不完整的行,但是,缓冲区总是以null字节结尾。对fgets的 下一次调用会继续读该行。
gets 是一个不推荐使用的函数。其问题是调用者在使用 gets 时不能 指定缓冲区的长度。这样就可能造成缓冲区溢出(如若该行长于缓冲区 长度),写到缓冲区之后的存储空间中,从而产生不可预料的后果。这 种缺陷曾被利用,造成1988年的因特网蠕虫事件。有关说明请见1989年 6月的Communications of the ACM(vol.32,no.6)。gets与fgets的另一个区 别是,gets并不将换行符存入缓冲区中。

虽然ISO C要求提供gets,但请使用fgets,而不要使用gets。事实 上,在SUSv4中, gets被标记为弃用的接口,而且在ISO C标准的最新版 本(ISO/IEC 9899:2011)中已被忽略。
fputs和puts提供每次输出一行的功能。

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

5.8 标准i/o的效率

使用前面所述的函数,我们能对标准I/O系统的效率有所了解。图 5-4程序类似于图3-4程序,它使用getc和putc将标准输入复制到标准输 出。这两个例程可以实现为宏。

将这3个程序的时间与图3-6中的时间进行比 较是很有趣的。图5-6中显示了对同一文件(98.5 MB,300万行)进行操 作所得的数据。


4064394-41c48222b5b465a1.png
image.png

对于这3个标准I/O版本的每一个,其用户CPU时间都大于图3-6中 的最佳read版本,因为在每次读一个字符的标准I/O版本中有一个要执 行1亿次的循环,而在每次读一行的版本中有一个要执行3 144 984次的 循环。在read版本中,其循环只需执行25 224次(对于缓冲区长度为4 096字节)。因为系统CPU时间几乎相同,所以用户CPU时间的差别以

及等待I/O结束所消耗时间的差别造成了时钟时间的差别。 系统CPU时间几乎相同,原因是因为所有这些程序对内核提出的
读、写请求数基本相同。注意,使用标准I/O例程的一个优点是无需考 虑缓冲及最佳I/O长度的选择。在使用fgets时需要考虑最大行长,但是 与选择最佳I/O长度比较,这要方便得多。
图5-6的最后一列是每个main函数的文本空间字节数(由C编译器产 生的机器指令)。从中可见,使用getc和putc的版本与使用fgetc和fputc 的版本在文本空间长度方面大体相同。通常,getc和putc实现为宏,但 在GNU C库实现中,宏简单地扩充为函数调用。
使用每次一行I/O版本的速度大约是每次一个字符版本速度的两 倍。如果fgets和fputs函数是用getc和putc实现的(参见Kernighan和 Ritchie[1988]的7.7节),那么,可以预期fgets版本的时间会与getc 版本接 近。实际上,每次一行的版本会更慢一些,因为除了现已存在的 6百万 次函数调用外还需另外增加 2 亿次函数调用。而在本测试中所用的每次 一行函数是用memccpy(3)实现的。通常,为了提高效率,memccpy函数 用汇编语言而非C语言编写。正因为如此,每次一行版本才会有较高的 速度。

这些时间数字的最后一个有趣之处在于:fgetc版本较图3-6中 BUFFSIZE=1的版本要快得多。两者都使用了约2亿次的函数调用,在 用户CPU时间方面,fgetc版本的速度大约是后者的16倍,而在时钟时间 方面几乎是39倍。造成这种差别的原因是:使用read的版本执行了2亿次 函数调用,这也就引起2亿次系统调用。而对于fgetc版本,它也执行2亿 次函数调用,但是这只引起25 224次系统调用。系统调用与普通的函数 调用相比需要花费更多的时间。

在本节及 3.9 节中我们了解到的基本事实 是,标准I/O库与直接调用read和write函数相比并不慢很多。对于大多 数比较复杂的应用程序,最主要的用户CPU时间是由应用本身的各种处 理消耗的,而不是由标准I/O例程消耗的。

5.9 二进制I/O

5.6节和5.7节中的函数以一次一个字符或一次一行的方式进行操 作。如果进行二进制I/O操作,那么我们更愿意一次读或写一个完整的 结构。如果使用getc或putc读、写一个结构,那么必须循环通过整个结 构,每次循环处理一个字节,一次读或写一个字节,这会非常麻烦而且 费时。如果使用fputs和fgets,那么因为fputs在遇到null字节时就停止, 而在结构中可能含有null字节,所以不能使用它实现读结构的要求。相 类似,如果输入数据中包含有null字节或换行符,则fgets也不能正确工 作。因此,提供了下列两个函数以执行二进制I/O操作。

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

两个函数的返回值:读或写的对象数 这些函数有以下两种常见的用法。
(1)读或写一个二进制数组。例如,为了将一个浮点数组的第 2~ 5 个元素写至一文件上,可以编写如下程序:

    float data[10];
    if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
err_sys("fwrite error");

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

(2)读或写一个结构。例如,可以编写如下程序:

struct {
short count;
long  total; char  name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
err_sys("fwrite error"); 

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

将这两个例子结合起来就可读或写一个结构数组。为了做到这一
点,size 应当是该结构的sizeof,nobj应是该数组中的元素个数。

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

#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
class A{
public:
    int double_data;
    char name[10] = "hello";
};

int main(int argc ,char * argv[]) {
    A *before = new A();
    before->double_data = 100;

    FILE *open_file = fopen(argv[1], "a+");
    if (fwrite(&before, sizeof(A), 1, open_file)!= 1) {
        printf("fwrite error\n");
    }
    A *after = new A();
    printf("dump ok\n");
    fclose(open_file);

    open_file = fopen(argv[1], "r");
    fread(&after, sizeof(A), 1, open_file);
    for (int i = 2; i < 6;++i) {
        printf("%d", after->double_data);
    }
    printf("fread ok\n");
    return 0;
}

使用二进制I/O的基本问题是,它只能用于读在同一系统上已写的 数据。多年之前,这并无问题(那时,所有UNIX系统都运行于PDP-11 上),而现在,很多异构系统通过网络相互连接起来,而且,这种情况 已经非常普遍。常常有这种情形,在一个系统上写的数据,要在另一个 系统上进行处理。在这种环境下,这两个函数可能就不能正常工作,其 原因是:
(1)在一个结构中,同一成员的偏移量可能随编译程序和系统的 不同而不同(由于不同的对齐要求)。确实,某些编译程序有一个选 项,选择它的不同值,或者使结构中的各成员紧密包装(这可以节省存 储空间,而运行性能则可能有所下降);或者准确对齐(以便在运行时 易于存取结构中的各成员)。这意味着即使在同一个系统上,一个结构 的二进制存放方式也可能因编译程序选项的不同而不同。
(2)用来存储多字节整数和浮点值的二进制格式在不同的系统结 构间也可能不同。

5.10 定位流

有3种方法定位标准I/O流。
(1)ftell 和fseek 函数。这两个函数自 V7 以来就存在了,但是它们 都假定文件的位置可以存放在一个长整型中。
(2)ftello和fseeko函数。Single UNIX Specification引入了这两个函 数,使文件偏移量可以不必一定使用长整型。它们使用off_t数据类型代 替了长整型。
(3)fgetpos和fsetpos函数。这两个函数是由ISO C引入的。它们使 用一个抽象数据类型fpos_t记录文件的位置。这种数据类型可以根据需 要定义为一个足够大的数,用以记录文件位置。
需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和 fsetpos。

#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的值与3.6节中lseek函数的 相同:SEEK_SET表示从文件的起始位置开始,SEEK_CUR表示从当前

文件位置开始,SEEK_END表示从文件的尾端开始。ISO C并不要求一 个实现对二进制文件支持SEEK_END规格说明,其原因是某些系统要 求二进制文件的长度是某个幻数的整数倍,结尾非实际内容部分则填充 为 0。但是在UNIX中,对于二进制文件,则是支持SEEK_END的。

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

#include <stdio.h> 
off_t ftello(FILE *fp);

返回值:若成功,返回当前文件位置;若出错,返回(off_t)-1
int fseeko(FILE *fp, off_t offset, int whence);
返回值:若成功,返回0;若出错,返回−1 回忆3.6节中对off_t数据类型的讨论。实现可将off_t类型定义为长于
32位。

正如我们已提及的,fgetpos和fsetpos两个函数是ISO C标准引入的。

#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中。sprintf 在 该数组的尾端自动加一个 null字节,但该字符不包括在返回值中。

注意,sprintf函数可能会造成由buf指向的缓冲区的溢出。调用者有 责任确保该缓冲区足够大。因为缓冲区溢出会造成程序不稳定甚至安全 隐患,为了解决这种缓冲区溢出问题,引入了snprintf函数。在该函数 中,缓冲区长度是一个显式参数,超过缓冲区尾端写的所有字符都被丢

弃。如果缓冲区足够大,snprintf函数就会返回写入缓冲区的字符数。与 sprintf相同,该返回值不包括结尾的null字节。若snprintf函数返回小于缓 冲区长度n的正值,那么没有截断输出。若发生了一个编码的错误, snprintf返回负值。
虽然 dprintf 不处理文件指针,但我们仍然把它包括在处理格式化输 出的函数中。注意,使用dprintf不需要调用fdopen将文件描述符转换为 文件指针(fprintf需要)。

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

%[flags][fldwidth][precision][lenmodifier]convtype
4064394-7894d5b49c7a70e3.png
image.png

fldwidth说明最小字段宽度。转换后参数字符数若小于宽度,则多 余字符位置用空格填充。字段宽度是一个非负十进制数,或是一个星号 ()。
precision 说明整型转换后最少输出数字位数、浮点数转换后小数点 后的最少位数、字符串转换后最大字节数。精度是一个点(.),其后 跟随一个可选的非负十进制数或一个星号(
)。

宽度和精度字段两者皆可为*。此时,一个整型参数指定宽度或精 度的值。该整型参数正好位于被转换的参数之前。

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


4064394-3aded9ac72364d66.png
image.png

convtype不是可选的。它控制如何解释参数。图5-9中列出了各种转 换类型字符。
根据常规的转换说明,转换是按照它们出现在 format参数之后的顺 序应用于参数的。一种替代的转换说明语法也允许显式地用%n序列来 表示第n个参数的形式来命名参数。注意,这两种语法不能在同一格式 说明中混用。在替代的语法中,参数从 1 开始计数。如果参数既没有提 供字段宽度和也没有提供精度,通配符星号的语法就更改为*m,m指 明提供值的参数的位置。

4064394-ed332fee1520a8f2.png
转换说明中的转换类型

下列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);
函数返回值:若缓冲区足够大,返回存入数组的字符数;若编码出错,
                           返回负值

在附录B的出错处理例程中,将使用vsnprintf函数。
关于ISO C标准中有关可变长度参数表的详细说明请参阅Kernighan 和Ritchie[1988]的7.3节。应当了解的是,由ISO C提供的可变长度参数表 例程(<stdarg.h>头文件和相关的例程)与由较早版本UNIX提供的 <varargs.h>例程是不同的。

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

5.12 实现细节

正如前述,在UNIX中,标准I/O库最终都要调用第3章中说明的 I/O例程。每个标准I/O流都有一个与其相关联的文件描述符,可以对 一个流调用fileno函数以获得其描述符。
注意,fileno不是ISO C标准部分,而是POSIX.1支持的扩展。

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

返回值:与该流相关联的文件描述符 如果要调用dup或fcntl等函数,则需要此函数。

为了了解你所使用的系统中标准 I/O 库的实现,最好从头文件 <stdio.h>开始。从中可以看到FILE对象是如何定义的、每个流标志的定 义以及定义为宏的各个标准I/O例程(如getc)。Kernighan和 Ritchie[1988]中的8.5节含有一个示例实现,从中可以看到很多UNIX实现 的基本样式。Plauger[1992]的第12章提供了标准I/O库一种实现的全部源 代码。GNU标准I/O库的实现也是公开可用的。

5.14 内存流

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

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

返回值:若成功,返回流指针;若错误,返回NULL fmemopen 函数允许调用者提供缓冲区用于内存流:buf 参数指向缓
冲区的开始位置,size参数指定了缓冲区大小的字节数。如果buf参数为 空,fmemopen函数分配size字节数的缓冲区。在这种情况下,当流关闭 时缓冲区会被释放。
type参数控制如何使用流。type可能的取值如图5-14所示。


4064394-7669b0996e7e0895.png
image.png

注意,这些取值对应于基于文件的标准I/O流的type参数取值,但 其中有些微小差别。第一,无论何时以追加写方式打开内存流时,当前 文件位置设为缓冲区中的第一个null字节。如果缓冲区中不存在null字 节,则当前位置就设为缓冲区结尾的后一个字节。当流并不是以追加写 方式打开时,当前位置设为缓冲区的开始位置。因为追加写模式通过第 一个null字节确定数据的尾端,内存流并不适合存储二进制数据(二进 制数据在数据尾端之前就可能包含多个null字节)。
第二,如果buf参数是一个null指针,打开流进行读或者写都没有任 何意义。因为在这种情况下缓冲区是通过fmemopen进行分配的,没有 办法找到缓冲区的地址,只写方式打开流意味着无法读取已写入的数 据,同样,以读方式打开流意味着只能读取那些我们无法写入的缓冲区 中的数据。
第三,任何时候需要增加流缓冲区中数据量以及调用fclose、 fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节。

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define BUFFSIZE 4096
int main(int argc, char * argv[]) {
    char buf[BUFFSIZE];
    printf("open mem file_stream\n");
    FILE *fp = fmemopen(buf, BUFFSIZE, "r+");
    char c;
    printf("fputs\n");
    fputs("hello world", fp);
    char line[BUFFSIZE];
    printf("%s\n", buf);

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值