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

#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 做输入和数字转换。

相关推荐
我是哈哈hh几秒前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
Dontla33 分钟前
Rust泛型系统类型推导原理(Rust类型推导、泛型类型推导、泛型推导)为什么在某些情况必须手动添加泛型特征约束?(泛型trait约束)
开发语言·算法·rust
杜若南星1 小时前
保研考研机试攻略(满分篇):第二章——满分之路上(1)
数据结构·c++·经验分享·笔记·考研·算法·贪心算法
Neophyte06081 小时前
C++算法练习-day40——617.合并二叉树
开发语言·c++·算法
慕容复之巅1 小时前
基于MATLAB的条形码的识别图像处理报告
开发语言·图像处理·matlab
zqzgng2 小时前
Python 数据可视化pilot
开发语言·python·信息可视化
写bug的小屁孩2 小时前
websocket初始化
服务器·开发语言·网络·c++·websocket·网络协议·qt creator
Dr_eamboat2 小时前
【Java】枚举类映射
java·开发语言·python
代码小鑫2 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
五味香2 小时前
Linux学习,ip 命令
linux·服务器·c语言·开发语言·git·学习·tcp/ip