我想知道scanf()
的缺点。
在许多网站中,我已经读过使用scanf
可能会导致缓冲区溢出。这是什么原因? scanf
还有其他任何缺点吗?
答案 0 :(得分:53)
到目前为止,大多数答案似乎都集中在字符串缓冲区溢出问题上。实际上,可以与scanf
函数一起使用的格式说明符支持显式字段宽度设置,这限制了输入的最大大小并防止缓冲区溢出。这使得scanf
中存在的字符串缓冲区溢出危险的流行指控几乎毫无根据。声称scanf
在某种程度上类似于gets
是完全错误的。 scanf
和gets
之间存在重大的质量差异:scanf
确实为用户提供了字符串缓冲区溢出防止功能,而gets
则没有。
有人可能会争辩说这些scanf
特征难以使用,因为字段宽度必须嵌入到格式字符串中(没有办法通过可变参数传递它,因为它可以在{{{ 1}})。这确实是事实。 printf
在这方面确实设计得很差。但是,尽管如此,scanf
在字符串缓冲区溢出安全性方面有些无可救药地被破坏的说法完全是假的,通常由懒惰的程序员做出。
scanf
的真正问题具有完全不同的性质,即使它也与溢出有关。当scanf
函数用于将数字的十进制表示转换为算术类型的值时,它不提供算术溢出的保护。如果发生溢出,scanf
会产生未定义的行为。因此,在C标准库中执行转换的唯一正确方法是来自scanf
系列的函数。
因此,总结一下上述问题,strto...
的问题在于尽管可能正确且安全地使用字符串缓冲区。并且不可能安全地用于算术输入。后者是真正的问题。前者只是给您带来不便。
P.S。以上内容旨在涵盖整个scanf
函数系列(包括scanf
和fscanf
)。特别是sscanf
,显而易见的问题是使用严格格式化的函数来读取潜在的交互式输入的想法是值得怀疑的。
答案 1 :(得分:52)
scanf的问题是(至少):
%s
从用户处获取字符串,这可能导致字符串可能比缓冲区长,从而导致溢出。我非常喜欢使用fgets
来读取整行,以便您可以限制读取的数据量。如果你有一个1K缓冲区,并且用fgets
读取了一行,你可以通过没有终止换行字符(尽管没有换行符的文件的最后一行)判断该行是否太长了
然后您可以向用户投诉,或者为线路的其余部分分配更多空间(如果有必要,可以持续留出足够的空间)。在任何一种情况下,都没有缓冲区溢出的风险。
一旦你读完了这一行,你知道你就在下一行,所以那里没有问题。然后,您可以{1}}将您的字符串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;
}
并且,它的测试驱动程序:
// 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;
}
最后,测试运行以显示它的实际效果:
$ ./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]
答案 2 :(得分:12)
来自comp.lang.c FAQ:Why does everyone say not to use scanf? What should I use instead?
scanf
存在许多问题 - 请参阅问题12.17,12.18a和12.19。此外,其%s
格式与gets()
具有相同的问题(请参阅问题12.23) - 很难保证接收缓冲区不会溢出。 [footnote]更一般地说,
scanf
是为相对结构化的格式化输入而设计的(其名称实际上源自“扫描格式化”)。如果你注意,它会告诉你它是成功还是失败,但它只能告诉你它大概失败的地方,而不是告诉你如何或为什么。您几乎没有机会进行任何错误恢复。然而,交互式用户输入是最少结构化的输入。精心设计的用户界面将允许用户输入几乎任何东西 - 不仅仅是字母或标点符号,而不仅仅是预期的数字,还有比预期更多或更少的字符,或者根本没有字符( ie < / em>,只是RETURN键),或过早的EOF,或任何东西。使用
scanf
时,几乎不可能优雅地处理所有这些潜在问题;读取整行(使用fgets
等)要容易得多,然后使用sscanf
或其他技术解释它们。 (strtol
,strtok
和atoi
等函数通常很有用;另请参阅问题12.16和13.6。)如果您使用任何scanf
} variant,请务必检查返回值以确保找到预期的项目数。此外,如果您使用%s
,请务必防止缓冲区溢出。顺便提一下,对
scanf
的批评不一定是对fscanf
和sscanf
的起诉。scanf
从stdin
读取,fscanf
通常是一个交互式键盘,因此受约束最少,导致大多数问题。另一方面,当数据文件具有已知格式时,使用sscanf
读取它可能是适当的。使用{{1}}解析字符串是完全合适的(只要检查返回值),因为它很容易重新获得控制,重新启动扫描,如果输入不匹配则丢弃输入等等。其他链接:
参考文献:K&amp; R2 Sec。 7.4 p。 159
答案 3 :(得分:5)
是的,你是对的。 scanf
系列(scanf
,sscanf
,fscanf
等等)esp在读取字符串时存在严重的安全漏洞,因为它们不会占用缓冲区(他们正在阅读)。
示例:
char buf[3];
sscanf("abcdef","%s",buf);
显然缓冲区buf
可以容纳MAX 3
个字符。但是sscanf
会尝试将"abcdef"
放入其中导致缓冲区溢出。
答案 4 :(得分:5)
很难让scanf
做你想做的事情。当然,你可以,但像scanf("%s", buf);
这样的事情和gets(buf);
一样危险,正如大家所说的那样。
作为一个例子,paxdiablo在他的阅读功能中所做的事情可以通过以下方式完成:
scanf("%10[^\n]%*[^\n]", buf));
getchar();
上面将读取一行,将前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;
}
scanf
的其他问题之一是溢出时的行为。例如,在阅读int
:
int i;
scanf("%d", &i);
以上不能在溢出的情况下安全使用。即使对于第一种情况,使用fgets
而不是scanf
来阅读字符串要简单得多。
答案 5 :(得分:3)
我与*scanf()
家庭的问题:
printf()
不同,您无法在scanf()
调用中将其作为参数;它必须在转换说明符中进行硬编码。scanf("%d", &value);
将成功转换并将12分配给value
,使“w4”卡在输入流中以填充将来的读数。理想情况下,应拒绝整个输入字符串,但scanf()
不会为您提供简单的机制。 如果您知道您的输入总是使用固定长度的字符串以及不会出现溢出的数值,那么scanf()
就是一个很好的工具。如果您正在处理无法保证格式良好的交互式输入或输入,请使用其他内容。
答案 6 :(得分:3)
这里的许多答案都讨论了使用scanf("%s", buf)
的潜在溢出问题,但最新的POSIX规范或多或少解决了这个问题,它提供了一个m
赋值分配字符,可以在格式中使用c
,s
和[
格式的说明符。这将允许scanf
根据需要为malloc
分配尽可能多的内存(因此必须稍后使用free
释放。)
使用它的一个例子:
char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.
// use buf
free(buf);
见here。这种方法的缺点是它是POSIX规范的一个相对较新的补充,并且它根本没有在C规范中指定,因此它现在仍然是相当不可移植的。
答案 7 :(得分:3)
scanf
存在一个大问题 - 类似功能 - 缺少任何类型的安全性。也就是说,您可以对此进行编码:
int i;
scanf("%10s", &i);
天哪,即使这是&#34;罚款&#34;:
scanf("%10s", i);
它比printf
更糟糕 - 就像函数一样,因为scanf
需要一个指针,所以崩溃的可能性更大。
当然,有一些格式规范的检查器,但是,那些不完美,很好,它们不是语言或标准库的一部分。
答案 8 :(得分:3)
scanf
的优点是,一旦您了解了如何使用该工具,就像您应该在C中一样,它具有非常有用的用例。您可以学习如何使用{{1}和朋友一起阅读和理解the manual。如果你在没有严重的理解问题的情况下无法通过该手册,这可能表明你不太了解C。
scanf
和朋友遭遇了不幸的设计选择,这使得在不阅读文档的情况下正确使用(有时甚至不可能),正如其他答案所示。不幸的是,这发生在整个C中,所以如果我建议不要使用scanf
,那么我可能会建议不要使用C.
其中一个最大的缺点似乎纯粹是在不知情的情况下获得的声誉;与C的许多有用功能一样,我们应该在使用它之前充分了解它们。关键是要认识到,与C的其余部分一样,它似乎简洁而惯用,但这可能会产生微妙的误导。这在C中很普遍;初学者很容易编写他们认为有意义的代码,甚至可能最初为他们工作,但没有意义,可能会灾难性地失败。
例如,不熟悉的人通常希望scanf
委托会导致一行被读取,虽然这看起来很直观,但不一定如此。将字段描述为 a word 更合适。强烈建议您阅读本手册。
对于这个问题的任何回应,如果没有提及它缺乏安全性和缓冲区溢出的风险,会怎样?正如我们已经涵盖的那样,C不是一种安全的语言,并且允许我们偷工减料,可能以牺牲正确性为代价来应用优化,或者更可能因为我们是懒惰的程序员。因此,当我们知道系统永远不会收到大于固定字节数的字符串时,我们就能够声明一个大小和放弃边界检查的数组。我并不认为这是一次堕落;这是一个选择。同样,强烈建议您阅读本手册,并向我们透露此选项。
懒惰的程序员不是唯一被%s
蜇伤的程序员。例如,看到人们尝试使用scanf
读取float
或double
值并不罕见。他们通常错误地认为实现将在幕后进行某种转换,这是有道理的,因为类似的转换发生在整个语言的其余部分,但事实并非如此。正如我先前所说,%d
和朋友(实际上是C的其余部分)具有欺骗性;它们似乎简洁而惯用,但它们不是。
没有经验的程序员不会被迫考虑操作的成功。假设当我们告诉scanf
使用scanf
读取和转换十进制数字序列时,用户输入完全非数字的内容。我们拦截这种错误数据的唯一方法是检查返回值,以及我们多久检查一次返回值?
与%d
非常相似,当fgets
和朋友未能阅读他们被告知阅读的内容时,该流将处于异常状态;
- 在scanf
的情况下,如果没有足够的空间来存储一条完整的行,那么未读的行的剩余部分可能被错误地处理,就好像它不是新行一样。
- 在fgets
和朋友的情况下,转换失败,如上所述,错误的数据在流上未被读取,可能被错误地视为不同字段的一部分。
使用scanf
和朋友并不比使用scanf
更容易。如果我们在使用fgets
时查找'\n'
或在我们使用fgets
和朋友时检查返回值来检查是否成功,我们发现我们已经阅读了使用scanf
不完整的行或使用fgets
无法读取字段,那么我们就面临同样的现实:我们可能丢弃输入(通常直到和包括下一个换行符)! Yuuuuuuck!
不幸的是,scanf
同时使得以这种方式丢弃输入变得困难(非直观)和容易(最少的键击)。面对丢弃用户输入的这种现实,有些人尝试过 ,没有意识到scanf
scanf("%*[^\n]%*c");
委托只遇到换行符时会失败,因此新行将会仍然留在溪流上。
稍微调整一下,将两个格式代表分开,我们在这里看到了一些成功:%*[^\n]
。尝试使用其他工具进行如此少的击键操作;)