5

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

 3 years ago
source link: https://blog.popkx.com/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9C%89%E7%BB%8F%E9%AA%8C%E7%9A%84c%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F%E5%91%98%E9%83%BD%E4%B8%8D%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8-scanf-%E5%87%BD%E6%95%B0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

也可以输入

第二个问题

弄清楚上一个问题后,小明又写了一段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;
}
d92005e8cd595b05cc51cef835695f6d.png

编译并执行这段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;
}
91b9db19111f9188b07748e625c0d4b2.png

小明检查 scanf() 函数的返回值,是为了确保用户输入的是数字。但是他的程序有时候会陷入死循环:
fc118dea50f2e248cc6fdba6072324e8.png

这是怎么回事呢?

当 scanf() 尝试解析数字时,遇到任何非数字字符都会终止转换,这些非数字字符会被留在输入流中。因此,如果用户输入了“x”,scanf() 永远不会跳过它,C语言程序将陷入死循环,不断的打印“try again: ”,但是又不真的给用户重新输入的机会。

可见,scanf() 函数有不少不方便的地方。另外,它的 %s 格式和 gets() 有相同的问题——很难保证接收缓冲区不会溢出(这点我之后的文章会细说,敬请关注。)

scanf() 函数还有一个不方便的地方,它的返回值可以告诉调用者是执行成功了还是失败了,但是它只能告诉调用者它失败的大概位置,而不能准确的提供失败原因,所以调用者几乎没有机会进行任何错误恢复。

设计良好的交互输入系统应该允许用户输入任何内容——不仅仅是字母和标点符号,还可以输入多于或者少于预期的字符,或者根本没有字符,以及提前的 EOF 等其他内容,此时使用 scanf() 几乎不可能优雅的处理这些输入。

如果确实要使用 scanf(),应该检查其返回值,以确保输入符合预期。如果使用了 %s 格式,还应该确保缓冲区不会溢出。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK