为什么有经验的C语言程序员都不推荐使用 scanf() 函数?

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 格式,还应该确保缓冲区不会溢出。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif