scanf 的缺点

Disadvantages of scanf

提问人:karthi_ms 提问时间:3/12/2010 最后编辑:Spikatrixkarthi_ms 更新时间:4/14/2022 访问量:80050

问:

我想知道 的缺点。scanf()

在许多站点中,我读到使用可能会导致缓冲区溢出。这是什么原因?还有其他缺点吗?scanfscanf

C 输入 用户输入 扫描

评论

4赞 Jonathan Leffler 7/19/2017
另请参阅远离 scanf() 的初学者指南

答:

72赞 paxdiablo 3/12/2010 #1

scanf 的问题是(至少):

  • 用于从用户那里获取字符串,这可能会导致字符串可能比缓冲区长,从而导致溢出。%s
  • 扫描失败的可能性,将文件指针留在不确定的位置。

我非常喜欢使用读取整行,以便您可以限制读取的数据量。如果你有一个 1K 的缓冲区,并且你读了一行,你可以通过没有终止换行符(尽管没有换行符的文件的最后一行)来判断该行是否太长。fgetsfgets

然后,您可以向用户投诉,或为行的其余部分分配更多空间(如有必要,请连续分配,直到您有足够的空间)。无论哪种情况,都没有缓冲区溢出的风险。

一旦你读完了这一行,你就知道你位于下一行,所以那里没有问题。然后,您可以随心所欲地使用字符串,而无需保存和恢复文件指针以供重新阅读。sscanf

下面是一段代码,我经常使用它来确保在向用户询问信息时不会出现缓冲区溢出。

如有必要,可以很容易地调整为使用标准输入以外的文件,你也可以让它分配自己的缓冲区(并不断增加它,直到它足够大),然后再把它交还给调用者(当然,调用者将负责释放它)。

#include <stdio.h>
#include <string.h>

#define OK         0
#define NO_INPUT   1
#define TOO_LONG   2
#define SMALL_BUFF 3
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Size zero or one cannot store enough, so don't even
    // try - we need space for at least newline and terminator.

    if (sz < 2)
        return SMALL_BUFF;

    // Output prompt.

    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }

    // Get line with buffer overrun protection.

    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // Catch possibility of `\0` in the input stream.

    size_t len = strlen(buff);
    if (len < 1)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.

    if (buff[len - 1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[len - 1] = '\0';
    return OK;
}

并且,它的测试驱动程序:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        // Extra NL since my system doesn't output that on EOF.
        printf ("\nNo input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long [%s]\n", buff);
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

最后,测试运行以显示其运行情况:

$ printf "\0" | ./tstprg     # Singular NUL in input stream.
Enter string>
No input

$ ./tstprg < /dev/null       # EOF in input stream.
Enter string>
No input

$ ./tstprg                   # A one-character string.
Enter string> a
OK [a]

$ ./tstprg                   # Longer string but still able to fit.
Enter string> hello
OK [hello]

$ ./tstprg                   # Too long for buffer.
Enter string> hello there
Input too long [hello the]

$ ./tstprg                   # Test limit of buffer.
Enter string> 123456789
OK [123456789]

$ ./tstprg                   # Test just over limit.
Enter string> 1234567890
Input too long [123456789]

评论

0赞 Fabio Carello 3/30/2013
if (fgets (buff, sz, stdin) == NULL) return NO_INPUT;为什么使用作为返回值? 仅在出错时返回。NO_INPUTfgetsNULL
0赞 paxdiablo 3/30/2013
@Fabio,不完全是。如果在进行任何输入之前关闭流,它还返回 null。这里就是这种情况。不要误以为NO_INPUT表示空输入(先按 ENTER 键),后者会给你一个空字符串,没有NO_INPUT错误代码。
2赞 dreamlax 10/3/2014
最新的 POSIX 标准允许为您分配足够的空间(因此以后必须释放),这将有助于防止缓冲区溢出。char *buf; scanf("%ms", &buf);malloc
1赞 autistic 7/12/2018
如果我们以 as 参数调用会发生什么? 是问题发生的地方。也许当您通过时,它确实不会溢出,并且具有以零开销为您删除的额外好处,尽管应该注意,您的代码可以通过战略性地使用 scanf 来增强......getLine1szif (buff[strlen(buff)-1] != '\n')if (!sz) { return TOO_LONG; } if (buff[sz = strcspn(buff, "\n")] == '\n' || getchar() == '\n') { buff[sz] = '\0'; return OK; } unsigned char c; while (fread(&c, 1, 1, stdin) == 1 && c != '\n'); return TOO_LONG;sz <= 1'\n'
1赞 paxdiablo 9/1/2020
这是一个很好的收获,@chux,我为此添加了一个额外的检查,将其视为“无输入”。进行了测试以验证原始问题并修复。我想我从来没有检查过这样的疯狂输入场景(但我该死的应该有)。谢谢你的提醒。printf "\0" | exeName
5赞 codaddict 3/12/2010 #2

是的,你是对的。家庭存在重大安全漏洞(,, ..etc) 尤其是在读取字符串时,因为它们没有考虑缓冲区(它们正在读取的)的长度。scanfscanfsscanffscanf

例:

char buf[3];
sscanf("abcdef","%s",buf);

显然,缓冲区可以保存 MAX 字符。但是会尝试放入其中,导致缓冲区溢出。buf3sscanf"abcdef"

评论

4赞 dreamlax 3/12/2010
您可以提供“%10s”作为格式说明符,它将在缓冲区中读取不超过 10 个字符。
5赞 Larry Osterman 3/12/2010
当然 - 可以安全地使用 API。也可以使用炸药安全地清除花园中的污垢。但我也不建议这样做,特别是因为有更安全的替代品。
4赞 paxdiablo 3/12/2010
我父亲曾经用明胶石来清理农场的树木。你只需要了解你的工具并知道危险。
2赞 AnT stands with Russia 3/12/2010
@codaddict:有人不使用字段宽度的事实是这个人的问题,而不是 .它与所讨论的问题完全无关。这毕竟是 C,不是 Java。scanfscanf
2赞 John Bode 3/12/2010
问题在于必须在转换说明符中硬编码字段宽度;使用 ,您可以在转换说明符中使用,并将长度作为参数传递。但是,由于 中的意思不同,这是行不通的,所以你基本上必须为每次读取生成一种新的格式,就像 Alok 在他的示例中所做的那样。它只会增加更多的工作和混乱;还不如使用它并完成它。scanf()printf()**scanf()fgets()
22赞 jamesdlin 3/12/2010 #3

来自 comp.lang.c 常见问题解答:为什么每个人都说不要使用 scanf?我应该用什么来代替?

scanf存在许多问题,请参阅问题 12.17、12.18a12.19此外,它的格式也存在同样的问题(参见问题 12.23),很难保证接收缓冲区不会溢出。[脚注]%sgets()

更一般地说,它是为相对结构化的格式化输入而设计的(它的名称实际上来源于“扫描格式化”)。如果你注意,它会告诉你它是成功还是失败,但它只能告诉你它失败的大致位置,而根本无法告诉你如何或为什么。您几乎没有机会进行任何错误恢复。scanf

然而,交互式用户输入是最不结构化的输入。精心设计的用户界面将允许用户输入几乎任何内容 - 不仅仅是字母或标点符号,当数字是预期的,还有比预期更多或更少的字符,或者根本没有字符(,只有 RETURN 键),或过早的 EOF,或任何东西。使用时几乎不可能优雅地处理所有这些潜在问题;阅读整行(有或类似的东西),然后使用或其他一些技术来解释它们要容易得多。(像 、 和 这样的函数通常很有用;另见问题 12.1613.6。如果使用任何变体,请务必检查返回值,以确保找到预期的项数。此外,如果使用 ,请务必防止缓冲区溢出。scanffgetssscanfstrtolstrtokatoiscanf%s

顺便说一句,请注意,对 和 的批评不一定是对 和 的控诉。 reads from ,它通常是一个交互式键盘,因此受约束最少,导致的问题最多。另一方面,当数据文件具有已知格式时,使用 .解析字符串是完全合适的(只要检查返回值),因为它很容易重新获得控制权、重新开始扫描、如果输入不匹配就丢弃输入等。scanffscanfsscanfscanfstdinfscanfsscanf

其他链接:

参考资料:K&R2 Sec. 7.4 p. 159

6赞 Alok Singhal 3/12/2010 #4

做你想做的事是非常困难的。当然,你可以,但像这样的事情就像每个人都说的那样危险。scanfscanf("%s", buf);gets(buf);

举个例子,paxdiablo 在他的函数中所做的是阅读,可以用这样的东西来完成:

scanf("%10[^\n]%*[^\n]", buf));
getchar();

上面将读取一行,将前 10 个非换行符存储在 中,然后丢弃所有内容,直到(包括)换行符。因此,paxdiablo 的函数可以使用以下方式编写:bufscanf

#include <stdio.h>

enum read_status {
    OK,
    NO_INPUT,
    TOO_LONG
};

static int get_line(const char *prompt, char *buf, size_t sz)
{
    char fmt[40];
    int i;
    int nscanned;

    printf("%s", prompt);
    fflush(stdout);

    sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1);
    /* read at most sz-1 characters on, discarding the rest */
    i = scanf(fmt, buf, &nscanned);
    if (i > 0) {
        getchar();
        if (nscanned >= sz) {
            return TOO_LONG;
        } else {
            return OK;
        }
    } else {
        return NO_INPUT;
    }
}

int main(void)
{
    char buf[10+1];
    int rc;

    while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) {
        if (rc == TOO_LONG) {
            printf("Input too long: ");
        }
        printf("->%s<-\n", buf);
    }
    return 0;
}

另一个问题是它在溢出时的行为。例如,当读取:scanfint

int i;
scanf("%d", &i);

如果发生溢出,上述产品不能安全使用。即使是第一种情况,读取字符串也比使用 .fgetsscanf

85赞 AnT stands with Russia 3/12/2010 #5

到目前为止,大多数答案似乎都集中在字符串缓冲区溢出问题上。实际上,可与函数一起使用的格式说明符支持显式字段宽度设置,这限制了输入的最大大小并防止缓冲区溢出。这使得对字符串缓冲区溢出危险的普遍指责几乎是毫无根据的。声称这在某种程度上类似于在这方面是完全不正确的。和 之间有一个主要的质的区别:确实为用户提供了字符串缓冲区溢出防止功能,而没有。scanfscanfscanfgetsscanfgetsscanfgets

有人可能会争辩说这些功能很难使用,因为字段宽度必须嵌入到格式字符串中(没有办法通过可变参数传递它,因为它可以在 中完成)。这实际上是真的。 在这方面确实设计得相当糟糕。但是,尽管如此,任何关于字符串缓冲区溢出安全性的声明都是完全虚假的,通常是由懒惰的程序员提出的。scanfprintfscanfscanf

真正的问题具有完全不同的性质,尽管它也与溢出有关。当函数用于将数字的十进制表示形式转换为算术类型的值时,它不能防止算术溢出。如果发生溢出,则产生未定义的行为。因此,在 C 标准库中执行转换的唯一正确方法是来自族的函数。scanfscanfscanfstrto...

因此,综上所述,问题在于很难(尽管可能)正确安全地使用字符串缓冲区。并且不可能安全地用于算术输入。后者才是真正的问题。前者只是一种不便。scanf

P.S. 以上旨在介绍整个函数系列(也包括 和 )。具体来说,显而易见的问题是,使用严格格式化的函数来读取潜在的交互式输入的想法是相当值得怀疑的。scanffscanfsscanfscanf

评论

6赞 1/3/2014
我只需要指出,这并不是说你不能安全地读取算术输入,而是你不能正确稳健地读取脏输入。对我来说,当用户尝试有目的的恶作剧时,使我的程序崩溃和/或打开操作系统进行攻击与简单地获得一些错误的值之间存在巨大差异。如果他们输入了 1431337.4044194872987 并得到了 4.0,我该在乎什么?无论哪种方式,他们都进入了 4.0。(有时这可能很重要,但多久一次?
1赞 2501 1/20/2016
第三段:如果在字符串中遇到 >2^32 的值,scanf 会很乐意将值读取为 32 位整数并导致未定义的行为?
0赞 AnT stands with Russia 1/20/2016
@2501:是的,没错。至少根据语言标准是这样。
2赞 autistic 7/12/2018
“声称scanf在某种程度上类似于得到尊重是完全不正确的。我明白了,至少确实允许您指定最大字段大小,但是意识形态上的使用肯定与 存在相同的问题,并且与 C 中的许多其他危险但有用的工具一样,它们都很容易被滥用。甚至有其危险,所以与其建议人们停止使用部分 C,我们难道不能跳到建议人们停止使用所有 C 语言吗?scanf%sgetsstrtoul
4赞 John Bode 3/12/2010 #6

我与家人的问题:*scanf()

  • %s 和 %[ 转换说明符可能导致缓冲区溢出。是的,您可以指定最大字段宽度,但与 不同的是,您不能将其作为调用中的参数;它必须在转换说明符中硬编码。printf()scanf()
  • %d、%i 等的算术溢出的可能性。
  • 检测和拒绝格式错误的输入的能力有限。例如,“12w4”不是一个有效的整数,但会成功转换并分配 12 ,使“w4”停留在输入流中以破坏未来的读取。理想情况下,整个输入字符串应该被拒绝,但不会给你一个简单的机制来做到这一点。scanf("%d", &value);valuescanf()

如果你知道你的输入总是会用固定长度的字符串和不会溢出的数值来格式良好,那么这是一个很棒的工具。如果您正在处理交互式输入或不能保证格式正确的输入,请使用其他内容。scanf()

评论

1赞 Rajkumar S 3/23/2012
还有哪些其他合理的替代方法可以安全地读取固定长度的字符串和数值?
4赞 dreamlax 10/3/2014 #7

这里的许多答案都讨论了使用 的潜在溢出问题,但最新的 POSIX 规范或多或少地解决了这个问题,它提供了一个赋值分配字符,该字符可用于 、 和 格式的格式说明符。这将允许分配尽可能多的内存(因此以后必须使用 释放)。scanf("%s", buf)mcs[scanfmallocfree

其使用示例:

char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.

// use buf

free(buf);

看这里。这种方法的缺点是它是 POSIX 规范中相对较新的补充,并且根本没有在 C 规范中指定,因此它目前仍然相当不可移植。

4赞 Vladimir Veljkovic 10/13/2015 #8

类函数存在一个大问题 - 缺乏任何类型的安全性。也就是说,您可以编写以下代码:scanf

int i;
scanf("%10s", &i);

见鬼,即使这样也“很好”:

scanf("%10s", i);

它比 -like 函数更糟糕,因为需要指针,因此崩溃的可能性更大。printfscanf

当然,有一些格式说明器检查器,但是,这些检查器并不完美,它们不是语言或标准库的一部分。

评论

0赞 Graeme 10/3/2018
这更像是一个历史问题,因为大多数现代编译器会检查参数的类型是否与格式字符串中指定的参数匹配,如果它们不匹配,则会生成警告。但是,我敢肯定仍然有很多没有。
5赞 autistic 3/22/2016 #9

scanf 的优势在于,一旦你学会了如何使用这个工具,就像你应该在 C 中所做的那样,它就有了非常有用的用例。您可以通过阅读和理解手册来学习如何使用和朋友。如果你不能在没有严重理解问题的情况下阅读该手册,这可能表明你不太了解 C。scanf


正如其他答案所显示的那样,Scanf 和朋友们遭受了不幸的设计选择,这使得在不阅读文档的情况下很难(有时甚至不可能)正确使用。不幸的是,这种情况发生在整个 C 语言中,所以如果我建议不要使用 C,那么我可能会建议不要使用 C。scanf

最大的缺点之一似乎纯粹是它在外行中赢得的声誉;与 C 的许多有用功能一样,我们应该在使用它之前充分了解它。关键是要意识到,与C语言的其余部分一样,它看起来简洁明了,但这可能会产生微妙的误导。这在 C 语言中很普遍;对于初学者来说,编写他们认为有意义的代码很容易,甚至可能在最初对他们有用,但没有意义,并且可能会灾难性地失败。

例如,外行通常期望委托会导致一被读取,虽然这看起来很直观,但不一定是真的。将字段描述为一个单词更合适。强烈建议阅读手册以了解每个功能。%s

如果不提及其缺乏安全性和缓冲区溢出的风险,对这个问题的回答会是什么?正如我们已经介绍过的,C 语言不是一种安全的语言,它会允许我们偷工减料,可能会以牺牲正确性为代价进行优化,或者更有可能是因为我们是懒惰的程序员。因此,当我们知道系统永远不会收到大于固定字节数的字符串时,我们就能够声明一个大小并放弃边界检查的数组。我真的不认为这是一种垮台;这是一种选择。同样,强烈建议阅读手册,并向我们揭示此选项。

懒惰的程序员并不是唯一被 scanf 刺痛的人。例如,看到人们试图阅读或使用价值观的情况并不少见。他们通常错误地认为实现会在幕后执行某种转换,这是有道理的,因为类似的转换发生在语言的其余部分,但这里的情况并非如此。正如我之前所说,朋友(实际上是 C 的其余部分)是欺骗性的;它们看起来简洁明了,但事实并非如此。floatdouble%dscanf

没有经验的程序员不会被迫考虑操作的成功。假设用户输入了一些完全非数字的东西,而我们告诉用户使用 读取和转换十进制数字序列。我们拦截这些错误数据的唯一方法是检查返回值,我们多久检查一次返回值?scanf%d

就像 fgets 一样,当 scanf 和朋友无法阅读他们被告知要阅读的内容时,流将处于异常状态;

  • 在 的情况下,如果没有足够的空间来存储一整行,则未读的行的其余部分可能会被错误地视为新行,而实际上它不是。fgets
  • 在 和 friends 的情况下,如上所述,转换失败,错误数据在流上未读取,并且可能被错误地视为不同字段的一部分。scanf

使用 scanf 和 friends 并不比使用 fgets 容易。如果我们通过在使用时查找 a 或在使用 和 friends 时检查返回值来检查成功,并且我们发现我们读取了不完整的行 using 或未能读取字段 using ,那么我们将面临同样的现实:我们很可能会丢弃输入(通常直到并包括下一个换行符)!呜!'\n'fgetsscanffgetsscanf

不幸的是,两者都同时使得以这种方式丢弃输入变得困难(不直观)和容易(击键最少)。面对这种丢弃用户输入的现实,一些人尝试了 scanf(“%*[^\n]%*c”);,没有意识到当委托只遇到换行符时会失败,因此换行符仍将留在流中。scanf%*[^\n]

通过分离两个格式代表,稍微调整一下,我们在这里看到了一些成功:.尝试使用其他工具以如此少的击键来做到这一点;)scanf("%*[^\n]"); getchar();