scanf的缺点


Answers:


67

scanf的问题(至少)是:

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

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

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

读完该行后,您将知道自己位于下一行,因此在那里没有问题。然后,您可以sscanf将字符串放入您内心的内容,而不必保存和恢复文件指针以进行重新读取。

这是我经常使用的代码片段,以确保在询问用户信息时不会出现缓冲区溢出。

可以根据需要轻松地调整它以使用标准输入以外的文件,也可以让它分配自己的缓冲区(并不断增加直到它足够大),然后再将其返回给调用者(尽管调用者将对此负责)释放它,当然)。

并且,为此的测试驱动程序:

最后,进行测试以显示其实际效果:


if (fgets (buff, sz, stdin) == NULL) return NO_INPUT;您为什么使用它NO_INPUT作为返回值?仅在错误时fgets返回NULL
法比奥·卡洛洛

@Fabio,不是。如果在进行任何输入之前关闭流,它也将返回null。就是这种情况。不要误认为NO_INPUT表示空输入(在其他之前先按ENTER)-后者会为您提供一个空字符串,且没有NO_INPUT错误代码。
paxdiablo 2013年

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

1
如果调用getLinewith1作为sz参数会发生什么?if (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'在零开销下为您删除代码的附加好处,尽管应该注意,可以通过战略性使用scanf...来增强您的代码……
自闭症患者

1
@chux,这很不错,我为此添加了额外的检查,将其视为“无输入”。完成了测试printf "\0" | exeName以验证原始问题并修复。我想我从来没有检查过像这样的疯狂输入场景(但我该死的应该有)。感谢您的注意。
paxdiablo

62

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

可以说这些scanf功能难以使用,因为字段宽度必须嵌入格式字符串中(无法通过可变参数传递它,因为可以在中完成printf)。确实是这样。scanf在这方面确实设计得很差。但是,尽管如此scanf,关于字符串缓冲区溢出安全性无可避免地被打破的任何主张都是完全虚假的,通常是由懒惰的程序员提出的。

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

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

PS以上内容旨在scanf涵盖整个功能家族(包括fscanfand sscanf)。有了scanf明确,明显的问题是,使用严格的格式化功能读取潜在的想法交互式输入是相当可疑的。


3
我只需要指出,这并不是说您不能安全地读取算术输入,更多的是您不能正确可靠地处理脏输入。对我来说,崩溃程序和/或打开操作系统进行攻击与用户尝试故意作弊时仅获取一些错误值之间存在巨大差异。如果他们输入1431337.4044194872987并获得4.0,我该怎么办?无论哪种方式,他们都进入了4.0。(有时可能很重要,但是多久一次?)

AnT,“将数字的十进制表示转换为算术类型的值”是什么意思?你能举个例子吗?谢谢
snowfox 2015年

@snowfox:我只是想将字符串 "123"转换为number的内部整数表示形式123
AnT

第三段:如果在字符串中遇到scanf,它将很乐意将> 2 ^ 32的值读取为32位整数并导致未定义的行为?
2501年

1
scanf在某种程度上类似于获得尊重的主张是完全错误的。” 我明白了,scanf至少确实允许您指定最大字段大小,但是意识形态上的使用%s肯定与存在相同的问题gets,并且与C中的许多其他危险而有用的工具一样,它们都很容易被滥用。即使strtoul有危险,所以不是暗示人们停止使用部分的C,我们不能只跳到暗示人们停止使用所有的C
自闭症

15

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

scanf有一些问题,参见问题12.1712.18a12.19。同样,其%s格式也存在相同的问题gets()(请参见问题12.23)—很难保证接收缓冲区不会溢出。[脚注]

更一般地说,scanf是为相对结构化的格式化输入而设计的(其名称实际上是从“扫描格式化”派生的)。如果您注意的话,它会告诉您它是成功还是失败,但是它只能告诉您它在哪里失败,而根本不告诉您如何或为什么。您几乎没有机会进行任何错误恢复。

然而,交互式用户输入是其中结构最少的输入。精心设计的用户界面将允许用户键入几乎所有内容的可能性-不仅是预期数字时的字母或标点符号,而且比预期的字符更多或更少,或者根本没有字符(,仅返回)键),或过早的EOF或其他任何内容。使用时几乎不可能优雅地处理所有这些潜在问题scanf;使用(fgets或类似方法)阅读整行,然后解释它们,要容易得多sscanf。(如strtolstrtokatoi等功能通常很有用;另请参见问题12.1613.6。)如果确实使用任何scanf变体,请确保检查返回值以确保找到了预期的项目数。另外,如果您使用%s,请确保防止缓冲区溢出。

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

附加链接:

参考:K&R2第2节。7.4羽 159


6

scanf做你想做的事情很难。当然可以,但是就像每个人都说过的scanf("%s", buf);一样危险gets(buf);

例如,paxdiablo在阅读功能中的作用可以通过以下方式完成:

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

另一个问题之一scanf是在溢出情况下的行为。例如,当阅读时int

如果发生溢出,以上内容将无法安全使用。即使是第一种情况下,读书的字符串是更简单的与做的fgets,而不是用scanf


5

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

例:

显然,缓冲区buf可以容纳MAX 3char。但sscanf将尝试把"abcdef"它,导致缓冲区溢出。


2
您可以提供“%10s”作为格式说明符,它将读取不超过10个字符的缓冲区。
dreamlax

5
当然-可以安全地使用API​​。也可以使用炸药安全清除花园里的污垢。但是我也不推荐,特别是因为有更安全的选择。
恢复莫妮卡·拉里·奥斯特曼

4
我父亲曾经用褐铁矿清理农场里的树木。您只需要了解您的工具并了解危险即可。
paxdiablo

1
@codaddict:某人不使用字段宽度的事实是该人scanf的问题,而不是scanf。这与所讨论的问题完全无关。毕竟这是C,而不是Java。
AnT

1
问题是scanf()必须在转换说明符中对字段宽度in进行硬编码。使用printf(),您可以*在转换说明符中使用并将长度作为参数传递。但是由于*表示中的内容有所不同scanf(),因此不起作用,因此您基本上必须像Alok在他的示例中那样为每个读取生成新的格式。它只会增加更多的工作和混乱;不妨使用fgets()并完成它。
约翰·博德2010年

4

这里的许多答案,讨论使用的潜在溢出的问题scanf("%s", buf),但最新的POSIX规范更多或更少的通过提供解决此问题m,可以在格式说明可用于分配,分配的角色cs[格式。这将允许scanf使用分配尽可能多的内存malloc(因此必须稍后使用释放它free)。

其用法示例:

这里。这种方法的缺点是它是POSIX规范中相对较新的新增功能,并且在C规范中完全没有指定,因此目前仍然相当不便。


3

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

  • 具有%s和%[转换说明符的缓冲区溢出的可能性。是的,您可以指定最大字段宽度,但是与不同printf(),您不能在scanf()调用中将其设为参数;必须在转化说明符中进行硬编码。
  • %d,%i等发生算术溢出的可能性
  • 检测和拒绝格式错误的输入的能力有限。例如,“ 12w4”不是有效的整数,但scanf("%d", &value);将成功转换并分配12至value,从而使“ w4”停留在输入流中,以阻止将来的读取。理想情况下,应拒绝整个输入字符串,但scanf()不会给您提供简便的机制。

如果您知道您的输入将始终使用固定长度的字符串和不会被溢出调情的数值构成,那么它scanf()就是一个很好的工具。如果您要处理交互式输入或不能保证格式正确的输入,请使用其他方法。


1
还有哪些其他合理的替代方法可以安全地读取固定长度的字符串和数值?
Rajkumar S

3

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

地狱,即使这是“罚款”:

它比printf类功能差,因为scanf需要一个指针,所以崩溃的可能性更大。

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


这更多的是历史问题,因为大多数现代编译器都会检查参数的类型是否与格式字符串中指定的参数匹配,如果不匹配,则会产生警告。但是,我敢肯定还有很多不是。
格雷姆

3

的优势scanf在于,一旦您学会了如何使用该工具(就像您在C中一样),它就会具有非常有用的用例。您可以scanf通过阅读和理解本手册来学习如何使用和与朋友。如果没有严重的理解问题无法通读该手册,则可能表明您不太了解C。


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

最大的劣势之一似乎纯粹是它在没有经验的人中赢得的声誉。。与C的许多有用功能一样,在使用它之前我们应该充分了解它。关键是要意识到,与C的其余部分一样,它看起来简洁明了,但是这可能会引起误解。这在C语言中很普遍;对于初学者来说,很容易编写他们认为有意义的代码,甚至可能一开始就可以为他们工作,但是却没有意义,并且可能导致灾难性的失败。

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

不提安全性不足和缓冲区溢出的风险,对这个问题有何反应?正如我们已经介绍的那样,C不是安全的语言,它将使我们走捷径,可能以牺牲正确性为代价来应用优化,或者更可能是因为我们是懒惰的程序员。因此,当我们知道系统将永远不会收到大于固定字节数的字符串时,我们就可以声明一个经过大小和前移边界检查的数组。我并不认为这是一个失败。这是一个选择。同样,强烈建议您阅读本手册,并将其告知我们。

懒惰的程序员并不是唯一受困的人scanf。例如,看到人们尝试使用读取值floatdouble值的情况并不少见%d。他们通常会误以为该实现会在幕后进行某种转换,这是有道理的,因为在该语言的其余部分中也会发生类似的转换,但事实并非如此。就像我之前说的,scanf朋友(甚至是C的其他成员)都是骗人的;它们看起来简洁而惯用,但事实并非如此。

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

很像fgets,当scanf和朋友无法阅读被告知阅读的内容时,信息流将处于异常状态; -在的情况下fgets,如果没有足够的空间来存储完整的行,则可能会将未读的其余行错误地当作是新行,而不是新行。-在scanf和朋友的情况下,转换失败,如上所述,错误的数据在流中未被读取,可能会被错误地视为它属于不同字段的一部分。

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

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

轻微的调整,通过分隔两个格式的代表和我们在这里看到了一些成功:scanf("%*[^\n]"); getchar();。尝试使用其他工具进行很少的按键操作;)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.