11、格式化输入输出
(1)格式化的输出
格式化输出有如下四种函数:
#include <stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
两者返回:如果输出成功返回输出字符数目,如果输出错误返回负数。
int sprintf(char *restrict buf, const char *restrict format, ...);
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
两者返回:如果成功返回存放到数组中的字符数目,如果编码错误返回负数。
printf
将字符串写入到标准输出; fprintf
写入到指定的 stream
; sprintf
将格式化的字符串写入到 buf
数组中,并且将一个'\0'字符追加到最后,但是返回值却不包含这个'\0'; snprintf
和 sprintf
类似,但是指定了要写入的大小限制,写入包含'\0'在内最多n的字符到 buf
数组中。
需要注意的是,可能 sprintf
会溢出 buf
指向的缓存,调用这个函数的调用者需要保证缓存空间足够大。因为,会导致缓存溢出问题,所以引入了函数 snprintf
。这个函数将 buffer
大小做为一个显示指定的参数,任何可能导致写过缓存末端的字符都会被截断。如果缓存空间足够大,那么 snprintf
函数会返回写入到缓存中的字符的数目,当然和 sprintf
类似,其返回值没有将 null
所占的字节算进来。如果 snprintf
返回了一个小于缓存大小n的数目,那么输出就没被截断,如果出现编码错误,那么回返回负数。
也就是说,对于这几个函数的返回值,它们都返回不包括'\0'的、写入的字符数目。另外,如果 snprintf
由于n限制把写入的字符串截断了,那么它会返回假设没有被截断(也就是n足够大)的时候的字符串字符数目(当然也不包含'\0'),所以如果返回值是n或者更多则表示输出被截断了。
format
的格式:
参数前面的 format
参数指名了格式,其形式是:
%[flags][fldwidth][precision][lenmodifier]convtype
(最好不用翻译,但是翻译起来就是: %[标记][宽度][精度][长度调整]类型
)中括号代表可选。这里:
对于 flags
有:
- -:指定的参数的输出在所在处是左对齐的(如果存在下面将说的0那么忽略0),默认右对齐。
- +:指定对于有符号的
convtype
(相当于类型),输出其符号。 - (空格):指定如果不显示符号的话就以一个空格作为前缀。
- #:用可选的方式显示。例如:八进制类型(用o)就以0开头,十六进制(用x)就以
0x
开头等,见man
. - 0:参数的前面用0填充(如果同时存在-将被-覆盖,导致0被忽略)而不是用空格了。
对于 fldwidth
:
它指明了最小宽度,如果参数小于这个宽度就填充空格。 fldwidth
可以是一个正的十进制数,或者是一个'*'。例如:5,或者*
对于 precision:
它指定了整数的最少显示数字数目,浮点数小数点后面的数字数目以及字符串的最大字节数目。 presion
可以是一个点'.'后面接整数或者'*'。例如:.5或者.*
注意:对于 fldwidth
和 precision
都可以指定一个'*',这时候,需要在将要显示的参数前面再添加一个整数参数来指定将要显示的值。可以参见代码中的例子。其他的可以参考下面的表格,这里主要注意#和*在 format
参数中的作用。
lenmodifier
指定参数大小,其值如下:
格式规范中的长度选项
+--------------------------------------------------------------+
| Length modifier | Description |
|-----------------+--------------------------------------------|
| hh | signed or unsigned char |
|-----------------+--------------------------------------------|
| h | signed or unsigned short |
|-----------------+--------------------------------------------|
| l | signed or unsigned long or wide character |
|-----------------+--------------------------------------------|
| ll | signed or unsigned long long |
|-----------------+--------------------------------------------|
| j | intmax_t or uintmax_t |
|-----------------+--------------------------------------------|
| z | size_t |
|-----------------+--------------------------------------------|
| t | ptrdiff_t |
|-----------------+--------------------------------------------|
| L | long double |
+--------------------------------------------------------------+
convtype
在多个参数的时候是必选的,指定参数的解释方式。如下表:
格式规范中的转换类型选项
+--------------------------------------------------------------------------------------------------------------+
| Conversion type | Description |
|-----------------+--------------------------------------------------------------------------------------------|
| d,i | signed decimal |
|-----------------+--------------------------------------------------------------------------------------------|
| o | unsigned octal |
|-----------------+--------------------------------------------------------------------------------------------|
| u | unsigned decimal |
|-----------------+--------------------------------------------------------------------------------------------|
| x,X | unsigned hexadecimal |
|-----------------+--------------------------------------------------------------------------------------------|
| f,F | double floating-point number |
|-----------------+--------------------------------------------------------------------------------------------|
| e,E | double floating-point number in exponential format |
|-----------------+--------------------------------------------------------------------------------------------|
| g,G | interpreted as f, F, e, or E, depending on value converted |
|-----------------+--------------------------------------------------------------------------------------------|
| a,A | double floating-point number in hexadecimal exponential format |
|-----------------+--------------------------------------------------------------------------------------------|
| c | character (with l length modifier, wide character) |
|-----------------+--------------------------------------------------------------------------------------------|
| s | string (with l length modifier, wide character string) |
|-----------------+--------------------------------------------------------------------------------------------|
| p | pointer to a void |
|-----------------+--------------------------------------------------------------------------------------------|
| n | pointer to a signed integer into which is written the number of characters written so far |
|-----------------+--------------------------------------------------------------------------------------------|
| % | a % character |
|-----------------+--------------------------------------------------------------------------------------------|
| C | wide character (an XSI extension, equivalent to lc) |
|-----------------+--------------------------------------------------------------------------------------------|
| S | wide character string (an XSI extension, equivalent to ls) |
+--------------------------------------------------------------------------------------------------------------+
下面是前面四个类似printf格式化输出系列的函数的变种, 区别仅在于将前面的可变参数列表(…)在这里变成了参数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 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);
两者返回:如果成功将返回存放到数组buf中的字符数目,如果错误返回负数。
(2)格式化输入
如下函数进行格式化输入的处理:
#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, ...);
三者返回:指定的输入的数目。如果输入错误或者在任何 conversion
之前(也就是转化完成之前)到达文件结尾,返回 EOF
。
(这个部分说的有些费解,但是最好实践一下???)
这些格式化输入函数用来将输入的字符序列读取并转化成指定的类型变量做为程序输入。参数 format
后面的参数包含了用转化来的输入值,所初始化的变量的地址。
format
参数用来控制如何转化并赋值。%字符指明一个 conversion
规范的开始。除了空白以及 conversion
规范, format
中的其他的字符需要和输入相匹配。如果有一个字符没有匹配,那么就会导致进程停止,并留下没有读取得输入。
conversion
规范中,有三个可选的部分,通过下面的方括号括起来。
%[*][fldwidth][lenmodifier]convtype
这里,星号(*)可选,用来禁止转换。输入会按照 conversion
规范剩余的部分所指定的那样来进行转换,但是结果不会被存放到参数中去。(有些费解???)
fldwidth
部分指定最大的字符宽度。 lenmodifier
指定 conversion
的结果值初始化的参数的大小,和 printf
的含义一样。
convtype
域和 printf
的差不多,但是也有些不同。其中一个就是,存放到无符号类型的变量的转化结果,可以在输入的时候存放在有符号变量中。下表列出了各种标记:
格式规范中的转换类型
+-----------------------------------------------------------------------------------------------------------+
| Conversion | Description |
| type | |
|-----------------+-----------------------------------------------------------------------------------------|
| d | signed decimal, base 10 |
|-----------------+-----------------------------------------------------------------------------------------|
| i | signed decimal, base determined by format of input |
|-----------------+-----------------------------------------------------------------------------------------|
| o | unsigned octal (input optionally signed) |
|-----------------+-----------------------------------------------------------------------------------------|
| u | unsigned decimal, base 10 (input optionally signed) |
|-----------------+-----------------------------------------------------------------------------------------|
| x | unsigned hexadecimal (input optionally signed) |
|-----------------+-----------------------------------------------------------------------------------------|
| a,A,e,E,f,F,g,G | floating-point number |
|-----------------+-----------------------------------------------------------------------------------------|
| c | character (with l length modifier, wide character) |
|-----------------+-----------------------------------------------------------------------------------------|
| s | string (with l length modifier, wide character string) |
|-----------------+-----------------------------------------------------------------------------------------|
| [ | matches a sequence of listed characters, ending with ] |
|-----------------+-----------------------------------------------------------------------------------------|
| [^ | matches all characters except the ones listed, ending with ] |
|-----------------+-----------------------------------------------------------------------------------------|
| p | pointer to a void |
|-----------------+-----------------------------------------------------------------------------------------|
| n | pointer to a signed integer into which is written the number of characters read so far |
|-----------------+-----------------------------------------------------------------------------------------|
| % | a % character |
|-----------------+-----------------------------------------------------------------------------------------|
| C | wide character (an XSI extension, equivalent to lc) |
|-----------------+-----------------------------------------------------------------------------------------|
| S | wide character string (an XSI extension, equivalent to ls) |
+-----------------------------------------------------------------------------------------------------------+
类似格式化输出,也有一些格式化输入的变种函数:
#include <stdarg.h>
#include <stdio.h>
int vscanf(const char *restrict format, va_list arg);
int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg);
int vsscanf(const char *restrict buf, const char *restrict format, va_list arg);
所有三个函数返回:指定的输入的数目,如果输入错误或者在转化完之前 到达文件结尾,返回EOF。
译者注
printf
的例子
下面给出一个 printf
的例子:
/*程序功能:主要测试printf的格式部分
*
* 关于printf的格式:
* int printf(const char *restrict format, ...);
* 参数前面的format参数指名了格式,其形式是:
* %[flags][fldwidth][precision][lenmodifier]convtype
* 最好不用翻译,但是翻译起来就是:%[标记][宽度][精度][长度调整]类型
* 中括号代表可选。这里:
* 对于flags有:
* 1)-:指定的参数的输出在所在处是左对齐的(如果存在下面将说的0那么忽略0),默认右对齐。
* 2)+:指定对于有符号的convtype(相当于类型),输出其符号。
* 3) (空格):指定如果不显示符号的话就以一个空格作为前缀。
* 4)#:用可选的方式显示。例如:八进制类型(用o)就以0开头,十六进制(用x)就以0x开头等,见man.
* 5)0:参数的前面用0填充(如果同时存在-将被-覆盖,导致0被忽略)而不是用空格了。
*
* 对于fldwidth:
* 它指明了最小宽度,如果参数小于这个宽度就填充空格。
* fldwidth可以是一个正的十进制数,或者是一个'*'。例如:5,或者*
*
* 对于precision:
* 它指定了整数的最少显示数字数目,浮点数小数点后面的数字数目以及字符串的最大字节数目。
* presion可以是一个点'.'后面接整数或者'*'。例如:.5或者.*
*
* 注意:对于fldwidth和precision都可以指定一个'*',这时候,需要在将要显示的参数前面再添加一个整数参数来指定将要显示的值。可以参见代码中的例子。
*
* 其他的暂时不说了,这里主要就说了#和*在format参数中的作用。
*
* */
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("%-5d\n", 1);//宽度为5,左对齐
printf("%5d\n", 2);//宽度为5,默认右对齐
printf("%+5d\n", 3);//宽度为5,显示符号(正号)的3
printf("%-+5d\n", 4);//宽度为5,显示符号并且左对齐的4.
printf("%- 5d\n",5);//宽度为5,将正号不显式代之为一个空格,并且左对齐。
printf("%- 5d\n",-6);//宽度为5,左对齐同%-5d。
printf("%05d\n",7);//宽度为5,不足的填充为0。
printf("%*d\n",8,9);//宽度在参数里面指定为8(!!!!!!!!!!!!!)
printf("%0-5d\n",8);//宽度为5,不足的填充为0,忽略左对齐。
printf("%7.5d\n",8);//宽度为7,精度为5的整数(00008)。
printf("%7.5f\n",7.1);//宽度为7,精度为5的浮点数(7.10000)。
printf("%7.*f\n",3,6.1);//宽度为7,精度在参数中指定为3的浮点数(6.100)。
printf("%3.5f\n",5.1);//宽度为3,精度为5的浮点数,精度大于宽度(5.10000)。不被截断!!
printf("%7.3f\n",4.12345);//宽度7,精度3的浮点数,精度小于实际的数值精度(4.123)。截断!!
printf("%3s\n", "abcdef");//宽度3长度大于3的字符串.不被截断!!
printf("%.3s\n", "abcdef");//精度3长度大于3的字符串.截断!!
printf("%.3s\n", "abc");//精度5长度小于5的字符串,没有什么.
return 0;
}
原文参考
12、实现细节
每一个标准 I/O
流都和一个文件描述符号相关联,通过如下函数获得一个标准 I/O
流的文件描述符号:
#include <stdio.h>
int fileno(FILE *fp);
返回:和 fp
流相关的文件描述符号。
注意这个函数不是 ISO C
指定的,而是被做为 POSIX.1
的扩展指定。
下面例子打印三种标准流以及一个普通文件的缓存:
void pr_stdio(const char *, FILE *);
int main(void)
{
FILE *fp;
fputs("enter any character\n", stdout);
if (getchar() == EOF)
err_sys("getchar error");/*错误情况,这里可以忽略它。该函数是原书中为了叙述方便实现的,可参考原书附录*/
fputs("one line to standard error\n", stderr);
pr_stdio("stdin", stdin);
pr_stdio("stdout", stdout);
pr_stdio("stderr", stderr);
if ((fp = fopen("/etc/motd", "r")) == NULL)
err_sys("fopen error");
if (getc(fp) == EOF)
err_sys("getc error");
pr_stdio("/etc/motd", fp);
exit(0);
}
void pr_stdio(const char *name, FILE *fp)
{
printf("stream = %s, ", name);
/*
* The following is nonportable.
*/
if (fp->_IO_file_flags & _IO_UNBUFFERED)
printf("unbuffered");
else if (fp->_IO_file_flags & _IO_LINE_BUF)
printf("line buffered");
else /* if neither of above */
printf("fully buffered");
printf(", buffer size = %d\n", fp->_IO_buf_end - fp->_IO_buf_base);
}
需要注意的是,我们在打印缓存状态之前对每个流进行了 I/O
操作,因为第一次的 I/O
操作会为流分配一个缓存。结构成员 _IO_file_flags
, IO_buf_base
, 以及 _IO_buf_end
和常量 _IO_UNBUFFERED
与 _IO_LINE_BUFFERED
由 Linux 上面的GNU标准I/O库定义。需要注意其他的UNIX系统可能有不同的标准I/O库实现。
下面是运行以上程序两次的结果,一次将三个标准流连接到终端,一次将它们重定向到文件:
$ ./a.out stdin, stdout, and stderr connected to terminal
enter any character
we type a newline
one line to standard error
stream = stdin, line buffered, buffer size = 1024
stream = stdout, line buffered, buffer size = 1024
stream = stderr, unbuffered, buffer size = 1
stream = /etc/motd, fully buffered, buffer size = 4096
$ ./a.out < /etc/termcap > std.out 2> std.err
run it again with all three streams redirected
$ cat std.err
one line to standard error
$ cat std.out
enter any character
stream = stdin, fully buffered, buffer size = 4096
stream = stdout, fully buffered, buffer size = 4096
stream = stderr, unbuffered, buffer size = 1
stream = /etc/motd, fully buffered, buffer size = 4096
由上可知,默认系统当标准输入输出连接到终端上面的时候,其缓存是行缓存。行缓存长度是 1024
字节。这并不表示我们只能进行 1024
字节的行输入和输出,那只是一个缓存的大小。所以如果进行了 2048
字节的行输出将会导致调用两次 write
系统调用。当我们将这两个流重定向到普通文件的时候,它们是满缓存的,缓存的大小和文件系统 I/O
时候导致更优时间、 stat
结构的、 st_blksize
大小相等。我们也看到,标准错误总是非缓存的(重定向之后也如此),普通文件是满缓存的。
译者注
原文参考
13、临时文件
ISO C
标准定义了两个函数,它们由标准 I/O
库赖提供,以创建临时文件。
#include <stdio.h>
char *tmpnam(char *ptr);
返回:一个指向唯一路径的指针。
FILE *tmpfile(void);
返回:如果成功返回文件指针,如果错误返回 NULL
。
tmpnam
函数会产生一个字符串(合法路径),这个字符串将会和已有的任何文件名称不相同。每次调用这个函数的时候,都会产生一个不同的字符串,最多使用 TMP_MAX
次, TMP_MAX
这个常量在 <stdio.h>
文件中有定义。如果 ptr
是 NULL
的,那么产生的字符串就会存储在一个静态的区域,并且返回指向这个区域的指针。之后的每一个 tmpnam
调用都会重写这个静态存储区域(如果我们要保存原来的值,需要自己复制一份)。如果 ptr
参数非 NULL
,那么会假设指向一个至少有 L_tmpnam
个字符的数组中( L_tmpnam
在 stdio.h
中有定义),生成的路径就存放在这个数组中,并且 prt
作为函数的返回值。
tmpfile
函数创建一个临时的二进制文件(指定 wb+
模式),这个文件在它被关闭或者程序结束的时候会被自动删除。
一般 tmpfile
函数采用的技术是调用 tmpnam
来生成一个唯一的路径,然后创建相应的文件,然后立即将这个文件 unlink
.注意, unlink
并没有把文件的内容删除了,这样,只在这个程序空间中可以访问这个文件,(在系统中已经无法再访问这个文件了,因为虽然文件的内容还健在, unlink
之后没有了相应的文件路径_)。这样,当文件被关闭的时候,或者程序退出的时候,这个文件的内容就会被删除了。
例子:
int main(void)
{
char name[L_tmpnam], line[MAXLINE];
FILE *fp;
printf("%s\n", tmpnam(NULL)); /* first temp name */
tmpnam(name); /* second temp name */
printf("%s\n", name);
if ((fp = tmpfile()) == NULL) /* create temp file */
err_sys("tmpfile error");
fputs("one line of output\n", 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);
}
SUS
定义了两个额外的函数,做为 Single UNIX Specification
的 XSI
扩展,来创建临时文件:
#include <stdio.h>
char *tempnam(const char *directory, const char *prefix);
返回:一个唯一路径的指针。
tempnam
函数是 tmpnam
的一个变体,它允许调用者指定生成的字符串的目录和前缀。然后这个函数按照如下的规则依次进行处理:
- 如果
TMPDIR
环境变量被定义了,那么它将会被用作directory
. - 如果
directory
是非NULL
的,那么它会被用作目录。 - 采用
stdio.h
中的P_tmpdir
字符串作为directory
. - 采用本地的一个目录,一般是
/tmp
做为directory
.
如果 prefix
是非 NULL
,那么它应该是个至多5个字节的字符串,做为生成的字符串相应文件的前缀。
这个函数调用 malloc
函数来为生成的 pathname
存储分配空间,我们处理的时候,可以将这个 pathname
的存储空间给 free
掉。
如果 directory
是一个不存在的目录,那么规则中的2将会被忽略,然后采用下一条匹配规则。
#include <stdlib.h>
int mkstemp(char *template);
返回:如果成功返回文件描述符号,如果错误返回1。
mkstemp
函数,也会创建临时文件,不同的是它返回一个文件描述符号。临时文件的文件名称使用 template
字符串来进行生成。这个字符串是一个路径名称,它的后六个字符被设置成 XXXXXX
.这个函数会把这些字符替换成不同的字符,这样可以创建一个唯一的路径名称。如果 mkstemp
返回 success
,它会修改 template
字符串,这个字符串就是生成的临时文件。
和 tmpfile
不同,由 mkstemp
生成的临时文件,不会被自动地移除。如果我们想要把它从文件系统中去掉,我们需要自己亲自对它进行 unlink
.
tmpnam
和 tempnam
有一个缺点:在返回 pathname
和创建 file
的时候,会有一个时间间隙,在这个时间间隙内有可能其他的程序会创建同样的名称文件,而 tempfile
和 mkstemp
函数就没有这个缺陷。
mktemp
函数和 mkstemp
类似,不同的是,它创建一个只适合用作临时文件的名称。这个 mktemp
函数,并不创建文件,所以它也有 tmpname
和 tempnam
同样的缺点。 mktemp
函数是 Unix Specification
中将要被废弃的函数,所以要尽量避免使用它。
译者注
原文参考
14、标准输入输出库的替代库
除了标准输入输出库( stdio
)之外,还有其他的可选的输入输出库用于特定的目的。
标准输入输出库并不是完美的,例如在我们使用 line
的 buffer
流进行标准输入输出时候,需要拷贝两次数据:一次是从内核到标准输入输出流的 buffer
,另外一次是从标准输入输出流的 buffer
拷贝到我们自己的 linebuffer
(前面第5章2节中也提到过这一点)。 fast i/o
( fio
)库解决了这个问题。
sfio
库的速度类似 fio
库,通常都比 stdio
快。还有适合嵌入式占用内存较少的 uClibc
和 newLibc
等。
译者注
原文参考
15、总结
标准输入输出库被大多数 UNIX
应用程序使用,我们本章看到了这个库提供的所有函数,以及一些实现的细节和效率方面问题的考虑。我们需要注意这个库中的缓存的情况,因为正是由于这些缓存,导致产生了很多让人困扰的问题。