scanf的缺点

kar*_*_ms 58 c user-input input scanf

我想知道它的缺点scanf().

在许多站点中,我已经读过使用scanf可能导致缓冲区溢出.这是什么原因?还有其他缺点scanf吗?

AnT*_*AnT 56

到目前为止,大多数答案似乎都集中在字符串缓冲区溢出问题上.实际上,可以与scanf函数一起使用的格式说明符支持显式字段宽度设置,这限制了输入的最大大小并防止缓冲区溢出.这使得字符缓冲区溢出危险的流行指控scanf几乎毫无根据.声称scanf在某种程度上类似于gets尊重是完全错误的.scanf和之间存在一个主要的质量差异gets:scanf确实为用户提供了字符串缓冲区溢出防止功能,而gets不是.

可以说这些scanf特征难以使用,因为字段宽度必须嵌入到格式字符串中(没有办法通过可变参数传递它,因为它可以在其中完成printf).这确实是事实.scanf在这方面确实设计得很差.但是scanf,对于字符串缓冲区溢出安全性而言,任何在某种程度上无可救药地破坏的声明都是完全虚假的,通常是由懒惰程序员完成的.

真正的问题scanf具有完全不同的性质,即使它也是溢出的.当scanf函数用于将数字的十进制表示转换为算术类型的值时,它不提供算术溢出的保护.如果发生溢出,则scanf产生未定义的行为.因此,在C标准库中执行转换的唯一正确方法是来自strto...family的函数.

因此,总结一下上面的问题scanf是,使用字符串缓冲区很难(虽然可能)正确安全地使用.并且不可能安全地用于算术输入.后者是真正的问题.前者只是给您带来不便.

PS以上内容旨在介绍整个scanf功能系列(包括fscanfsscanf).有了scanf明确,明显的问题是,使用严格的格式化功能读取潜在的想法交互式输入是相当可疑的.

  • 我只需要指出,并不是你不能安全地读取算术输入,更多的是你无法正确地读取*和*强健的脏输入.对我来说,崩溃我的程序和/或打开操作系统进行攻击之间存在巨大差异,当用户尝试有目的的恶作剧时,只会得到一些错误的值.如果他们输入1431337.4044194872987并获得4.0,我该怎么办?无论哪种方式,他们进入4.0.(有时它可能很重要,但多久一次?) (2认同)
  • *“声称`scanf`在某种程度上类似于gets是完全不正确的。”*我明白了,`scanf`至少*确实*允许您指定最大字段大小,但是`%s的意识形态使用` 当然与 `gets` 具有相同的问题,并且与 C 中许多其他危险但有用的工具一样,它们都很容易被滥用。即使“strtoul”也有其危险,因此我们不能直接建议人们停止使用“全部”C*,而不是建议人们停止使用“部分”C*? (2认同)

pax*_*blo 53

scanf的问题是(至少):

  • 使用%s以从用户那里获取,这将导致该字符串可能超过你的缓冲区,引起溢出的可能性的字符串.
  • 扫描失败的可能性将文件指针留在不确定的位置.

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

然后你可以向用户投诉,或者为线路的其余部分分配更多的空间(必要时连续,直到你有足够的空间).在任何一种情况下,都没有缓冲区溢出的风险.

一旦你读完了这一行,你知道你已经定位在下一行,所以那里没有问题.然后,您可以将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;

    // 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.
    size_t lastPos = strlen(buff) - 1;
    if (buff[lastPos] != '\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[lastPos] = '\0';
    return OK;
}
Run Code Online (Sandbox Code Playgroud)

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

// 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;
}
Run Code Online (Sandbox Code Playgroud)

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

$ ./tstprg
Enter string>[CTRL-D]
No input

$ ./tstprg
Enter string> a
OK [a]

$ ./tstprg
Enter string> hello
OK [hello]

$ ./tstprg
Enter string> hello there
Input too long [hello the]

$ ./tstprg
Enter string> i am pax
OK [i am pax]
Run Code Online (Sandbox Code Playgroud)

  • 最新的 POSIX 标准允许 `char *buf; scanf("%ms", &amp;buf);` 它将使用 `malloc` 为您分配足够的空间(因此必须稍后释放),这将有助于防止缓冲区溢出。 (2认同)

jam*_*lin 13

来自comp.lang.c FAQ:为什么每个人都说不使用scanf?我应该用什么呢?

scanf有一些问题,看问题12.17,12.18a12.19.此外,它的%s格式也有同样的问题gets()(见问题12.23) - 很难保证接收缓冲区不会溢出.[脚注]

更一般地,scanf设计用于相对结构化的格式化输入(其名称实际上源自"扫描格式化").如果你注意,它会告诉你它是成功还是失败,但它只能告诉你它大概失败的地方,而不是告诉你如何或为什么.您几乎没有机会进行任何错误恢复.

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

顺便提一下,批评scanf不一定是对fscanf和的起诉sscanf.scanf读取stdin,通常是一个交互式键盘,因此受到的约束最少,导致最多的问题.另一方面,当数据文件具有已知格式时,可能适合用它来读取fscanf.解析字符串是非常合适的sscanf(只要检查返回值),因为它很容易重新获得控制权,重新启动扫描,如果输入不匹配则丢弃输入等.

其他链接:

参考文献:K&R2 Sec.7.4 p.159


Alo*_*hal 6

scanf要做你想做的事很难.当然,你可以,但是像所有人都说的scanf("%s", buf);那样危险gets(buf);.

作为一个例子,paxdiablo在他的阅读函数中所做的事情可以通过以下方式完成:

scanf("%10[^\n]%*[^\n]", buf));
getchar();
Run Code Online (Sandbox Code Playgroud)

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

其中一个问题scanf是它在溢出时的行为.例如,阅读时int:

int i;
scanf("%d", &i);
Run Code Online (Sandbox Code Playgroud)

如果溢出,上述情况不能安全使用.即使对于第一种情况,读取字符串也更简单,fgets而不是使用scanf.


cod*_*ict 5

是的,你是对的.有一个重大的安全漏洞scanf家庭(scanf,sscanf,fscanfESP阅读字符串时,因为不拿缓冲区的长度(进入他们正在阅读)进去..等).

例:

char buf[3];
sscanf("abcdef","%s",buf);
Run Code Online (Sandbox Code Playgroud)

显然缓冲区buf可以容纳MAX 3char.但是sscanf会试图"abcdef"加入它导致缓冲区溢出.

  • 当然 - 可以安全地使用API​​.也可以使用炸药安全地清除花园里的污垢.但我也不建议,特别是因为有更安全的替代方案. (5认同)
  • 我父亲曾经使用gelignite清理农场的树木.您只需要了解您的工具并了解危险. (4认同)
  • 您可以提供"%10s"作为格式说明符,它将在缓冲区中读取不超过10个字符. (2认同)
  • @codacci :有人不将字段宽度与 `scanf` 一起使用的事实是那个人的问题,而不是 `scanf`。它与所讨论的问题完全无关。毕竟这是C,不是Java。 (2认同)
  • 问题是“scanf()”中的字段宽度必须在转换说明符中硬编码;使用 printf() 时,您可以在转换说明符中使用 * 并将长度作为参数传递。但由于“*”在“scanf()”中意味着不同的东西,所以这是行不通的,所以你基本上必须为每次读取生成一个新的格式,就像 Alok 在他的示例中所做的那样。它只会增加更多的工作和混乱;不妨使用“fgets()”并完成它。 (2认同)

aut*_*tic 5

优点是scanf一旦您学会了如何使用该工具(就像您在 C 语言中应该经常做的那样),它就有非常有用的用例。scanf您可以通过阅读和理解说明书来学习如何使用和使用。如果您在没有严重理解问题的情况下无法读完该手册,这可能表明您不太了解 C。


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

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

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

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

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

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

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

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

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

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

稍作调整,通过分离两种格式委托,我们在这里看到了一些成功:scanf("%*[^\n]"); getchar();。尝试使用其他工具通过很少的击键来完成此操作;)