C语言初学者一般对 scanf() 函数比较熟悉,它使程序能够接收用户的输入,例如下面这段C语言代码:
int n;
scanf("%d", &n);
printf("n = %d\n", n);
程序运行到 scanf() 时会停下来,等待用户输入一个数字,然后会把该数字存放在变量 n 里。看起来,这段C语言代码可以很好的工作,可为什么几乎每个有经验的C语言程序员都建议不要使用 scanf() 呢?
看了下面几个问题,相信读者就明白了。
第一个问题
程序员小明写出了下面的C语言代码,他打算使用 scanf() 函数和 "%d\n" 格式接收键盘输入的数字,请看:
#include <stdio.h>
int main()
{
int n;
scanf("%d\n", &n);
printf("n = %d\n", n);
return 0;
}
编译并执行这段C语言代码,小明发现需要输入两次,程序才会正常打印出 n 的值,否则会一直阻塞在 scanf():
# gcc t.c
# ./a.out
123
123
n = 123
这是怎么回事呢?
"\n" 对于 scanf() 来说并不意味着需要换行,而是读取和丢弃空白字符(如空格、换行符等)。事实上,scanf() 格式字符串中的任何空白字符都意味着读取和丢弃空白字符。此外,像 "%d" 这样的格式也会丢弃前导空白字符,所以在编写C语言程序调用 scanf() 时,无需再显式的指定空格了。
因此,scanf("%d\n", &n); 中的 "\n" 会导致 scanf() 读取用户的键盘输入直到遇到非空白字符,并且在这一过程中很可能还需要读取其他行。所以要解决上述问题,可以将“%d\n”改成 "%d",不再使用 "\n"。
scanf() 是为了尽量满足输入的方便性而设计的,对于 scanf() 来说,空白字符和换行并没有什么不同,所以 "%d %d %d" 格式的 scanf(),用户可以输入:
1 2 3
也可以输入
1
2
3
第二个问题
弄清楚上一个问题后,小明又写了一段C语言程序,它先使用了 scanf() 和 "%d",接着又调用了 gets() 函数,相关C语言代码如下,请看:
#include <stdio.h>
int main()
{
int n;
char str[80];
printf("enter a number: ");
scanf("%d", &n);
printf("enter a string: ");
gets(str);
printf("you typed %d and \"%s\"\n", n, str);
return 0;
}
编译并执行这段C语言代码,小明发现程序跳过了 gets() 的调用:
# gcc t.c
# ./a.out
enter a number: 123
enter a string: you typed 123 and ""
显然,C语言程序并没有给小明输入 str 的机会,在接收到 123 后,程序就直接打印,结束运行了。这是怎么回事呢?
我们来设想一下,假如小明希望输入下面这两行信息:
123
a string
那么 scanf() 函数将读取 123,但是不会读取后面的换行符,该换行符将保留在标准输入缓冲里,接下来的 gets() 函数遇到缓冲里的换行符时,会立即得到满足(就像小明按下回车一样),第二行的“a string”根本不会被读取。
不过,如果在同一行里同时输入数字和字符串:
123 a string
这段C语言程序就会按照预期输出了,不过,也只是按照预期“输出”而已,程序的逻辑依然是不正常的,预期的 "enter a string" 后并未允许小明输入一段字符串:
# ./a.out
enter a number: 123 a string
enter a string: you typed 123 and " a string"
事实上,scanf() 和 gets() 不该在一起使用。scanf() 对于换行的处理总是会导致麻烦,所以要么使用 scanf() 读取所有内容,要么就什么都不读。
第三个问题
弄清楚第二个问题后,小明不再混用 scanf() 和 gets() 函数了。scanf() 函数是有返回值的,小明感觉检查 scanf() 函数的返回值会让C语言程序更加安全,于是他写出了下面这样的代码,请看:
#include <stdio.h>
int main()
{
int n;
while(1) {
printf("enter a number: ");
if(scanf("%d", &n) == 1)
break;
printf("try again: ");
}
printf("you typed %d\n", n);
return 0;
}
小明检查 scanf() 函数的返回值,是为了确保用户输入的是数字。但是他的程序有时候会陷入死循环:
这是怎么回事呢?
当 scanf() 尝试解析数字时,遇到任何非数字字符都会终止转换,这些非数字字符会被留在输入流中。因此,如果用户输入了“x”,scanf() 永远不会跳过它,C语言程序将陷入死循环,不断的打印“try again: ”,但是又不真的给用户重新输入的机会。
小结
可见,scanf() 函数有不少不方便的地方。另外,它的 %s 格式和 gets() 有相同的问题——很难保证接收缓冲区不会溢出(这点我之后的文章会细说,敬请关注。)
scanf() 函数还有一个不方便的地方,它的返回值可以告诉调用者是执行成功了还是失败了,但是它只能告诉调用者它失败的大概位置,而不能准确的提供失败原因,所以调用者几乎没有机会进行任何错误恢复。
设计良好的交互输入系统应该允许用户输入任何内容——不仅仅是字母和标点符号,还可以输入多于或者少于预期的字符,或者根本没有字符,以及提前的 EOF 等其他内容,此时使用 scanf() 几乎不可能优雅的处理这些输入。
如果确实要使用 scanf(),应该检查其返回值,以确保输入符合预期。如果使用了 %s 格式,还应该确保缓冲区不会溢出。