两次调用scanf函数可能存在的问题
1.问题描述
scanf()函数可以从stdin中读取数据并写入指定的内存地址处。它的工作原理大致如下:
- 程序运行至scanf()处时,scanf()检查stdin是否有数据。
- 如果stdin中有数据则按照格式串的格式读取数据并写入给定的内存地址处。如果stdin中没有数据,则阻塞等待用户输入。
- 将用户的第一个非空白符输入(即不算空格和回车)与格式串中的第一个格式做匹配,只要输入的格式与scanf()格式串中规定的格式不一致,则scanf()函数立马结束。
- 如果在非空白符后输入空白符(如空格、回车),则表明接下来的输入与格式串的下一个格式做匹配。
- 如果按照格式串的格式,顺序接收到了与之匹配的输入项,则scanf()函数结束。
空白符就是空白字符的意思,通俗理解就是不会显示出来的字符,类似空格符、回车换行符、制表符等,从视觉效果来看,就是一个空白区域。
发生问题的地方就在于,第二次调用scanf()时,它的格式串中第一个格式如果是字符格式,即scanf("%c ...", &char, ...);
,将可能导致第二次scanf()调用失败,即如果你在程序中调用了两次scanf(),在程序运行时,它只正确读取了你第一个输入值,然后程序就结束了。如果此时程序有打印,会发现只打印了第一个输入的值,没有打印第二个输入值。具体来看下面的程序演示。
2.程序演示
在程序中调用两次scanf(),第一次scanf()输入格式为一个整数,第二次scanf()输入格式为一个字符。最后打印两次输入的值。
#include <stdio.h>
int main()
{
int a;
char c;
puts("Please input value:");
scanf("%d", &a);
scanf("%c", &c);
printf("%d %c\n", a, c);
return 0;
}
然后运行程序。这里我们进行两次不同的输入
(1)第一次输入:5 Space c
space表示按下空格键。也就是依次输入5,空格,字符c。
运行结果:
Please input value:
5 c
5
最后的打印只打印出了整数5,字符c没有打印出来。
接下来我们进行第二次输入:5 Enter
第二次的输入把空格换成回车键Enter,在依次输入5和回车后,程序就结束了。
运行结果:
Please input value:
5
5
第二次的运行结果多打印了一个空行。
现在我们对两次的运行结果进行分析。先从输入结果来看,也就是两次运行结果的第2行。scanf()在用户输入数据后,会在屏幕上回显输入值,所以当我们键入5后,屏幕回显了5。接下来就是两次输入结果发生差异的地方。在第一次输入中,键入5后是按下了空格,屏幕在回显了空格后还在等待第二次输入,也就是还可以输入字符c。但是第二次输入中,键入5后按下了回车,程序不再等待用户输入,直接运行结束。
然后再观察两次输入的打印结果,也就是两次运行结果的第3行。会明显的发现,虽然两次结果都只打印了整型值,但是第二次的打印结果还多打印了一个空行。这是为啥?接下来进行原理分析。
3.原理分析
第一次调用scanf("%d", …)时,stdin中没有数据,所以scanf()阻塞等待。当用户键入5后,在5后面又继续输入了空白符(空格或回车),scanf("%d", …)认为格式串中第一个格式对应的输入已经完成,5就是第一个输入的数据,然后读取5并与格式化字符串的第一个格式%d
进行匹配,5符合%d
所以scanf()将5写入到对应地址处,因为第一个scanf("%d", …)只需要接收一个参数,所以第一个scanf("%d", …)此时已经调用结束了。接下来轮到第二个scanf("%c", …)在stdin中读取数据。注意,在上次scanf("%d", …)结束后,stdin中是遗留了空白符(空格符或换行符)的,对,就是为了告诉scanf("%d", …)第一个格式输入结束而使用的空格(Space)或回车(Enter),所以此时stdin中是有数据的,而且是空格符或换行符的字符型数据。第二个scanf("%c", …)发现stdin中有数据,自然要直接进行读取,不再阻塞等待。因为它请求的输入格式是%c,stdin中遗留的空格或换行符恰好符合格式,所以第二次scanf()就把遗留的字符直接进行读取。
那为什么第二次scanf()读取到空格后还会回显下一次输入,而读取到换行则不会回显下一次的输入?
首先知道scanf("%c", …)与scanf(" %c", …)的不同,前者不会跳过空白字符,而后者会跳过所有空白字符(包括空格和回车换行)。后者后面再细说,现在只说前者。第一次scanf()结束后,空格遗留在stdin中,第二次scanf("%c")不会跳过空白符(空格或回车都不会放过,都被当作可读取的字符),所以会直接读取遗留的空格。另外,空格符还会让scanf()继续等待下一次输入(’\n’不会有这个效果,这里都是针对格式串为%c来说),且scanf("%c")与scanf(" %c")不同,scanf("%c")不是直到下一个非空字符才结束,而是直到下一个非空格字符才结束。也就是当你在空格符后按下回车后,它也会结束,而且此时屏幕没有回显程序就结束了(因为换行’\n’是空白符所以不会回显)。
依然是上面的程序,现在输入5 Space Enter,看结果:
Please input value:
5
5
此时的运行结果在空格后输入Enter,程序就结束了。
所以第二次scanf()读取到空格后会回显下一次输入,而读取到换行则不会回显下一次输入的原因是:空格和回车换行虽然都会被当作第一个字符参数读取,但是对于scanf("%c"),’\n’并不会让scanf()继续等待下一次输入,scanf()读取了’\n’就结束了。但是空格不同,scanf("%c")在读取了空格后,空格会让scanf()继续等待下一次输入,直到下一次的输入为非空格字符为止(上面的运行结果证明了这一点),注意是非空格而不是非空白字符,只有scanf(" %c")才会让scanf()等待下一次输入直到是非空白字符为止。
但是注意,如果第二个scanf()的参数不是字符,是
%d
,那么只要是空白符(无论空格或回车)都会让scanf()继续等待下一个输入。
另外,在输入数字5后再按空格与直接在输入格式后多加一个空格scanf("%d ", ...)
这种写法的效果是有区别的,前者scanf("%d", …)在读取到5后就结束了,而后者scanf("%d “, …)在读取数字后,会继续读取下一个,并丢弃所有空白,直到在输入中看到非空白字符为止,并且该非空白字符将保留为输入函数要读取的下一个值。也就是此时scanf(”%d ", …)还不会结束,直到接收到了一个非空白字符时才结束,这个非空白字符会留在stdin缓冲区中,留给下一个函数进行读取。
现在改动上面的程序,把第二次scanf("%c", …)改为scanf("%c ", …),并在其后加一个读取字符的getchar()函数,将遗留在stdin中的下一个字符读取出来。
#include <stdio.h>
int main()
{
int a;
char c;
char d;
puts("Please input value:");
scanf("%d", &a);
scanf("%c ", &c); /* 格式串后多加一个空格 */
d = getchar();/* 读取遗留在stdin缓冲区的字符 */
printf("%d %c %c\n", a, c, d);
return 0;
}
运行结果:依次输入5 Space 字符c
Please input value:
5 c
5 c
从这次的运行结果可以看出,第一次scanf("%d", …)读取了5后结束,5后的空格留在了stdin中。第二次scanf("%c “, …)从stdin中直接读取了空格,然后继续等待输入,直到遇到非空白字符,此处是字符c,然后第二次scanf(”%c ", …)结束,字符c遗留在了stdin缓冲区中。接着用getchar()可以将字符c读取出来,运行结果与分析一致。
4.解决方式
错误的解决方式:
在两次scanf()中间添加fflush(stdin)。
这种方式是一种未定义的行为,也就是C标准并没有定义这样做会产生什么样的结果。另外,在gcc中这样做是没有效果的,可能使用别的编译器会有效,不建议这种方式。
正确的解决方式:
(1)使用getchar()吸收掉遗留在stdin中多余的字符。
#include <stdio.h>
int main()
{
int a;
char c;
puts("Please input value:");
scanf("%d", &a);
getchar(); /* 使用getchar()吸收掉多余的字符 */
scanf("%c", &c);
printf("%d %c\n", a, c);
return 0;
}
(2)在第二次scanf()的格式串前面多加一个空格。
#include <stdio.h>
int main()
{
int a;
char c;
puts("Please input value:");
scanf("%d", &a);
scanf(" %c", &c); /* 在格式串%c前面多加一个空格 */
printf("%d %c\n", a, c);
return 0;
}
在%c
前面多加一个空格会让scanf()显式地跳过所有空白符(包括空格和回车),直到接收到一个非空白字符。这样就可以让遗留在stdin中的空格符被忽略掉,不会被当作第一个字符参数被读取。