输入缓冲区
所有从键盘输入的数据,不管是字符还是数字,都是先存储在内存的缓冲区中,叫作“键盘输入缓冲区”,简称“输入缓冲区”或“输入流”。我们先来看一个程序:
# include <stdio.h> int main(void) { int a, b, c; scanf("%d", &a); printf("a = %d/n", a); scanf("%d", &b); printf("b = %d/n", b); scanf("%d", &c); printf("c = %d/n", c); return 0; }
输出结果是:
1
a = 1
2
b = 2
3
c = 3
或者:
1 2 3
a = 1
b = 2
c = 3
从输出结果可以看出,不管是一个一个地输入:1(回车)2(回车)3(回车);还是三个数字一次性输入:1(空格)2(空格)3(回车),这两种输入方法的结果都是一样的。原因是从键盘输入的数据都会被依次存入缓冲区,不管是数字还是字符都会被当成数据存进去。但只有按回车,scanf 才会进去取数据,所取数据的个数取决于 scanf 中“输入参数”的个数。因此不在于怎么输入,可以存一个取一个,也可以一次性全存入进去,然后一个个取。
那么一次性全存进去,scanf 是如何取数据的呢?这就好比开闸放水,我们将水一次性全部放到闸里以后,开一次闸就放掉一点,开一次闸就放掉一点,直到放光了为止。开闸的动作就相当于调用一次 scanf,开闸的开关就是回车;每开一次闸放掉多少水,取决于 scanf 中“输入参数”的个数。
所以,输入的数据放在输入缓冲区中,先输入的排在最前面,后输入的依次往后排。如果 scanf 中“输入参数”的个数只有一个,那么我们调用一次 scanf 就把缓冲区中离出口最近的一个数据输出给 scanf,也就是把排在最前面的一个数据输出给 scanf。输出后,缓冲区中就没有这个数据了。
如果 scanf 中“输入参数”的个数为 n,那么就从排在最前面的开始,依次往后取 n 个数据输出给 scanf。没取完的仍旧放在缓冲区中,直到取用完毕为止。如果缓冲区中的数据全被取完了,但还有 scanf 要取数据,那就要再从键盘输入数据。
%d和%c读取缓冲区的差别
需要注意的是,对于 %d,在缓冲区中,空格、回车、Tab 键都只是分隔符,不会被 scanf 当成数据取用。%d 遇到它们就跳过,取下一个数据。但是如果是 %c,那么空格、回车、Tab 键都会被当成数据输出给 scanf 取用,例如下面这个程序:
# include <stdio.h> int main(void) { int a, c; char b; scanf("%d%c%d", &a, &b, &c); printf("a = %d, b = %c, c = %d/n", a, b, c); return 0; }
输出结果是:
1 5 6
a = 1, b = , c = 5
在此程序中,原本希望的是将数字 1 赋给变量 a,将字符 '5' 赋给变量 b,将数字 6 赋给变量 c。但从输出结果可以看出,按一下回车,scanf 开始到缓冲区中取数据,因为“输入参数”有三个,所以 scanf 从缓冲区中取三个数据。数字 1 赋给变量 a,而因为变量 b 是 %c,所以前三种情况分别将空格、回车和 Tab 键赋给变量 b,然后数字 5 赋给变量 c,而数字 6 仍然在缓冲区中,等待下一个 scanf 来取。这样的话就会有一个问题,我们看下面这个程序:
# include <stdio.h> int main(void) { int a; char i; while (1) { printf("请输入一个数字:"); scanf("%d", &a); printf("a = %d/n", a); printf("您想继续吗(Y/N):"); scanf("%c", &i); if (('Y' == i) || ('y' == i)) { ; } else { break; // 跳出本层循环体 } } return 0; }
输出结果是:
请输入一个数字:10
a = 10您想继续吗(Y/N):
当我们输入“10”之后希望系统问:“您想继续吗(Y/N):”,若为“Y”就再重新输入一个值,然后输出,否则就跳出本循环体。但是执行的时候我们发现,刚按完“10”然后回车,直接就结束了,都不给我们输入“Y”和“N”的机会,这是为什么?
因为输入“10”然后回车,“10”赋给了a,但是回车遗留在了缓冲区,所以等下面又遇到“scanf("%c",&i);”的时候就直接把字符 '/n' 赋给变量 i 了(注意,按回车不是把回车符 '/r' 存到缓冲区,而是把换行符 '/n' 存进去了,因为按回车确实就是换行)。字符 '/n' 明显不等于字符 'Y',所以直接 break 跳出本层循环体。
那么该怎么办呢?方法有两个
- 既然不想将字符'/n' 赋给变量 i,那么就先定义一个字符变量 ch,然后用 scanf 将字符 '/n' 取出来给变量 ch,这样就有机会输入“Y”或者“N”了;
- 直接清空输入缓冲区。
用scanf吸收回车
# include <stdio.h> int main(void) { int a; char i; char ch; while (1) { printf("请输入一个数字:"); scanf("%d", &a); printf("a = %d/n", a); printf("您想继续吗(Y/N):"); scanf("%c", &ch); //用scanf吸收回车 scanf("%c", &i); if (('Y' == i) || ('y' == i)) { ; } else { break; // 跳出本层循环体 } } return 0; }
输出结果是:
请输入一个数字:10
a = 10您想继续吗, Y想, N不想:Y
请输入一个数字:5
a = 5您想继续吗, Y想, N不想:Y
请输入一个数字:333
a = 333您想继续吗, Y想, N不想:N
这时有人说,如果缓冲区前面排了三个字符'/x20'(空格),我都不需要,想先把它们取出来,那是不是要先定义三个变量呢?当然不是!存储不需要的垃圾字符只需要一个变量即可,因为它们都是垃圾,所以直接覆盖就行了。取一个后,再取一个就把第一个覆盖,再取一个就再覆盖。
但是在实际编程中,一般不会用 scanf 吸收回车,也不会用 scanf 给一个字符变量赋值,因为有更简单的方法,就是用 getchar()。getchar() 是专门从缓冲区读取一个字符的函数。它是“吸收回车专业户”,简单、方便、好用。
getchar()
该函数的原型为:
# include <stdio.h>
int getchar(void);
功能是从缓冲区中读取一个字符。这个函数非常简单,连参数都没有,非常好用。下面用 scanf 给字符变量赋值和吸收回车的程序用 getchar() 修改一下:
# include <stdio.h> int main(void) { int a; char ch; while (1) { printf("请输入一个数字:"); scanf("%d", &a); printf("a = %d/n", a); printf("您想继续吗(Y/N):"); getchar(); /*用getchar吸收回车, 简单、方便、好用, 都不需要定义变量用来存储获取的回车符*/ ch = getchar(); //用getchar从缓冲区中读取一个字符赋给字符变量ch if (('Y' == ch) || ('y' == ch)) { ; } else { break; // 跳出本层循环体 } } return 0; }
输出结果是:
请输入一个数字:10
a = 10您想继续吗(Y/N):y
请输入一个数字:5
a = 5您想继续吗(Y/N):y
请输入一个数字:333
a = 333您想继续吗(Y/N):n
在程序中,“ch=getchar();”这句之前我们先用 getchar() 清空缓冲区,然后重新从键盘输入一个字符。同样,必须按回车 getchar() 才会进去取这个字符。这时候需要注意的是,同 scanf 一样,按的这个回车也会被遗留在缓冲区中,大家要注意。
这时有人会说,如果前面有多个 scanf 给 int 型变量赋值,那么每个 scanf 都会遗留一个回车,那这时是不是有几个 scanf 就要用几个 getchar() 呢?
回答是“不需要”,仍然只需要一个 getchar()!前面说过,当 scanf 用 %d 取缓冲区数据的时候,如果遇到空格、回车或 Tab 键就跳过去。不仅如此,这些被跳过去的空白符都被释放了。所以假如前面有三个 scanf 给 int 型变量赋值,那么第一个 scanf 输入回车后把回车遗留在了缓冲区,而第二个 scanf 取值时会越过第一个 scanf 遗留在缓冲区中的回车,那么这个回车就会从缓冲区中释放。但第二个 scanf 取完值后也在缓冲区中留下了一个回车,而当第三个 scanf 到缓冲区中取值时会跳过第二个 scanf 遗留的回车,这个回车同样也会从缓冲区中释放,所以归根结底最后缓冲区中只有一个回车,也就是说,缓冲区中永远不可能遗留多个回车。
下面来写一个程序验证一下:
# include <stdio.h> int main(void) { int a, b, c, d; char ch; printf("请输入第一个数:"); scanf("%d", &a); printf("请输入第二个数:"); scanf("%d", &b); printf("请输入第三个数:"); scanf("%d", &c); printf("您想继续吗(Y/N):"); getchar(); //只需要用一个getchar吸收回车 ch = getchar(); //用getchar获取一个字符赋给ch if (('Y' == ch) || ('y' == ch)) { printf("请输入密码:"); scanf("%d", &d); printf("恭喜成功获得密码%d/n", d); } return 0; }
输出结果是:
请输入第一个数:1
请输入第二个数:2
请输入第三个数:3
您想继续吗(Y/N):y
请输入密码:5678
恭喜成功获得密码5678
由此我们知道,当用 %d 获取输入流中的数据的时候,如果遇到字符(空格、回车、Tab 除外),则直接从输入流中退出来,什么都不取。但如果是用 %c 获取,那么任何数据都会被当作一个字符。所以如果你要从输入流中取一个字符,但在之前使用过 scanf,那么此时就必须要先用 getchar() 吸收回车。否则取到的将不是你想要的字符,而是 scanf 遗留在输入流中的回车。如果你要从输入流中取的不是字符,那就不需要用 getchar() 吸收回车了。
以上详细分析了什么时候需要吸收回车,什么时候不需要。但实际编程中,程序往往很长,我们很难预测到下一次到缓冲区中取数据的是 %d 还是 %c 或者是 gets()、fgets()。所以为了避免忘记吸收回车或耗费精力考虑回车的问题,习惯上 scanf 后面都加上 getchar()。
fflush(stdin)
前面介绍了使用 getchar() 吸收回车的方法,除此之外还有一个更强大、更直接的方法,就是直接将输入缓冲区全部清空。
清空缓冲区只需加一句 fflush(stdin) 即可。fflush 是包含在文件 stdio.h 中的函数。stdin 是“标准输入”的意思。std 即 standard(标准),in 即 input(输入),合起来就是标准输入。fflush(stdin) 的功能是:清空输入缓冲区。下面将前面的一个程序修改一下:
# include <stdio.h> int main(void) { int a; char i; while (1) { printf("请输入一个数字:"); scanf("%d", &a); //因为读取的是数字, 所以不需要清空缓冲区 printf("a = %d/n", a); printf("您想继续吗, Y想, N不想:"); fflush(stdin); scanf("%c", &i); if ('Y'==i || 'y'==i) { ; } else { break; // 跳出本层循环体 } } return 0; }
输出结果是:
请输入一个数字:10safa
a = 10
您想继续吗, Y想, N不想:Y
请输入一个数字:58jlkj/*&**^
a = 58
您想继续吗, Y想, N不想:N
fflush 一般用于清除用户前面遗留的垃圾数据,提高代码的健壮性。因为如果是自己编程的话,一般都会按要求输入。但对于用户而言,难免会有一些误操作,多输入了一些其他没有用的字符,如果程序中不对此进行处理的话可能会导致程序瘫痪。所以编程时一定要考虑到各种情况,提高代码的健壮性和容错性。使用 fflush 就可以将用户输入的垃圾数据全部清除。
但是 fflush 有一个问题,就是可移植性。并不是所有的编译器都支持 fflush,比如 gcc 就不支持。那么此时怎么办?还是用 getchar()。
getchar()的高级用法
while (getchar() != '/n');
这种用法其实在前面也使用过,它可以完全代替 fflush(stdion) 来清空缓冲区。不管用户输入多少个没用的字符,他最后都得按回车,而且只能按一次。只要他按回车那么回车之前的字符就都会被 getchar() 取出来。只要 getchar() 取出来的不是回车 ('/n') 那么就会一直取,直到将用户输入的垃圾字符全部取完为止。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/21487.html