[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函数分配动态存储区,用其存放所构造的路径名。当不再使用该路径名时就可释放次存储区。