结合C语言缓冲区谈scanf函数,那些奇怪的行为其实都有章可循

上节讲到,scanf() 是从标准输入设备(键盘)读取数据,带有行缓冲区的,这让 scanf() 具有了一些独特的“性格”,例如,

  • 可以连续输入、可以输入多余的数据等
  • 反过来,scanf() 也出现了一些奇怪的行为,例如,有时候两份数据之间有空格会读取失败,而有时候两份数据之间又必须有空格。

scanf() 的这些特性都是有章可循的,其根源就是行缓冲区

当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据

  • 如果没有,就等待用户输入用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符\n,输入结束,scanf() 再从缓冲区中读取数据,赋值给变量
  • 如果有数据,那就看是否符合控制字符串的规则
    • 如果能够匹配整个控制字符串,那最好了,直接从缓冲区中读取就可以了,就不用等待用户输入了
    • 如果缓冲区中剩余的所有数据只能匹配前半部分控制字符串,那就等待用户输入剩下的数据
    • 如果不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等
      • 如果这种尝试成功(可以忽略一些空白符),那么再重复以上的匹配过程
      • 如果这种尝试失败(不能忽略空白符),那么只有一种结果,就是读取失败。
你看,scanf() 并不是直接让用户从键盘输入数据,而是先检查缓冲区,处理缓冲区中的数据。对于初学者来说,scanf() 检查缓冲区的规则也许有点复杂,下面我们通过几个典型的例子来“细嚼慢咽”。

【实例1】scanf() 连续输入

#include <stdio.h>
int main()
{
int a, b, c;
scanf("%d", &a);
scanf("%d", &b);
scanf("%d", &c);
printf("a=%d, b=%d, c=%d\n", a, b, c);
return 0;
}

运行结果:

100 200 300↙
a=100, b=200, c=300
  • 程序执行到第一个 scanf(),由于缓冲区中没有数据,所以会等待用户输入。从键盘输入100 200 300后按下回车键,输入就结束了,scanf() 开始从缓冲区中读取数据
  • 第一个 scanf() 的控制字符串是"%d"会匹配到第一个整数,也就是 100,于是将 100 赋值给变量 a,并将内部的位置标记移动到 100 以后,此时缓冲区中剩下 200 300↙注意,换行符也是一个字符,也会进入缓冲区
位置标记是什么?系统内部有一个专门用来记录 scanf() 读取到哪个位置的标记,随着 scanf() 的读取,该标记会向后移动,下一个 scanf() 就从这个新的位置开始读取
  • 第二个 scanf() 的控制字符串也是"%d",需要读取一个整数,而此时缓冲区中的内容是 200 300↙开头是一个空格,并不是一个有效的数字,不符合控制字符串的规则。空格是一个空白符,此处是可以忽略的,于是 scanf() 忽略空格后再继续匹配,就得到了数字 200,终于匹配成功了
  • 到了第三个 scanf(),缓冲区中剩下 300↙,同样会忽略开头的空格,匹配到数字 300。

最终,三个 scanf() 都匹配成功了,缓冲区中只留下了。嗯,那就留着吧,已经没用了,等程序运行结束了,会释放缓冲区内存,一切数据都灰飞烟灭了

【实例2】scanf() 读取失败

#include <stdio.h>
int main()
{
int a, b=999;
char str[30];
printf("b=%d\n", b);
scanf("%d", &a);
scanf("%d", &b);
scanf("%s", str);
printf("a=%d, b=%d, str=%s\n", a, b, str);
return 0;
}

运行结果:

b=999
100 http://www.baidu.com↙
a=100, b=999, str=http://www.baidu.com
  • 程序执行到第一个 scanf() 时等待用户输入,从键盘输入100 http://www.baidu.com按下回车键,scanf() 匹配到 100,赋值给变量a,同时将内部的位置指针移动到 100 后面
  • 到了第二个 scanf(),缓冲区中有数据,会直接读取。此时缓冲区中的内容为 http://www.baidu.com↙即使忽略开头的空格也不是 scanf() 想要的整数,所以匹配失败了,不会给变量 b 赋值,b 的值保持不变,这就是两次输出变量 b 的值相同的原因
  • 匹配失败意味着不会移动内部的位置指针,此时缓冲区中的内容仍然是http://www.baidu.com↙。执行到第三个 scanf() 时,它想要一个字符串,这不是正好捡漏吗,把http://www.baidu.com赋值给 str 就好了。
注意,scanf()、gets() 在读取字符串时会忽略换行符,不会把换行符作为字符串的内容

【实例3】不能忽略空白符的情形

#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}

输入示例:

a=99↙
a=99, b=2

输入a=99,按下回车键,程序竟然运行结束了,只有第一个 scanf() 成功读取了数据,第二个 scanf() 仿佛没有执行一样,根本没有给用户任何机会去输入数据。这是为什么呢?

第一个 scanf() 执行完后,将 99 赋值给了 a,缓冲区中只剩下一个换行符\n到了第二个 scanf(),发现缓冲区中有内容,但是又不符合控制字符串的格式,于是尝试忽略这个空白符。注意,这个时候的空白符是不能忽略的,所以就没有办法了,只能读取失败了

实测发现,空白符在大部分情况下都可以忽略,前面的两个例子就是这样。但是当控制字符串不是以格式控制符 %d、%c、%f 等开头时,空白符就不能忽略了,它会参与匹配过程,如果匹配失败,就意味着 scanf() 读取失败了

本例中,第二个 scanf() 的开头并不是格式控制符,而是写死的b字符,所以不会忽略换行符,而换行符和b又不匹配,怎么办呢?没办法,只能读取失败了

如果我们换一种输入方式呢?

a=99 b=200↙
a=99, b=2

你看,第二个 scanf() 也读取失败了。执行到第二个 scanf() 时,缓冲区中剩下 b=200↙,开头的空格依然不能忽略,然而又和控制字符串不匹配,所以只能读取失败了

两种输入方式都不行,究竟该如何输入呢?很简单,不要让两份数据之间有空白符,只能像下面一样输入:

a=99b=200↙
a=99, b=200

这样 a 和 b 都能够正确读取了。注意,a=99b=200中间是没有任何空格的。

最后,我们再修改一下上面的代码,将第二个 scanf() 改成下面的样子:

scanf("%d", &b);

运行结果:

a=100↙
200↙
a=100, b=200

此时,第二个 scanf() 的控制字符串以%d开头,就可以忽略换行符了。忽略换行符以后,缓冲区中就没有内容了,所以会等待用户输入。输入 200 以后,第二个 scanf() 就匹配成功了,将 200 赋值给变量 b。

那么,为什么只有当控制字符串以格式控制符开头时,才会忽略换行符呢?我也觉得这个特性很奇怪,目前还未想明白,也没有资料可查,请读者先记住这个结论。

posted on 2022-05-11 23:57  朴素贝叶斯  阅读(973)  评论(0编辑  收藏  举报

导航