1 二进制IO(Binary IO)
在前一篇我们了解了逐字符读写和逐行读写函数。
如果我们在读写二进制文件,希望以此读写整个文件内容,这两个函数虽然可以实现,但是明显会很麻烦且多次循环明显效率很低。
为了应对这种场景,标准IO库提供了fread和fwrite函数。
函数声明:
#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);
函数用法;
a) 读写一个数组。
float data[10];
if (write(&data[2], sizeof(float), 4, fp) != 4)
err_sys(“fwrite error");
本例中,从流fp中读取4个float型数据填入到数组下表从2到5得位置中。
b) 读写一个结构体
struct {
short count;
long total;
char name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
err_sys(“fwrite error");
本例中,从fp读取数据填入到一个结构体中。
上面两例都可以认为是读写一个结构体的数组,参数size是结构体的长度,参数nobj是数组中要读写的元素的个数。
函数返回值:
两个函数的返回值都是读写的元素个数。
对于读函数,返回值可能会比nobj小,如果有异常抛出或者读到了文件结尾。这时需要调用函数ferror或feof来判断。
对于写函数,返回值比nobj小,则一定是有异常抛出。
函数细节:
在上面的例子中,我们通过fwrite函数填充了一个结构体,那么如果读写不在一个系统中,那么结构体的内存布局可能并不相同,这对于现在的多系统互联工作的场景下很常见。我们会在讨论socket时回来继续看这个问题,实际的解决方案就是在不同系统间读写二进制数据时使用相同的协议。
2 定位流(Positioning a Stream)
我们有三种方法对流进行定位:
- 函数ftell和fseek。将文件的当前偏移位置存储在long integer型变量中;
- 函数ftello和fseeko。将文件的当前偏移量存储在off_t型变量中;
- 函数fgetpos和fsetpos。使用数据类型fpos_t记录文件的当前偏移量。
ftell和fseek函数声明:
#include <stdio.h>
long ftell(FILE* fp); // Returns:current file position indicator if OK, -1L on error
int fseek(FILE* fp, long offset, int whence); // Returns:0 if OK , -1 on error
void rewind(FILE* fp);
函数细节:
- 二进制文件的偏移量是从文件开始到当前位置的字节数;
- ftell函数返回当前文件的偏移位置;
- fseek函数用来定位文件到指定偏移位置;
- fseek函数的参数whence,用来设置计算偏移量的方法:SEEK_SET表示从文件开头开始计算,SEEK_CUR表示从文件当前偏移位置开始计算,SEEK_END表示从文件结尾开始计算。
- 对于一些非Unix操作系统,存储文本文件的存储格式会有所不同,当前文件偏移量无法通过字节数来表示,这种情况下,参数whence需要设置为SEEK_SET,并且offset只有两个值可以使用:0,表示倒回文本开头;另一个可用值为函数ftell的返回值。
ftello和fseeko函数声明:
#include <stdio.h>
off_t ftello(FILE* fp); // Returns: current file position indicator if OK, (off_t) -1 on error
int fseeko(FILE* fp, off_t offset, int whence); /// Returns: 0 if OK, -1 on error
函数细节:
- 这两个函数和上面的ftell和fseek功能相同,只是返回值类型不是long,而改成了off_t,实现上可以让off_t的表示范围更大。
fgetpos和fsetpos函数声明:
#include <stdio.h>
int fgetpos(FILE* restrict fp, fpos_t *restrict pos);
int fsetpos(FILE* fp, const fpos_t pos);
函数细节:
- fgetpos函数保存当前文件偏移量到参数pos中
- fgetpos得到的pos可以用来使用fsetpos设置当前文件偏移量到之前的位置。
3 格式化输入输出
格式化输出函数
有五个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, ..);
// All three return : number of characters output if OK , negative value if output error
int sprintf(char *resrict buf, const char *restrict format, ...);
// Returns: number of characters stored in array if OK, negative value if encoding error
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
// Returns: number of characters,that would have been stored in array if buffer was large enough, negative value if encoding error
函数细节:
- printf输出到标准输出;
- fprintf输出到指定的流中;
- dprintf输出到指定的文件描述符中;
- sprintf将格式化字符串写入到指定的buffer数组中,自动在结尾处加上一个null结尾符,但是不计入返回值中,并且,sprintf在buffer不够大时可能发生越界,因此需要使用者保证buffer足够大;
- snprintf防止越界,在springf的参数中增加了buffer的大小参数,所有越界写入的字符都被忽略,如果返回值比buffer得长度要小,则说明输出没有被截断。
格式化输入函数
函数声明:
#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, ...);
函数细节:
- format参数后面接得参数,包含存放读入字符串的变量地址。
更多关于格式化输入输出的细节可以自己查询Unix操作系统手册。
4 从流中获取文件描述符
函数声明:
#include <stdio.h>
int fileno(FILE* fp); // Returns: the file descriptor associated with the stream
如果我们需要调用dup和fcntl,则需要调用该函数。
5 临时文件(Temporary Files)
标准IO库提供了两个函数用于创建临时文件。
函数声明:
#include <stdio.h>
char* tempnam(char *ptr);
FILE* tmpfile(void);
函数细节:
- 函数tmpnam生成一个字符串,该字符串为一个合法的路径名,并且不和任何已存在的文件重复。
- 函数tmpnam每次调用都生成不同的字符串,知道TMP_MAX次数。
- 如果函数tempnam的参数ptr为NULL,则生成的路径字符串存在内存静态区,函数返回值为指向该路径字符串的指针。如果随后再次使用null参数调用tempnam,会覆盖之前生成的字符串。
- 如果函数tempnam的参数ptr不是NULL,那么生成的路径字符串存在ptr指向的数组内,所以需要保证ptr指向的数组的长度至少为L_tmpnam。
- 函数tmpfile函数创建一个临时二进制文件(type wb+),程序终止或者该文件被关闭,则该文件自动被删除。对于UNIX操作系统而言,生成一个二进制文件并没有什么影响,因为内核并不区分文本文件还是二进制文件。
Example:
Code:
#include "apue.h"
int
main(void)
{
char name[L_tmpnam], line[MAXLINE];
FILE *fp;
printf("%s ", tmpnam(NULL)); /* first temp name */
tmpnam(name); /* second temp name */
printf("%s ", name);
if ((fp = tmpfile()) == NULL) /* create temp file */
err_sys("tmpfile error");
fputs("one line of output ", fp); /* write to temp file */
rewind(fp); /* then read it back */
if (fgets(line, sizeof(line), fp) == NULL)
err_sys("fgets error");
fputs(line, stdout); /* print the line we wrote */
exit(0);
}
在系统The Single UNIX Specification定义了另外两个函数处理临时文件:
函数声明:
char* mkdtemp(char* template); // Returns: pointer to directory name if OK, NULL on error
int mkstemp(char* template); // Returns: file descriptor if OK, -1 on error
函数细节:
- mkdtemp函数创建一个名字唯一的文件夹
- mkstemp函数创建一个名字唯一的常规文件(regular file)
- 命名规则为 template + 六位随机字符
6 内存流(Memory Streams)
有的标准输入输出流并没有对应打开的硬盘文件,所有操作都是与内存中buffer进行数据交换,这些流被叫做内存流(memory streams)。
函数声明:
#include <stdio.h>
FILE* fmemopen(void *restrict buf, size_t size, const char *restrict type);
// Returns: stream pointer if OK, NULL on error
函数细节:
- 参数buf指定使用的buffer,size为该buffer的大小,如果只指定size,而buf为null,那么fmemopen根据size的大小分配内存,由fmemopen分配的内存在流关闭时自动被释放;
- 参数type控制该流的功能.
7 总结
标准IO函数库被大多数UNIX应用使用。
在使用的时候,注意哪里使用了buffer来处理,因为这是容易引起迷惑的地方。
参考资料:
《Advanced Programming in the UNIX Envinronment 3rd》