发件人:Steve Summit
主题:Refflushgets
日期:2000/02/12
Message-ID: <clcm-20000211-0019@plethora.net>
Newsgroups: comp.lang.c,comp.lang.c.moderated

Peter S. Shenkin 曾写道
> 你为什么会想丢弃用户输入,
> 以及你会以何种方式知道要丢弃哪一部分?

你很可能从未尝试在同一个程序中调用scanfgets。如果你没有尝试过,你就幸福地不知道这个棘手的小问题。

假设你写了这个简单的程序

	#include <stdio.h>
	int main()
	{
		int i;
		char string[80];
		printf("enter an integer:\n");
		scanf("%d", &i);
		printf("enter a string:\n");
		gets(string);
		printf("You typed %d and \"%s\"\n", i, string);
		return 0;
	}

看起来很直接,对吧?但如果你编译并运行它(如果你还不熟悉这个问题,我鼓励你去这样做),你会看到一些奇怪的东西,你会发现自己(我保证)会问 comp.lang.c FAQ 列表中的问题 12.18:“我用scanf %d读取一个数字,然后用gets()读取一个字符串,但编译器似乎跳过了对“gets()!”(我们稍后会详细讨论如何使用

,但先记下这个想法)。gets)。

让我们仔细看看发生了什么。第一个printf调用打印第一个提示,我们输入“123”然后按回车键。输入流现在包含

	1 2 3 \n
现在我们按scanf调用,而scanf看到格式字符串%d,表示我们要求它读取一个整数。它从输入流中读取字符“1”,然后自言自语:“好的,这是一个数字,所以它可以是一个整数的一部分。”它读取字符“2”和“3”,它们也是数字。下一个字符是“\n”,这不是一个数字。所以scanf做了两件事

  1. 它终止了对%d指令的处理;它现在知道它已经读取的完整整数是 123;它将此值按请求存储到变量i.

  2. 的位置(这是关键点。)它将终止数字字符串但未使用的字符“\n”推回输入流。

所以,在第一次scanf调用之后,输入流包含

	\n
现在第二次printf调用打印第二个提示。假设我们输入“test”然后按回车键。输入流现在包含
	\n t e s t \n
所以现在我们来看gets调用。gets's job, of course, is to read one line of input, up to the next newline. But the very first character gets sees is a\n,所以对它来说,它只是读了一个空行。它返回一个空行(一个空字符串,因为gets总是将换行符从行中删除,然后返回给你),输入流中剩下
	t e s t \n
第三个printf调用打印两个输入的(有些令人惊讶的)结果,程序终止,字符串“test”和最后的换行符未被消耗。

如果这仍然不太清楚,请尝试再次运行程序,并在被要求“输入一个整数”时,在第一个提示符处输入比数字更多的内容。也就是说,尝试输入“123 abc”或“123abc”,然后按回车键。

(实际上,上述描述并不完全准确。无论你在第一行输入什么,第一个 scanf 调用后的输入流仍然包含一个 \n,所以gets调用立即读取它,而无需暂停让你输入更多内容。所以你实际上没有机会输入“test”。以另一种方式回答 FAQ 列表的问题,问题不是编译器如何“跳过”了gets调用,问题是gets调用以一种意想不到的方式满足了其输入需求,并跳过了暂停程序等待你输入更多内容的环节。)

有了上述场景作为背景,我们现在可以回答你的问题:“你为什么会想丢弃用户输入?”无论好坏,许多初学者程序员都使用scanf来读取数字,并使用gets来读取字符串。这很大程度上是因为这些函数在许多书籍和编程课程中被早期教授。而这又是因为这些函数表面上具有诱惑力;它们看起来非常简单方便使用。但它们根本不适合一起工作(此外它们还有一些其他问题,我们稍后会讲到)。

当初学者编写上述这样的程序并发现它不能正常工作时,他很可能会得到一个含糊的解释(来自讲师或教科书作者),即有一些“垃圾”被scanf留在输入流中。”(我们,更准确地理解情况的人,现在知道“垃圾”在例子中,就是我们输入请求的数字后按回车键产生的\n)。为了让后续输入如预期进行,这些讲师和作者会继续解释,必须“丢弃”这些“垃圾”。一种非常流行(并且再次,表面上很有吸引力)的方法是调用fflush(stdin),尽管这是一个对标准fflush函数的不恰当应用,一个不保证(实际上几乎肯定不)在所有地方都工作的应用。但它在大量流行的 PC C 编译器下“工作”,因此这种“习惯用法”不幸地被广泛传播。

正确的解决方案是什么?很容易纠结于这样一个事实:fflush(stdin),出于某种可能是愚蠢而迂腐的原因,不保证在所有地方都工作。然后人们开始四处寻找一个“可移植”的替代方案。问题是,取决于一个人具体想做什么,在尝试编写一些明确定义的或可移植的代码来“丢弃 stdin 中的垃圾”时,有相当多的不同方法可以采取。(在一般情况下,正如你正确地问到的,“[一个人]会以何种方式知道要丢弃哪一部分?”)

如果我们尝试丢弃的“stdin 中的垃圾”的定义是“前一行中未被scanf消耗的输入”,那么实际上有几种不完全不合理的方法。我们可以编写循环

	while((c = getchar()) != '\n' && c != EOF)
		/* discard the character */;

来读取和丢弃字符直到下一个换行符。(请注意,这个片段中的注释“/* 丢弃字符 */”并不代表我还没写的代码——它代表了我故意没写任何代码。循环体是空的;我们不处理我们读取的字符,从而丢弃它们。终止循环的\n也会被丢弃。)

由于每次scanf调用后都要插入这个循环,这会使我们的代码变得相当混乱,我们可以将它封装到一个可以调用的函数中,也许命名为“flushline”之类的。或者,认识到“读取直到换行符的字符”正是标准函数getsfgets已经做的事情,我们可以简单地插入对getsfgets的调用,读取到一个我们忽略(从而丢弃)的虚拟缓冲区中,也许附带注释解释这些虚拟调用是为了“摆脱 scanf 留下的垃圾”。但这仍然是丑陋、不干净、令人不满意 的解决方案。我们不会花太长时间,我们的一些 scanf 调用,出于某种原因,最终确实消耗了一个换行符,这样我们补偿性的“读取并丢弃直到换行符的字符”代码就会读取并丢弃下一行输入,一个真正的输入行,然后它会被期望它的输入读取代码丢失。我们可以尝试预测哪些scanf调用会留下“垃圾”,哪些scanf调用不会留下“垃圾”,并且只在那些需要它们scanf调用之后才插入“flushline”调用,但这是一种碰运气的方法,而后续的读者将永远无法确切理解我们在做什么。一定有更好的方法。

正如 FAQ 列表所示,“更好的方法”要么是完全放弃scanf,要么是专门使用它。如果你的输入是基于行的,你可以读取所有行的输入作为字符串,使用fgets或类似的函数,对于那些应该是数字的,可以使用像atoi, strtol, atof,或者甚至sscanf这样的函数将字符串转换为数字。(这是我推荐的通用方法。)或者,因为问题是scanf不与其他函数配合得好,你可以切换到一个方案,你使用scanf来处理所有事情,包括读取字符串(使用%s或类似的)。

最后,我应该补充几点。事实证明,scanf除了容易在输入流中留下未消化的“惊喜”之外,还有其他问题,所以还有其他理由考虑放弃它。当然,正如 comp.lang.c 近期广泛讨论的那样,gets有一个致命的缺陷,这使得它不适合做任何事情。

--

Steve Summit
scs@eskimo.com

编程挑战 #5:热爱你的抽象。
请看 http://www.eskimo.com/~scs/challenge/