当前位置:   article > 正文

C语言K&R圣经笔记 7.3变长参数列表 7.4标准输入-scanf

C语言K&R圣经笔记 7.3变长参数列表 7.4标准输入-scanf

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 的基础:

  1. #include <stdarg.h>
  2. /* minprintf:带可变参数列表的最简版本printf */
  3. void minprintf(char *fmt, ...)
  4. {
  5.     va_list ap;    /* 依次指向每个无名参数的指针 */
  6.     char *p, *sval;
  7.     int ival;
  8.     double dval;
  9.     va_start(ap, fmt);    /* 使ap指向第一个无名参数 */
  10.     for (p = fmt; *p; p++) {
  11.         if (*p != '%') {
  12.             putchar(*p);
  13.             continue;
  14.         }
  15.         switch(*++p) {
  16.         case 'd':
  17.             ival = va_arg(ap, int);
  18.             printf("%d", ival);
  19.             breal;
  20.         case 'f':
  21.             dval = va_arg(ap, double);
  22.             printf("%f", dval);
  23.             break;
  24.         case 's':
  25.             for (sval = va_arg(ap, char *); sval; sval++)
  26.                 putchar(*sval);
  27.             break;
  28.         default:
  29.             putchar(*p);
  30.         }
  31.     }
  32.     va_end(ap);    /* 完成后清理 */
  33. }

练习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。

表7-2 scanf的基本转换

字符输入数据参数类型
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 *。符号位、小数点和幂 均是可选的。
%字符本身不会进行赋值。

转换字符d,i,o,u 和 x 前面,可以加上 h 来表明参数列表中出现的指针是指向 short 而不是 int ,或者加上字母 l 来表明参数列表中指针指向的是 long。与之类似,转换字符 e,f 和 g 前面可以加上字母 l 来表明参数列表出现中的指针指向 double 而不是 float 。

把第四章的简易计算器改写为用 scanf 来做输入转换,作为第一个例子:

  1. #include <stdio.h>
  2. main()    /* 简易计算器 */
  3. {
  4.     double sum, v;
  5.     sum = 0;
  6.     while (scanf(%lf, &v) == 1)
  7.         printf("\t%.2f\n", sum += v);
  8.     return 0;
  9. }


假定我们想要读取包含如下格式日期的行:

    25 Dec 1988

则scanf 语句为:

  1. int day, year;
  2. char monthname[20];
  3. scanf("%d %s %d", &day, monthname, &year);

monthname 不需要加 & ,因为数组名称就是指针。

字面量的字符可以在 scanf 的格式化字符串中出现;它们必须匹配输入中相同的字符。因此我们可以使用下面的 scanf 语句来读格式为 mm/dd/yy 的日期:

  1. int day, month, year;
  2. scanf("%d/%d/%d", &month, &day, &year);

如果 scanf 格式化字符串中有空白字符,则对应输入位置的空白字符会被跳过。不仅如此,它在寻找输入值的时候,会跳过空白字符(空格,制表符,换行等)。为了读入格式不固定的输入,最好的方法通常是每次读一行,然后使用 sscanf 将它各部分提取出来。例如,假定我们想要读取可能包含上述两种格式日期的文本行,则可以写

  1. while (getline(line, sizeof(line) > 0) {
  2.     if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3)
  3.         printf("valid: %s\n", line);    /* 格式为 25 Dec 1988 */
  4.     else if (sscanf(line, "%d/%d/%d", &month, &day, &year) == 3)
  5.         printf("valid: %s\n", line);    /* 格式为 mm/dd/yy */
  6.     else
  7.         printf("invalid: %s\n", line);    /* 非法格式 */
  8. }

 scanf 可以与其他输入函数混着调用。在调用 scanf 后调用任何输入函数时,都会从上次 scanf 未读的第一个字符开始读。

最后给个警告:scanf 和 sscanf 的参数必须是指针。目前最常见的错误是写成

scanf("%d", n);

正确的应该是

scanf("%d", &n);

这个错误在编译期间通常不能检测出来。【现代编译器一般都会检测出来】

练习7-4、仿照上一节的 minprintf,写个私有版本的 scanf。
练习7-5、重写第四章的后缀计算器,使用 scanf 和/或 sscanf 做输入和数字转换。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/90844
推荐阅读
相关标签
  

闽ICP备14008679号