本文是笔者拜读《UNIX环境高级编程》第5章(标准I/O库)的学习笔记。本文的主要内容包括二进制I/O、定位流、格式化I/O、临时文件。文中不仅包含书中的知识点,也包括笔者的理解。
二进制I/O
在执行二进制I/O
操作时,通常一次性读写一个完整的数据块。下面两个函数执行二进制I/O
操作。
size
表示每个数据项的大小,nmemb
表示要读写的数据项数目。
这些函数有以下两种常见的用法:
(1)
读写一个二进制数组。例如,将一个浮点数组的第2~5
(从0
开始计数)个元素写到一个文件上。
float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4) {
perror("fwrite error");
}
(2)
读或写一个结构。
struct {
short count;
long total;
char name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1) {
perror("fwrite error");
}
二进制I/O
只能用于读写在同一系统上的数据。而在如今的异构系统中,常常在一个系统上写数据,在另一个系统上进行处理。这种情况下,fread
和fwrite
可能就不能正常工作,因为:
(1)
在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同。
(2)
多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。
在不同系统之间交换二进制数据的实际解决方法是使用互认的规范格式。
定位流
有3
种方法定位标准I/O
流。
(1)
ftell
和fseek
函数。这些函数假定文件的位置可以存放在一个长整型中。
(2)
ftello
和fseeko
函数。使用off_t
数据类型表示文件偏移量。
(3)
fgetpos
和fsetpos
函数。这些函数使用一个抽象数据类型fpos_t
记录文件的位置,这种数据类型可以根据需要定义为一个足够大的数。
ftell
返回当前文件位置指示;fseek
设置文件位置。文件位置相当于文件偏移量,fseek
相当于lseek
。使用rewind
可将一个流设置到文件的起始位置。
除了偏移量类型以外,ftello
和ftell
相同,fseeko
和fseek
相同。
fgetpos
将文件位置指示器的当前值存入由pos
指向的对象中(功能类似于ftell
),调用fsetpos
时,可以使用此值将流重新定位到该位置。
格式化I/O
格式化输出
printf
将格式化数据写到标准输出,fprintf
写至指定的流,dprintf
写至指定的文件描述符,sprintf
写至指定的数组(在数组尾端自动加一个'\0'
)。
为了解决缓冲区溢出问题,引入了指定缓冲区长度的snprintf
函数,超过缓冲区尾端的字符都会被丢弃。
格式说明控制其余参数如何编写,如何显示。每个参数按照转换说明编写,转换说明以%
开始。除转换说明外,格式字符串中的其它字符将按原样输出。一个转换说明有4
个可选择部分:
%[flags][fldwidth][precision][lenmodifier]convtype
flags | 说明 |
---|---|
' | 将整数按千位分组 |
- | 左对齐 |
+ | 带正负号 |
(空格) | 如果第一个字符不是正负号,则在前面加上一个空格 |
# | 指定另一种转换格式 |
0 | 使用0 进行填充 |
fldwidth
指定了最小字段宽度。转换后参数字符数若小于宽度,则多余字符用空格填充。字符宽度是一个非负的十进制数,或者*
。
precision
指定了精度。例如整型的数字位数、浮点数的小数位数、字符串的长度。精度表示为.
后面跟个可选的非负十进制数或*
。
宽度和精度字段皆可为*
。此时,一个整型参数指定宽度或精度的值,该整型参数正好位于被转换的参数之前。
lenmodifier
说明参数长度。
长度修饰符 | 说明 |
---|---|
hh | 将相应的参数按signed 或unsigned char 类型输出 |
h | 将相应的参数按signed 或unsigned short 类型输出 |
l | 将相应的参数按signed 或unsigned long 类型输出 |
ll | 将相应的参数按signed 或unsigned long long 类型输出 |
j | intmax_t 或uintmax_t |
z | size_t |
t | ptrdiff_t |
L | long double |
convtype
是必选的,它控制如何解释参数。
转换类型 | 说明 |
---|---|
d,i | 有符号十进制 |
o | 无符号八进制 |
u | 无符号十进制 |
x ,X | 无符号十六进制 |
f ,F | 双精度浮点数 |
e ,E | 指数格式双精度浮点数 |
g ,G | 根据转换后的值解释为f ,F ,e 或E |
a ,A | 十六进制指数格式双精度浮点数 |
c | 字符(若带长度修饰符l ,则为宽字符) |
s | 字符串(若带长度修饰符l ,则为宽字符) |
p | void* |
n | 到目前为止,此printf 调用输出的字符的数目将被写入到指针所指向的带符号整型中 |
% | 一个% 字符 |
C | 宽字符(等效于lc ) |
S | 宽字符串(等效于ls ) |
常规的转换是按照format
和...
中参数出现的顺序的,一种替代的转换说明语法也允许显式地用%n$
序列来表示第n
个参数的形式来命名参数(从1
开始计数)。这两种语法不能在同一格式说明中混用。如果参数既没有提供字段宽度也没有提供精度,通配符星号的语法就更改为*m$
,m
指明提供值的参数的位置。
vprintf
族是printf
族的变体,它的可变参数表...
被替换成了va_list
。
例:
#include <stdio.h>
int main() {
char str[] = "hello";
char ch = 'z';
int a = -123;
int b = 12339783;
double c = 13.234;
printf("str: %-10s!\n", str);
printf("ch: %c!\n", ch);
printf("a: %+d!\n", a);
printf("b: %+10d!\n", b);
printf("c: %.2f!\n", c);
return 0;
}
运行结果:
格式化输入
scanf
族用于分析输入字符串,并将字符序列转换成指定类型的变量,格式之后的个参数包含了变量的地址,用转换结果对这些变量赋值。
格式说明控制如何转换参数,以%
开始。除转换说明和空白字符外,格式字符串中的其它字符必须与输入匹配,若有一个不匹配,则停止后续处理。
%[*][fldwidth][m][lenmodifier]convtype
*
用于抑制转换。按照转换说明的其余部分对输入进行转换,但转换结果并不存放在参数中。
dlfwidth
说明最大宽度(最大字符数)。lenmodifier
说明要用转换结果赋值的参数大小。由printf
函数族支持的长度修饰符同样得到scanf
族函数的支持。
convtype
字段类似于printf
族的转换类型字段,但输入中带符号的可赋予无符号类型。例如输入流中的-1
被转换成UINT_MAX
。
在字段宽度和长度修饰符之间的m
是赋值分配符。它可用于%c
、%s
以及%[转换符
,迫使内存缓冲区分配空间以接纳转换字符串。
scanf
族同样支持显式地命名参数。
scanf
的转换类型与printf
大概一样。
转换类型 | 说明 |
---|---|
[ | 匹配列出的字符序列,以] 终止 |
[^ | 匹配除列出字符以外的所有字符,以] 终止 |
例:
练习scanf
函数。
#include <stdio.h>
int main() {
int a;
float b;
char c[100];
char d;
int e;
int n = fscanf(stdin, "%d %f %s %*[a-zA-Z] %c %d", &a, &b, c, &d, &e);
fprintf(stdout, "get %d variables\n", n);
fprintf(stdout, "%d %.2f %s station %c %d\n", a, b, c, d, e);
return 0;
}
运行结果:
程序中%*[a-zA-Z]
表示,如果匹配到了一个或多个字母,则忽略它们(不用将读到的字符串赋值给某个变量),否则函数返回或匹配下一个转换说明。
%*[^a-zA-Z]
表示,如果匹配到了一个或多个非字母,则忽略它们,否则函数返回或匹配下一个转换说明。
scanf
里的内容真复杂,笔者说不清楚。
实现细节
每个标准I/O
流都有一个与其相关联的文件描述符,可以对一个流调用fileno
函数以获得其描述符。
在打印缓冲状态信息之前,先对每个流执行I/O
操作,第一个I/O
操作通常就造成为该流分配缓冲区。在不同系统中,标准I/O库的实现有所不同,标准定义的是声明(接口)。
当标准输入、标准输出连至终端时,它们是行缓冲的。当将这两个流重定向到普通文件时,它们就是全缓冲的。不论重定向与否,标准错误流始终是不带缓冲的。
临时文件
以下函数创建临时文件。
tmpnam
函数产生一个与现有文件名不同的一个有效路径名字符串。
若s
是NULL
,则产生的路径名存放在静态区中,指向该静态区的指针作为函数的返回值。后续调用tmpnam
时,会重写该静态区。
若s
不是NULL
,则认为它指向一个字符数组,所产生的路径名存放在该数组中,返回s
。
tmpfile
使用tmpnam
生成的路径名(通常在/tmp/
目录下)创建一个临时二进制文件(类型wb+
),在关闭该文件或程序结束时会自动删除该文件。
mkdtemp
和mkstemp
也能创建临时文件。
mkdtemp
函数创建了一个目录,该目录有一个唯一的名字;mkstemp
函数创建了一个文件,该文件有一个唯一的名字。名字是通过template
字符串进行选择的。这个字符串是后6
位设置为XXXXXX
的路径名,函数将这些占位符替换成不同的字符来构建一个唯一的路径名。
由mkdtemp
函数创建的目录使用下列访问权限位集:S_IRUSR | S_IWUSR | S_IXUSR
,可以调用umask
进一步限制这些权限。mkdtemp
返回新目录的名字。
由mkstemp
创建的临时文件使用访问权限位S_IRUSR | S_IWUSR
,mkstemp
创建的临时文件不会被自动删除,必须对它解除链接。
例:
临时文件的创建和关闭
// temp.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
char buf1[100];
tmpnam(buf1);
FILE *fp = tmpfile();
fprintf(fp, "%s: %s", buf1, "hello");
rewind(fp);
fgets(buf1, 100, fp);
printf("%s\n", buf1);
fclose(fp);
char buf2[] = "/tmp/testXXXXXX";
int fd = mkstemp(buf2);
fp = fdopen(fd, "r+");
fprintf(fp, "%s: %s", buf2, "hello");
rewind(fp);
fgets(buf1, 100, fp);
printf("%s\n", buf1);
unlink(buf2);
fclose(fp);
return 0;
}
运行结果: