APUE读书笔记-05标准输入输出库(1)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了APUE读书笔记-05标准输入输出库(1)相关的知识,希望对你有一定的参考价值。
参考技术A 在本章,我们描述了标准输入输出库。由于这个库也被许多非UNIX的操作系统实现了,所以这个库由 ISO C 标准指定。其它的接口被 SUS 定义,作为 ISO C 标准的扩展。标准输入输出库处理了诸如缓存分配,以及分配优化的大小的块进行 I/O ,这样我们就不必担心如何使用正确的块大小进行 I/O 了(如原书3章9节所描述的那样)。这样,这个库就很容易使用了,但是同时,也引入了我们没有预料到的其他一些问题。
标准 I/O 库由Dennis Ritchie在大约1975年编写,它作为Mike Lesk编写的可移植 I/O 库的主要版本.并且在30年的期间内,对它改动非常少。
参考: APUE2/ch05lev1sec1.html
我们使用系统调用来操作文件的时候,是通过文件描述符号来标识这个文件,如果我们使用标准库函数来进行文件操作的我们通常是把一个 stream 和这个文件相关联了,然后通过这个 stream 对这个流进行操作,也即,标准 I/O 库是以流( stream )为中心进行操作的。
当我们使用库函数 fopen 打开一个文件的时候,会返回一个指向 FILE 对象的指针,这个对象包含了 I/O 库管理 stream 需要的所有信息,例如被操作的文件的描述符号,指向这个 stream 的缓存的指针,以及缓存的大小(注意这个缓存和系统调用时可能经过的内核的缓存不同,经过内核的缓存更接近下层,在第3章8节讨论 I/O 效率时的最后也提到过,但这属于内核驱动范围了,可以暂时忽略),当前缓存中的字符数目,错误标志等等。我们把这个 FILE 对象做为参数传递给标准输入输出库函数,就可以实现相应的操作了(使用库函数操作,也不用费心去关心输入输出缓存设置为多大才好了)。
应用程序不用检验 FILE 对象。引用一个流,只需将 FILE 指针作为参数传递给每个标准 I/O 函数。本书中,我们称指向 FILE 的指针(类型为 FILE* )为文件指针。
通常,有单字节和多字节字符。 stream 的 orientation 用于决定读写的是单字节字符还是多字节字符。在最开始一个 stream 创建的时候,它是没有 orientation 的,这时候,如果使用了一个多字节 I/O 函数对这个 stream 进行操作(例如 <wchar.h> 中的函数),那么这个 stream 就被设置为 wide-oriented 的;同理如果用单字节的 I/O 函数对这个 stream 进行操作,那么这个 stream 就被设置为 byte-oriented 的。一旦设置了 stream 的方向,除非被关闭,否则就不能改变了。当一个 stream 被 set 的时候,只有两个函数可以改变这个 stream 的 orientation 。它们是: fwide 和 freopen .
freopen 原型如下:
它会把 stream 的 orientation 给 clear .把原来的 stream 给关掉,然后把参数指定的新的 stream 和这个文件相互关联。
fwide 原型如下:
这里,如果 mode 是负数,那么设置成 byte-oriented ;如 mode 是正数,那么设置成 wide-oriented ;如果 mode 是0那么不会做设置,但也返回当前流的 orientation 值。
需要注意的是 fwide 不会改变已经被 oriented 的 stream ,也不会返回 error (也就是那个流刚开始被创建之后没有被使用过就没有被设置过,这个时候则可以用这个函数来设置)。
参考: APUE2/ch05lev1sec2.html
有三个重要的流: stdin , stdout , stderr 表示标准输入,输出和错误流,它们对应的文件描述符号是: STDIN_FILENO , STDOUT_FILENO ,和 STDERR_FILENO .
参考: APUE2/ch05lev1sec3.html
使用标准输入输出( Standard I/O )库函数进行操作的目的就是为了使用最小数目的的 read 和 write 系统调用(通过自动管理设置多大的缓存等等)。 Standard I/O 一般选择合适的大小来分配缓存。可能是 BUFSIZ 常量的值(在 <stdio.h> 中定义),也可能是 stat 结构中的 st_blksize 成员。
ISO C 规定:
但是,上面的规定却不是非常确定的,一般而言:
如果不是用默认的缓存类型,有两个可以设置缓存的函数:
这两个函数必须在 stream 被 open 之后,并且任何 i/o 操作之前,被调用。
使用 setbuf 我们可以设置和取消 buffer .如果设置 buffer ,指向 buf 的指针的数据的大小必须是 BUFSIZ ,一般这样 stream 就是 fully buffered .但是如果 stream 是和 terminal 相关联的也有些系统会把 buffered 设置为 line buffered 的。如果取消 buffer ,那么 buf 参数是 NULL 的。
使用 setvbuf ,我们可以精确地指定我们想要的 buffer 类型。我们通过 modde 参数来进行指定:
如果我们指定为 unbuffered ,那么 buf 和 size 参数会被忽略.
如果指定了 fully buffered 或者 line buffered 那么:我们可以通过 buf 和 size 来设置缓存和大小。如果这时候的 buf 参数是 NULL 那么 standio = 会自动分配一个合适的大小,例如 =BUFSIZ .
注意,你需要保证在 stream closed 的时候, buf 是存在的。如果我们自行指定分配的 buffer 是一个函数内部的局部变量,我们需要在这个函数返回之前把这个 stream 给 closed 了,另外,因为某些实现使用这个 buffer 的一部分做为记录的空间,所以我们存放在这个 buffer 中的数据应该小于它的实际大小。一般我们应该让系统自己选择分配合适的缓存和大小。
有些C库使用 stat 结构的 st_blksize 作为最优输入输出缓存大小,通过后面的讨论我们能够看出, GNU C 库就是使用这个方法。
我们需要注意,如果我们以一个函数中的自动变量为标准输入输出分配缓存,那么我们需要在返回函数之前关闭流。另外,有些实现使用这个缓存的部分内容作为内部索引之用,所以实际存放于缓存中的数据会比缓存的大小要小。一般来说,我们让系统自己选择缓存大小,以及自动分配缓存。这样,当我们关闭流的时候,标准 I/O 库会自动释放缓存。
我们可以用 fflush 函数将流刷新,其声明如下:
返回:如果成功返回0,如果错误返回 EOF 。
fflush 会导致在 stream 中没有被 written 的数据被提交到 kernel 中去。特别地,如果 fp 为 NULL ,那么这个函数会导致所有的输出流被刷新。
参考: APUE2/ch05lev1sec4.html
[APUE]标准IO库(下)
一、标准IO的效率
对比以下四个程序的用户CPU、系统CPU与时钟时间对比
程序1:系统IO
程序2:标准IO getc版本
程序3:标准IO fgets版本
结果:
【注:该表截取自APUE,上表中"表3-1中的最佳时间即《程序1》","表3-1中的单字节时间指的是《程序1》中BUFSIZE为1时运行时间结果",fgetc/fputc版本程序这里没放出】
对于三个标准IO版本的每一个其用户CPU时间都大于最佳read版本,因为每次读一个字符版本中有一个要执行150万次的循环,而在每次读一行的版本中有一个要执行30000次的循环。而在read版本中,其循环只需执行180次。因为系统CPU时间都相同,所以用户CPU时间的差别造成了时钟时间的差别。系统CPU时间相同的原因是所有这些程序对内核提出的读写请求数相同。
上表中最后一列是每个main函数的文本空间字节数(由c编译产生的机器指令)。从中可见getc/putc版本在文本空间做了大量宏替换,所以它所需的指令数超过了调用fgetc/fputc函数所用的指令数。从用户CPU时间看getc/putc版本与fgetc/fputc版本在此次测试中并没有多大的差别。
使用每次一行IO的版本其速度大约是每次一个字符版本的两倍(包括用户CPU时间和时钟时间)。如果fgets/fputs函数用getc/putc实现则可以预计fgets版本的时间会与getc版本相接近。可以预料每次一行的版本会更慢一些,因为除了现存的60000次函数调用外还需增加3百万次宏调用。而在本测试中每次一行参数是用memccoy实现的,为了提高效率memccpy函数用汇编写。
【重点】fgetc版本与程序1 BUFSIZE=1的版本要快得多,两者都用了约3百万次函数调用,造成速度差距这么大的原因在于《程序1》执行了3百万次函数调用这也执行了3百万次系统调用,而fgetc版本虽然执行了3百万次函数调用但是只引起了360次系统调用。系统调用与普通的函数调用相比是很耗时间的。
二、二进制IO
为了可以读取二进制文件我们可以通过getc/putc实现的,但是这样必须循环整个结构。而fputs/fgets在遇到null字符时就结束,在结构中可能含有null字节,所以不能使用fgets/fputs。综上所以提供了下面两个函数以执行二进制IO操作
#include <stdio.h> size_t fread(void *ptr, size_t size, size_t nobj, FILE *fp); size_t fwrite(const char *ptr, size_t size, size_t nobj, FILE *fp); 返回值:读或写的对象数
常见的用法:
- 读或写一个二进制数组。例如将一个浮点数组的第2至第5个元素写至一个文件上:
float data[10]; if (fwrite(&data[2], sizeof(float), 4, fp) != 4) { fprintf(stderr, "fwrite error"); }
其中,指定size为每个数组元素的长度,nobj为欲写的元素数。
-
读或写一个结构。例:
struct { short count; long total; char name[NAMESIZE]; } item; if (fwrite(&item, sizeof(item), 1, fp) != 1) { fprintf(stderr, "fwrite error"); }
对于读,如果出错或到达文件尾,则fread返回的数字可能少于nobj。这时应该调用ferro+feof判断是哪种情况。对于写如果返回之小于nobj则出错。
使用二进制IO的限制是只能用于读已写在同一系统上的数据。但是现在很多异构系统通过网络连接在一起,通常会在一个系统上读取另外一个系统上的数据,这样的话这两个函数就不能工作了,原因:
- 在一个结构中,同一成员的位移量可能随编译程序和系统的不同而异,有些编译器会有优化选项以对齐或紧密包装结构(节省存储空间)以便在运行时易于存取结构中的各个成员。这意味着即使在单一系统中,一个结构的二进制存放方式也可能因编译器选项不通融而不同。
- 用来存储多字节整数和浮点值的二进制格式在不同系统结构间也可能不同。
三、 定位流
有两种方式可以定位标准IO流。
- ftell和fseek。这两个函数都假定文件的位置可以存放在一个长整型中。
- fgetpos和fsetpos。这两个函数是ANSI C引入的。这两个函数引进了一个新的抽象数据类型fpos_t,它记录文件的位置。在非UNIX系统中,这种数据类型可以定义为记录一个文件的位置所需的长度。
需要移植到非UNIX系统上运行的程序应使用fgetpos和fsetpos。
#include <stdio.h> long ftell(FILE *fp); 返回值:成功则为当前位置相对于文件首的偏移字节数,出错为-1L int fseek(FILE *fp, long offset, int whence); 返回值:成功为0,出错为非0 void rewind(FILE *fp);
对于一个二进制文件,其位置指示是从文件起始位置开始度量并以字节为单位的。ftell用于二进制文件时,其返回值就是这种字节位置。为了用fseek定位一个二进制文件,必须指定一个字节offset,以及解释这种位移量的方式。whence与lseek函数相同:SEEK_SET表示从文件的起始位置开始,SEEK_CUR表示从当前位置,SEEK_END表示从文件的尾端。
对于文本文件,它们的文件当前位置可能不以简单的字节位移量来度量。在非UNIX系统中可能以不同的格式存放文本文件,为了定位一个文本文件,whence一定要是SEEK_SET,而且offset只能有两种值:0(表示反绕文件到其起始位置),或者是对该文件的ftell所返回的值。使用rewind函数也可以将一个流设置到文件的起始位置。
#include <stdio.h> int fgetpos(FILE *fp, fpos_t *pos); int fsetpos(FILE *fp, const fpos_t *pos); 返回值:成功为0,出错非0
fgetpos将当前位置存入pos指向的对象中。在以后调用fsetpos时,可以使用此值将流重定向至该位置。
四、 格式化IO
1. 格式化输出
#incldue <stdio.h> in printf(const char *format, ...);
返回值:成功则为输出字符数,出错为负值
int fprintf(FILE *fp, const char *format, ...);
返回值:成功则为输出字符数,出错为负值
int sprintf(char *buf, const char *format, ...);
返回值:存入数组的字符数
sprintf将格式化的字符送入数组buf中。sprintf在该数组的尾端自动加一个null字节,但该字节不包含在返回值中。sprintf有可能会使buf指向的缓存溢出。
printf族的三种变体类似于上面的三种,只不过是可变参数变成了arg
#include<stdarg.h> #include<stdio.h> int vprintf(const char * f o r m a t, va_list arg) ; int vfprintf(FILE *f p, const char * f o r m a t, va_list arg) ; 两个函数返回:若成功则为输出字符数,若输出出错则为负值 int vsprintf(char *b u f, const char * f o r m a t, va_list arg) ; 返回:存入数组的字符数
2. 格式化输入
三个scanf函数:
#include <stdio.h> int scanf(const char *format, ...); int fscanf(FILE *fp, const char *format, ...); int sscanf(const char *buf, const char *format, ...);
五、实现细节
在UNIX中标准IO最终都要调用系统IO。每个IO流都有一个与其关联的文件描述符,可以用fileno获取该流对应的文件描述符。
#include <stdio.h> int fileno(FILE *fp); 返回值:与流相关联的文件描述符
为了了解所使用的系统中标准IO的实现最好从stdio.h头文件开始。
【注:原书中下面有一个案例这里没有放出】
六、临时文件
标准IO库提供了两个函数以帮助创建临时文件
#include <stdio.h> char *tmpnam(char *ptr); 返回值:指向一唯一路径名的指针 FILE *tmpfile(void); 返回值:成功则为文件指针,出错为NULL
tmpnam产生一个与现在文件名(改文件名不是指ptr!该函数用来产生一个唯一文件)不同的一个有效路径名字符串。每次调用它时,它都产生一个不同的路径名,最多调用次数是TMP_MAX。TMP_MAX定义在<stdio.h>中
若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。下一次再调用tmpnam时会重写该静态区。(这意味着如果我们调用此函数多次,而且想保存路径名,那我们应该保存该路径名的副本而不是指针的副本) 如果ptr不是NULL,则认为它指向长度至少是L_tmpnam个字符的数组。(常数L_tmpnam定义在<stdio.h>中)所产生的路径名存放在该数组中,ptr也作为函数值返回。
tmpfile创建一个临时二进制文件。在关闭该文件或程序结束时会自动删除这种文件。
tempnam是tmpnam的一个变体,它允许调用者为所产生的路径名指定目录和前缀。
#include <stdio.h> char *tempnam(const char *directory, const char *prefix); 返回值:指向一唯一路径名的指针
对于目录有四种不同的选择:(优先级从高至低)
(1) 如果定义了环境变量TMPDIR,则用其作为目录。
(2) 如果参数directory非NULL,则用其作为目录。
(3) 将<stdio.h>中的字符串P_tmpdir用作为目录。
(4) 将本地目录,通常是/tmp用作为目录。
如果prefix非NULL,则它通常是最多包含5个字符的字符串,用其作为文件名的前几个字符。
该函数调用malloc函数分配动态存储区,用其存放所构造的路径名。当不再使用该路径名时就可释放次存储区。
以上是关于APUE读书笔记-05标准输入输出库(1)的主要内容,如果未能解决你的问题,请参考以下文章