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'; snprintfsprintf 类似,但是指定了要写入的大小限制,写入包含'\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或者.*

注意:对于 fldwidthprecision 都可以指定一个'*',这时候,需要在将要显示的参数前面再添加一个整数参数来指定将要显示的值。可以参见代码中的例子。其他的可以参考下面的表格,这里主要注意#和*在 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;
}

原文参考

参考: APUE2/ch05lev1sec11.html

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 大小相等。我们也看到,标准错误总是非缓存的(重定向之后也如此),普通文件是满缓存的。

译者注

原文参考

参考: APUE2/ch05lev1sec12.html

13、临时文件

ISO C 标准定义了两个函数,它们由标准 I/O 库赖提供,以创建临时文件。

#include <stdio.h>
char *tmpnam(char *ptr);

返回:一个指向唯一路径的指针。

FILE *tmpfile(void);

返回:如果成功返回文件指针,如果错误返回 NULL

tmpnam 函数会产生一个字符串(合法路径),这个字符串将会和已有的任何文件名称不相同。每次调用这个函数的时候,都会产生一个不同的字符串,最多使用 TMP_MAX 次, TMP_MAX 这个常量在 <stdio.h> 文件中有定义。如果 ptrNULL 的,那么产生的字符串就会存储在一个静态的区域,并且返回指向这个区域的指针。之后的每一个 tmpnam 调用都会重写这个静态存储区域(如果我们要保存原来的值,需要自己复制一份)。如果 ptr 参数非 NULL ,那么会假设指向一个至少有 L_tmpnam 个字符的数组中( L_tmpnamstdio.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 SpecificationXSI 扩展,来创建临时文件:

#include <stdio.h>
char *tempnam(const char *directory, const char *prefix);

返回:一个唯一路径的指针。

tempnam 函数是 tmpnam 的一个变体,它允许调用者指定生成的字符串的目录和前缀。然后这个函数按照如下的规则依次进行处理:

  1. 如果 TMPDIR 环境变量被定义了,那么它将会被用作 directory .
  2. 如果 directory 是非 NULL 的,那么它会被用作目录。
  3. 采用 stdio.h 中的 P_tmpdir 字符串作为 directory .
  4. 采用本地的一个目录,一般是 /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 .

tmpnamtempnam 有一个缺点:在返回 pathname 和创建 file 的时候,会有一个时间间隙,在这个时间间隙内有可能其他的程序会创建同样的名称文件,而 tempfilemkstemp 函数就没有这个缺陷。

mktemp 函数和 mkstemp 类似,不同的是,它创建一个只适合用作临时文件的名称。这个 mktemp 函数,并不创建文件,所以它也有 tmpnametempnam 同样的缺点。 mktemp 函数是 Unix Specification 中将要被废弃的函数,所以要尽量避免使用它。

译者注

原文参考

参考: APUE2/ch05lev1sec13.html

14、标准输入输出库的替代库

除了标准输入输出库( stdio )之外,还有其他的可选的输入输出库用于特定的目的。

标准输入输出库并不是完美的,例如在我们使用 linebuffer 流进行标准输入输出时候,需要拷贝两次数据:一次是从内核到标准输入输出流的 buffer ,另外一次是从标准输入输出流的 buffer 拷贝到我们自己的 linebuffer (前面第5章2节中也提到过这一点)。 fast i/o ( fio )库解决了这个问题。

sfio 库的速度类似 fio 库,通常都比 stdio 快。还有适合嵌入式占用内存较少的 uClibcnewLibc 等。

译者注

原文参考

参考: APUE2/ch05lev1sec14.html

15、总结

标准输入输出库被大多数 UNIX 应用程序使用,我们本章看到了这个库提供的所有函数,以及一些实现的细节和效率方面问题的考虑。我们需要注意这个库中的缓存的情况,因为正是由于这些缓存,导致产生了很多让人困扰的问题。

译者注

原文参考

参考: APUE2/ch05lev1sec15.html