7.3 变长参数列表
本节包含了一个最小版本的 printf 的实现,用以说明如何写出以可移植方式处理变长参数列表的函数。由于我们主要对参数处理感兴趣,故 minprintf 只做格式化字符串和参数的处理,而格式转换调用真正的 printf 来处理。
printf 的正确声明为
int printf(char *fmt, ...)
其中的 ... 意味着这些参数的数量和类型可以变化。声明 ... 只能出现在参数列表的末尾。我们的 minprintf 声明为 void,是因为它不像 printf 那样返回字符个数。
void minprintf(char *fmt, ...)
困难之处在于,如何让 minprintf 在参数列表连名字都没有的情况下遍历这个列表。标准库头文件 <stdarg.h> 包含了一系列如何遍历参数列表的宏定义。这个头文件的实现在不同的机器上是不同的,但它给出的接口是统一的。
va_list 类型用来声明一个依次指向每个参数的变量;在 minprintf中,这个变量称为 ap,代表"参数指针"(argument pointer)。宏 va_start 可以初始化 ap,使其指向第一个无名参数。在 ap 被使用之前,这个宏必须被调用一次。必须至少存在一个有名字的参数; va_start 使用最后一个有名字的参数来启动。
对 va_arg 的每次调用,都将返回一个参数并让 ap 步进到下一个参数。va_arg 使用一个类型名来确定返回的是什么类型,以及要步进多少字节。最后由 va_end 进行所有必需的清理操作。va_end 必须在函数返回之前调用。
上述特性构成了我们简化版 minprintf 的基础:
#include <stdarg.h>
/* minprintf:带可变参数列表的最简版本printf */
void minprintf(char *fmt, ...)
{
va_list ap; /* 依次指向每个无名参数的指针 */
char *p, *sval;
int ival;
double dval;
va_start(ap, fmt); /* 使ap指向第一个无名参数 */
for (p = fmt; *p; p++) {
if (*p != '%') {
putchar(*p);
continue;
}
switch(*++p) {
case 'd':
ival = va_arg(ap, int);
printf("%d", ival);
breal;
case 'f':
dval = va_arg(ap, double);
printf("%f", dval);
break;
case 's':
for (sval = va_arg(ap, char *); sval; sval++)
putchar(*sval);
break;
default:
putchar(*p);
}
}
va_end(ap); /* 完成后清理 */
}
练习7-3、修改 minprintf 以支持 printf 的其他功能。
7.4 标准输入-scanf
scanf 是类似 printf 的输入函数,也提供了很多同样的转换功能,不过方向与 printf 相反。
int scanf(char *format, ...)
scanf 从标准输入中读入字符,根据 format 中的规格对其进行解析,然后将结果保存到其他参数中。格式化参数会在后面描述;而其他参数,每个都必须是指针,指出对应的输入在转换后要储存到哪里。和 printf 一样,本节是对 scanf 最有用特性的总结,而不是详尽的列表。
scanf 在耗尽格式化字符串后停止,或是某个输入无法匹配到控制规格时停止。它的函数返回值是成功匹配并赋值的输入项的个数。这个值可以用来确定得到了多少输入项。遇到文件结束时,函数返回 EOF;注意这不同于返回 0,返回 0 意味着下一个输入字符无法匹配格式化字符串的第一个规格。如果上一个 scanf 没有把输入都处理完,则下一次调用 scanf 时,会紧接着上次已转换的最后一个字符之后,继续搜索匹配。
还有一个函数 sscanf ,是从字符串而不是标准输入中读取:
int sscanf(char *string, char *format, arg1, arg2, ...)
它根据 format 中的规格对 string 进行扫描,然后通过 arg1, arg2 等参数保存结果值。这些参数必须都是指针。
格式化字符串通常包含用于控制输入转换的转换规格。格式化字符串可以包含:
- 空格或制表符,不会被忽略。如果格式化字符串中有空白字符,则对应输入位置的空白字符会被跳过。
- 普通字符(非%),预计与输入流中的下一个非空白字符匹配。
- 转换规格,包含字符 %,用于抑制赋值的字符 *(可选),用于指定最大域宽度的数字(可选),用于指定目标宽度的字母 h,l 或 L(均可选),以及一个转换字符。
转换规格指明了下一个域该如何转换。转换结果通常会放到由对应参数指针所指向的变量中。然而,如果有 * 字符指示了抑制赋值,则输入域会被跳过。输入域被定义为一个非空白字符串;它的范围是到下一个空白字符,但若有指定域宽度,则到宽度耗尽为止。这隐含说明 scanf 在找输入时会读过行边界,因为换行符也是空白字符。(空白字符包括空格,制表符,换行,回车,垂直制表符和换页符。)
转换字符指示如何解析输入域。对应的参数必须是指针,这是由 C 语言的"值传递"语义要求的。转换字符见表7-2。
字符 | 输入数据 | 参数类型 |
---|---|---|
d | 十进制整数 | int * |
i | 整数 | int * 。整数可以是八进制(带前导0)或十六进制(带前导0x或0X) |
o | 无符号八进制整数 | unsigned int *。前导 0 可带可不带 |
u | 无符号十进制整数 | unsigned int * |
x | 无符号十六进制整数 | unsigned int *。前导 0x 或 0X 可带可不带 |
c | 字符 | char *。下一个输入的字符位于指定的位置(默认为1)。这里不会跳过空白字符;要读下一个非空白字符,应使用 %1s。 |
s | 字符串 | char *。不带双引号。指向长度足够放下该字符串的数组,且会添加末尾的 '\0'。 |
e,f,g | 浮点数 | float *。符号位、小数点和幂 均是可选的。 |
% | %字符本身 | 不会进行赋值。 |
[表7-2 scanf的基本转换] |
转换字符d,i,o,u 和 x 前面,可以加上 h 来表明参数列表中出现的指针是指向 short 而不是 int ,或者加上字母 l 来表明参数列表中指针指向的是 long。与之类似,转换字符 e,f 和 g 前面可以加上字母 l 来表明参数列表出现中的指针指向 double 而不是 float 。
把第四章的简易计算器改写为用 scanf 来做输入转换,作为第一个例子:
#include <stdio.h>
main() /* 简易计算器 */
{
double sum, v;
sum = 0;
while (scanf(%lf, &v) == 1)
printf("\t%.2f\n", sum += v);
return 0;
}
假定我们想要读取包含如下格式日期的行:
25 Dec 1988
则scanf 语句为:
int day, year;
char monthname[20];
scanf("%d %s %d", &day, monthname, &year);
monthname 不需要加 & ,因为数组名称就是指针。
字面量的字符可以在 scanf 的格式化字符串中出现;它们必须匹配输入中相同的字符。因此我们可以使用下面的 scanf 语句来读格式为 mm/dd/yy 的日期:
int day, month, year;
scanf("%d/%d/%d", &month, &day, &year);
如果 scanf 格式化字符串中有空白字符,则对应输入位置的空白字符会被跳过。不仅如此,它在寻找输入值的时候,会跳过空白字符(空格,制表符,换行等)。为了读入格式不固定的输入,最好的方法通常是每次读一行,然后使用 sscanf 将它各部分提取出来。例如,假定我们想要读取可能包含上述两种格式日期的文本行,则可以写
while (getline(line, sizeof(line) > 0) {
if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3)
printf("valid: %s\n", line); /* 格式为 25 Dec 1988 */
else if (sscanf(line, "%d/%d/%d", &month, &day, &year) == 3)
printf("valid: %s\n", line); /* 格式为 mm/dd/yy */
else
printf("invalid: %s\n", line); /* 非法格式 */
}
scanf 可以与其他输入函数混着调用。在调用 scanf 后调用任何输入函数时,都会从上次 scanf 未读的第一个字符开始读。
最后给个警告:scanf 和 sscanf 的参数必须是指针。目前最常见的错误是写成
scanf("%d", n);
正确的应该是
scanf("%d", &n);
这个错误在编译期间通常不能检测出来。【现代编译器一般都会检测出来】
练习7-4、仿照上一节的 minprintf,写个私有版本的 scanf。
练习7-5、重写第四章的后缀计算器,使用 scanf 和/或 sscanf 做输入和数字转换。