2.8 格式I/O
前几节介绍的流I/O函数除了以字符或行方式进行读写外,并不对数据进行解释,但在很多时候应用都会需要对输入输出数据进行解释,因为数据在计算机内的表示和人们可读的形式是不同的。数据在计算机内是二进制形式,在计算机外部常常为正文形式。例如,十进制数12在计算机内部的32位二进制表示是:00000000000000000000000000001100。当这个数在打印机上输出或者在终端屏幕上显示时,必须转换为字符'1'和'2',它们的ASCII编码分别为00110001和00110010。反之,当从键盘读入用ASCII字符表示的十进制整数时,必须将它们转换成计算机可处理的二进制表示。格式I/O函数能够自动完成这种外部和内部格式之间的转换工作,并且能够对输入输出数据进行诸如数据类型、精度、位置等格式控制。
所有格式I/O函数都通过一个格式字符串来对其余参数进行格式描述。格式字符串中用转换区分符来描述待输入输出参数的类型、精度、外部形式以及占据的字节宽度等。掌握好转换区分符是使用格式I/O函数的关键。这一节介绍格式I/O函数,给出转换区分符的语法成分,并通过例子说明它们的用法。
2.8.1 格式输出
格式输出由如下三种printf()函数来处理,它们是完成输出最方便的函数。
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
这三个函数功能相同,都在格式字符串format的控制下输出其他参数(这里表示为“...”)。不同的只是输出的流不同:printf()输出至标准输出流;fprintf()输出至参数stream指定的流;sprintf()不是输出至一个文件,而是输出至参数buf所指的字符数组中,并且在buf的末尾自动添加一个空字节。它们调用成功均返回实际输出的字符个数。
这些函数允许任意个数参数,格式字符串format中的转换区分符隐含地指明后继参数个数以及怎样解释和格式化这些参数。如果转换区分符的个数少于后继的参数个数,多余的参数将被忽略;如果转换区分符的个数多于后继的参数个数,多余的转换区分符输出的内容是不确定的。
格式字符串format由两类成分组成:普通字符和转换区分符。普通字符是除转换区分符之外的其余字符。输出格式字符串中的普通字符简单地按原样写至输出流。
转换区分符是以'%'开头、按如下语法规则组成的连续字符:
%[posp$][flags][width][.prec][size]fmt
其中只有fmt是必需的。fmt是表2-1列出的字符之一,称为转换字符。转换字符用于指明参数的类型和基本格式。形如%fmt的转换区分符是最基本且使用最频繁的形式。
下面是使用单个格式转换字符输出的例子:
为了更精确地对格式进行控制,在'%'和转换字符之间也可以有选择地写上各种修饰符。例如,可以用修饰符posp$指定下一个要输入输出的参数,用width指定数据占据的最小域宽度,用prec指定浮点数小数点后保留的位数,还可以用标志flags指定结果在域中的对齐方式。表2-2给出了主要的修饰符及其作用。
例如,在转换区分符'%-10.8ld'中,'-'是一个标志;'10'指明域宽,其精度为'8';字母'l'是类型修饰符(size),它指明参数所占存储字节大小;而最后一个字母'd'是转换字符。这个特定的转换区分符指明了在10字符宽度的域中以8位数左对齐方式打印一个类型为long int的十进制整数。
转换区分符所允许的修饰符以及含义根据具体的转换字符而变化,它们看起来似乎有点复杂。不过,在开始时我们几乎总是能够完全不用这些修饰符而实现相当自由的格式输出。完全掌握转换区分符并不困难,在掌握了它的语法和常用的转换字符之后,便可以根据需要灵活使用修饰符对打印格式进行调整。修饰符大部分不过是用来使得输出看起来更漂亮而已。下面是使用各种修饰符打印输出的例子。
请注意这些例子中,当参数的实际字符个数超过域宽时,按参数需要的域宽输出;例如,例4和例15虽然指明了域宽,但实际输出都超出了域宽范围。
在各种修饰符中,特别应当注意的是长度修饰符。长度修饰符是保证参数转换正确性必不可少的。当参数的类型是长整型时,一定要注意使用长度修饰符,否则输出的结果会不正确。对于浮点类型,由于默认情形下总是将它们提升为双精度类型进行处理,因此除了精度丢失外不会出现大的错误。但是,对于整数类型,不论长度是什么,默认情形下总是将它们处理成int类型。如果对长度占8字节的long int类型参数不指定长度修饰符'l',printf()将视为4字节的int类型进行格式转换,因而导致结果错误,如例16所示。
标准I/O库中还有另外三个格式输出函数vprintf()、vfprintf()、vsprintf(),它们与上述三个函数类似,不同之处仅在于用可变参数指针数组arg替代了输出变量参数表。
2.8.2 格式输入
常用的格式输入函数有如下三个:
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(char *s, const char *format, ...);
这三个函数都按format规定的格式读数据,不同的是scanf()读标准输入流,fscanf()读stream指定的流,sscanf()不是读文件而是读参数s指定的字符数组。每一个函数根据format参数读取若干字节,按指定的格式解释它们并存储结果于对应的参数中。位于format之后的可选参数应当是指向存放接收输入值的变量的指针,记住这一点非常重要。这些函数的正常返回值是成功读入了值的参数个数。
format字符串由三类成分组成:一至多个连续的空白符,普通字符(不包括'%'和空白符),转换区分符。这里特别提请读者注意区分术语“空白符”、“空格符”和“空字符”。空白符是空格符' '、制表符't'、水平制表符'v'、换行符'r'和走纸符'f'的统称,即isspace()返回值为真的字符的统称;空格符只指' '(即'040');空字符是NULL(即'0')。
格式字符串中的空白符导致scanf()跳过输入流中的空白字符,直至遇到一个未读过的非空白字符或者遇到文件尾为止。输入流中的空白字符不必完全与格式字符串中的空白符相同。例如格式字符串" , "识别输入流中一个前后有任意空白的逗号。
格式字符串中的普通字符指明必须出现在输入流中的字符,它必须完全与输入流中的下一字符相匹配,如果不匹配将导致匹配失败。例如:
int num;
scanf("Hello %d",&num);
仅当在标准输入流中当前的五个连续字符是“Hello”时,scanf()调用才会成功。在此之后,如果后面的字符形成了一个可识别的十进制数,则该数将被读出并存储在变量num中。这意味着对如下两种输入scanf()调用都将成功并赋值1234给num:
Hello 1234
Hello1234
格式字符串中的转换区分符指导输入域的转换。输入域定义为输入流中非空白字符组成的字符序列,其长度直至遇到一个不合适的字符或者到达指定的域宽为止。转换后的结果存储在对应的参数中,除非转换区分符指明了禁止赋值标志'*'。
输入转换区分符的一般形式为:
%[posp$][*][width][size]fmt
其中fmt是表2-1给出的转换字符,表2-3列出了转换区分符的常用输入修饰符。
同输出转换一样,输入转换也可通过指明域宽来限制输入吸收的字符数,可以用长度修饰符指明接收输入值参数的类型长度;也可以用%n$输入转换指定将输入结果存放在第n个参数所指对象中而不是下一个参数对象中;禁止赋值标志'*'可以指明要跳过的输入。下面是使用输入转换区分符的一些例子。
对于输入“Hello, world” (注意,在逗号和单词world之间有一个空格),用%10c读将产生“Hello, wor”,它只读入10个字符,结尾没有空字符;但用%10s读却产生“Hello,”,它停止在第一个空格字符处并在尾部添加空字符。
如下代码:
int i, n; float x; char name[50];
n = scanf ("%4d%f%80s", &i, &x, name);
对于输入“25 54.32E-1 thompson”,将导致25赋给变量i(因为25之后有一空格,尽管域宽为4),5.432赋给变量x,而name中将包含字符串“thompson”,并且变量n将得到值3。
如下代码:
int i,n; float x; char name[50];
(void) scanf ("%2d%f%*d %8[0-9]%n", &i, &x, name,&n);
对于输入“56789 0123 45a72”,将赋值56给i,789.0给x,跳过0123,并放置字符“450”于name,这对应于“%8[0-9]”转换,它要求读入8个数字,但是只读到第2个数字之后便遇到了字符'a',故实际只读入“45”。然后,读入的字符个数赋给变量n,其值为13。下一个待读入的字符是'a'。
注意,%n转换是确定文字匹配成功与否的唯一手段。如果%n位于匹配失败点之后则不会有值存入其内,因为scanf()在处理%n之前就已返回。如果在调用scanf()之前先存储–1至%n对应的那个参数,则在调用scanf()之后若其值仍为–1就表明到达%n之前已遇到错误。
如下代码:
double dx1,dx2;
(void)scanf("%f%lf", &dx1,&dx2);
printf("dx1=%12.10le,dx2=%12.10le",dx1,dx2);
对于输入值“567890123456 567890123456”,将产生输出:
dx1=8.2386878167e+91,dx2=5.6789012346e+11
结果显示读入至变量dx1中的值不对,dx1的值本应与dx2的相同。错误原因是dx1对应的转换区分符没有指定'l'标志。当参数类型长度与转换的默认类型长度不一致时,一定要注意指明长度标志。
格式输入函数的使用不像格式输出函数那么频繁,部分原因是其用法十分不灵活,正确地使用它匹配输入需要特别仔细,稍不留意便可能出现错误。另一个原因是它难以从匹配错误中恢复。
例2-9 计算机程序常常会要做十–二转换或二–十转换,借用格式输入输出可以很方便地做到这一点。程序2-9是利用格式输入输出实现这种转换的例子。该程序根据读入的圆半径计算圆的面积,计算结果以字符形式存储在数组buf中。