如题,本文深入了解了下C程序的缓冲输入方面问题。
通常,系统使用行缓冲输入,这意味着输入的内容会在您按下回车键之时被传输给程序,按下回车键的同时还将传输一个编程时需要注意的换行字符。ANSIC把缓冲输入作为标准。
为了说明何谓缓冲输入,特举了一个简单的例子(别在意例子意义,意在说明何谓缓冲输入):
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h> int main (void) { char ch; while (getchar() != 'a') continue; ch = getchar(); putchar(ch); putchar('\n'); return 0; } |
输入下行:
Not alone!
会发现输出为字符l,这就是缓冲输入的体现。由于简单,就不详讲了。
缓冲输入通常给用户带来方便,他提供了在将输入发送至程序前对其进行编辑的机会,但在使用字符输入时这会给编程人员带来麻烦。其主要问题在于缓冲输入需要您按下回车键来提交您的输入。这一动作还传输一个程序必须处理的换行符。
尤其是scanf()函数和getchar()函数混用的时候。这是因为getchar()读取每个字符,包括空格、制表符和换行符;而scanf()在读取数字时则会跳过空格、制表符和换行符。为了说明它产生的问题,举例如下。该程序读取一个字符和两个数作为输入,然后使用由所输入的两个数字指定的行数和列数来打印该字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /* 程序单1 */ #include <stdio.h> void display (char cr, int lines, int width); int main(void) { int ch; /* 要打印的字符 */ int rows, cols; /* 行数和列数 */ printf ("Enter a character and two integers: \n"); while ((ch = getchar()) != '\n') { scanf ("%d %d", &rows, &cols); display (ch, rows, cols); printf ("Enter another character and two integers: \n"); printf ("Enter a newline to quit.\n"); } printf ("Bye.\n"); return 0; } void display(char cr, int lines, int width) { int row, col; for (row = 1; row <= lines; row++) { for (col = 1; col <=width; col++) putchar(cr); putchar('\n'); /* 结束本行,开始新的一行 */ } } |
书上也说这个程序是有这大问题的,我们也来看下问题在哪。
运行时输入c 2 3,程序如期打印2行c字符,每行3个。然后该程序提示输入第二组数据,并在您还没能做出响应之前就退出了!这就是紧跟在第一个输入行的3后面的那个换行符所导致的问题。scanf()函数将该换行符留在了输入队列中。而getchar()由于并不跳过换行符,所以在下一个循环时您输入其他内容之前,这一换行符由getchar()读出,然后将其赋值给ch,而这正是终止循环的条件。
书中也给出了解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /* 程序单2 */ #include <stdio.h> void display (char cr, int lines, int width); int main(void) { int ch; /* 要打印的字符 */ int rows, cols; /* 行数和列数 */ printf ("Enter a character and two integers: \n"); while ((ch = getchar()) != '\n') { scanf ("%d %d", &rows, &cols); display (ch, rows, cols); while (getchar() != '\n') //添加的语句 continue; printf ("Enter another character and two integers: \n"); printf ("Enter a newline to quit.\n"); } printf ("Bye.\n"); return 0; } void display(char cr, int lines, int width) { int row, col; for (row = 1; row <= lines; row++) { for (col = 1; col <=width; col++) putchar(cr); putchar('\n'); /* 结束本行,开始新的一行 */ } } |
看到这里,可能就有人说了,你这样照搬照抄有何意思。下面就来说下我自己当时的一些疑惑吧。
在程序单1时,我想了下输入为cr 2 3时结果会是怎样,当时真没想出来。之后输入运行了下,输出:
Enter another character and two integers:
Enter a newline to quit.
rrr
rrr
Enter another character and two integers:
Enter a newline to quit.
Bye.
Press any key to continue
当时看到这些时更加困惑了,为什么会这样,c呢?百思不得其解。所以我又在程序单2中输入cr 2 3,刚开始输出如下:
Enter another character and two integers:
Enter a newline to quit.
并要求继续输入。当时颇感无语,完全没懂。然后又慢慢输入:
1
2
3
结果:
111
111
Enter another character and two integers:
Enter a newline to quit.
这个对了!那么为什么前面输入cr 2 3时是那样输出呢。按了下换行程序结束后苦思冥想。
现在真是想通了,理解的透彻,我来说下吧:
在程序单1输入cr 2 3时,getchar()读取了输入队列中的c之后,scanf()无法读取r及其之后的字符,只能使用默认的值。而这时的rows和cols的值由于只是在前面声明并没有赋值,所以一般是负的大数。就假设这一次循环的输入为c -16653 -16652,带入display程序,当然是什么都没显示啦。然后输出了两行提示信息。而这时由于缓冲的输入队列中有值,不等您反应继续带入,即r 2 3,而这就是之后所输出的内容了。说到这里您可以把程序单1中的rows和cols声明时分别初始化为1和2,看下结果就知道了。
至于程序单2,要先知道添加的while语句的作用。它把scanf()输入后的所有字符,包括换行符都给剔除了。这样能让循环准备好读取即将输入的下一行开始的第一个字符。也就是说,您输入cr 2 3时,他先分别像程序单1中那样输入c -16653 -16652,把您输入的c之后的r 2 3给剔除了包括3之后的换行符,然后输出两行提示等待您的再次输入。
PS:好,写到这里也差不多了(其实全部手码的,信不信→_→实体书还是有麻烦的),今天感悟颇大,收获颇丰。一直以为C学的还好,今天拿起以前买的C Primer Plus随便翻了翻发现有好多不懂,才知道实在是想当然了,我会的只是C的语法规则而已。看了8.5(创建更友好用户界面)和8.6(输入确认),终于明白了我们大学生的编程只拥有算法的正确性,而算法的健壮性真的是不堪入目,也终于明白了一个真正的程序所需要拥有的东西以及编写一个能用的软件有多艰难。下面再给出书中描述的输入流与字符的关系,个人感觉受教了。
输入流与字符:
如下一行输入:
is 28 12.4
在您眼中,该输入是一串字符后面跟着一个整数,然后是一个浮点值。对C程序而言,该输入是一个字节流。第一个字节是字母i的字符编码,第二个字节是字母s的字符编码,第三个字节是空格字符的字符编码,第四个字节是数字2的字符编码,等等。
虽然输入流由字符组成,但如果您指示了scanf()函数他就可以将这些字符转换成数值。例如,考虑下面输入:
42
如果您在scanf()中使用%c说明符,该函数将只读取字符4并将其存储在一个char类型的变量中。如果您使用%s说明符,该函数会读取两个字符,即字符4和2,并将它们存储在一个字符串中。如果使用%d说明符,则scanf()读取同样的两个字符,但是随后它会继续计算与它们相应的整数值为4*10+2,即42;然后讲该整数的二进制表示保存到一个int变量中。如果使用%f说明符,则scanf()读取这两个字符,计算它们对应的数值42,然后以内部浮点表示该值,并将结果保存在一个float变量中。
C程序将输入视为一个外来字节的流。简言之,输入由字符组成,但scanf()可以将输入转换成整数或浮点值。使用像%d或%f这样的说明符能限制可接受的输入的字符类型,但getchar()和使用%c的scanf()接收任何字符。