我想知道它的缺点scanf()
。
在许多站点中,我读到 usingscanf
可能会导致缓冲区溢出。这是什么原因?还有其他缺点scanf
吗?
到目前为止,大多数答案似乎都集中在字符串缓冲区溢出问题上。实际上,可以与scanf
函数一起使用的格式说明符支持显式字段宽度设置,这限制了输入的最大大小并防止缓冲区溢出。这使得流行的关于字符串缓冲区溢出危险的指控scanf
几乎毫无根据。声称scanf
在某种程度上类似于gets
这方面是完全不正确的。scanf
和gets
:之间有一个主要的质量差异,scanf
确实为用户提供了防止字符串缓冲区溢出的功能,而gets
没有。
有人可能会争辩说这些scanf
功能很难使用,因为字段宽度必须嵌入到格式字符串中(没有办法通过可变参数传递它,因为它可以在 中完成printf
)。这是真的。scanf
在这方面确实设计得很差。但是,任何scanf
关于字符串缓冲区溢出安全性的声明都是完全错误的,并且通常是由懒惰的程序员提出的。
真正的问题scanf
具有完全不同的性质,即使它也是关于溢出的。当scanf
函数用于将数字的十进制表示转换为算术类型的值时,它不提供算术溢出保护。如果发生溢出,scanf
会产生未定义的行为。因此,在 C 标准库中执行转换的唯一正确方法是使用strto...
族函数。
因此,综上所述,问题scanf
在于很难(尽管可能)正确和安全地使用字符串缓冲区。并且不可能安全地用于算术输入。后者才是真正的问题。前者只是一种不便。
PS 上面的意图是关于整个系列的scanf
功能(包括也fscanf
和sscanf
)。scanf
具体来说,明显的问题是使用严格格式的函数来读取潜在的交互式输入的想法是相当值得怀疑的。
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;
// 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]
来自comp.lang.c FAQ:为什么大家都说不要使用scanf?我应该改用什么?
scanf
有许多问题——见问题12.17、12.18a和12.19。此外,它的%s
格式也有同样的问题gets()
(见问题12.23)——很难保证接收缓冲区不会溢出。[脚注]更一般地说,
scanf
它是为相对结构化的格式化输入而设计的(它的名字实际上来源于“扫描格式化”)。如果你注意,它会告诉你它是成功还是失败,但它只能告诉你它失败的大致位置,而不是如何或为什么失败。您几乎没有机会进行任何错误恢复。然而交互式用户输入是最不结构化的输入。一个设计良好的用户界面将允许用户输入几乎任何内容——不仅仅是字母或标点符号,也可以是比预期更多或更少的字符,或者根本没有字符(即,只是 RETURN键),或过早的EOF,或任何东西。
scanf
使用;时要优雅地处理所有这些潜在问题几乎是不可能的。fgets
阅读整行(使用或类似的),然后使用sscanf
或其他一些技术来解释它们要容易得多。strtol
(类似,strtok
和的函数atoi
通常很有用;另请参阅问题12.16和13.6。)如果您确实使用了任何scanf
变体,请务必检查返回值以确保找到预期数量的项目。此外,如果您使用%s
,请务必防止缓冲区溢出。请注意,顺便说一句,对 的批评
scanf
不一定是对fscanf
和的控诉sscanf
。scanf
从 读取stdin
,这通常是一个交互式键盘,因此受到的限制最小,导致的问题最多。另一方面,当数据文件具有已知格式时,可能适合使用fscanf
. 解析字符串是非常合适的sscanf
(只要检查了返回值),因为它很容易重新获得控制,重新开始扫描,如果输入不匹配则丢弃等等。附加链接:
参考文献:K&R2 Sec。7.4 页。159
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
即使对于第一种情况,使用 with而不是读取字符串要简单得多scanf
。
是的你是对的。scanf
在读取字符串时, family( scanf
, sscanf
, ..etc) esp存在一个重大的安全漏洞fscanf
,因为它们没有考虑缓冲区的长度(它们正在读取的内容)。
例子:
char buf[3];
sscanf("abcdef","%s",buf);
显然缓冲区buf
可以容纳 MAX3
字符。但是sscanf
会尝试放入"abcdef"
它导致缓冲区溢出。
我与*scanf()
家人的问题:
printf()
,您不能将其作为调用中的参数scanf()
;它必须在转换说明符中进行硬编码。scanf("%d", &value);
会成功地将 12 转换并分配给value
,从而使“w4”卡在输入流中,从而破坏未来的读取。理想情况下,整个输入字符串都应该被拒绝,但scanf()
并没有给你一个简单的机制来做到这一点。 如果您知道您的输入总是会使用固定长度的字符串和不会溢出的数值,那么scanf()
它是一个很好的工具。如果您正在处理交互式输入或不能保证格式正确的输入,请使用其他内容。
这里的许多答案都讨论了 using 的潜在溢出问题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);
见这里。这种方法的缺点是它是 POSIX 规范中相对较新的补充,并且根本没有在 C 规范中指定,因此目前它仍然相当不可移植。
类函数存在一个大问题scanf
——缺乏任何类型安全性。也就是说,你可以这样编码:
int i;
scanf("%10s", &i);
地狱,即使这是“好”:
scanf("%10s", i);
它比printf
-like 函数更糟糕,因为scanf
需要一个指针,因此更有可能发生崩溃。
当然,有一些格式说明符检查器,但是,它们并不完美,它们不是语言或标准库的一部分。
的优点scanf
是一旦你学会了如何使用这个工具,就像你在 C 中应该做的那样,它有非常有用的用例。scanf
您可以通过阅读和理解手册了解如何使用和朋友。如果您在没有严重理解问题的情况下无法阅读该手册,这可能表明您不太了解 C。
scanf
和朋友们遭受了不幸的设计选择,这使得在不阅读文档的情况下很难(有时甚至不可能)正确使用,正如其他答案所显示的那样。不幸的是,这发生在整个 C 中,所以如果我建议不要使用,scanf
那么我可能会建议不要使用 C。
最大的缺点之一似乎纯粹是它在外行中赢得的声誉;与 C 的许多有用特性一样,我们在使用它之前应该充分了解它。关键是要意识到,与 C 的其余部分一样,它看起来简洁而惯用,但这可能会产生微妙的误导。这在 C 中很普遍;初学者很容易编写他们认为有意义的代码,甚至最初可能对他们有用,但没有意义并且可能会发生灾难性的失败。
例如,外行通常期望%s
委托会导致读取一行,虽然这可能看起来很直观,但不一定是正确的。将阅读的领域描述为一个词更合适。强烈建议您阅读手册以了解每个功能。
如果不提及它缺乏安全性和缓冲区溢出的风险,对这个问题的任何回应是什么?正如我们已经介绍过的,C 不是一种安全的语言,它允许我们偷工减料,可能会以牺牲正确性为代价进行优化,或者更可能是因为我们是懒惰的程序员。因此,当我们知道系统永远不会接收到大于固定字节数的字符串时,我们就可以声明一个该大小的数组并放弃边界检查。我真的不认为这是一个失败。这是一个选择。同样,强烈建议阅读手册,并会向我们揭示此选项。
懒惰的程序员并不是唯一被scanf
. 例如,看到人们试图阅读float
或double
使用 的值并不少见%d
。他们通常错误地认为实现将在幕后执行某种转换,这是有道理的,因为类似的转换发生在整个语言的其余部分,但这里不是这种情况。正如我之前所说,scanf
朋友(以及 C 的其余部分)都是骗人的;它们看起来简洁而惯用,但事实并非如此。
没有经验的程序员不必考虑操作是否成功。假设当我们告诉scanf
用户使用%d
. 我们可以拦截这种错误数据的唯一方法是检查返回值,我们多久检查一次返回值?
就像fgets
,当scanf
和朋友没有阅读他们被告知要阅读的内容时,流将处于异常状态;
- 在 的情况下fgets
,如果没有足够的空间来存储完整的行,则未读的行的其余部分可能会被错误地视为好像它不是新行。- 在scanf
和朋友的情况下,转换失败,如上所述,错误的数据在流中未被读取,并且可能被错误地视为不同字段的一部分。
使用起来并不容易scanf
,朋友们比使用fgets
. 如果我们通过在'\n'
使用时查找 afgets
或在使用 and friends 时检查返回值来检查是否成功scanf
,并且我们发现使用 读取了不完整的行fgets
或使用 读取字段失败scanf
,那么我们就是面对同样的现实:我们可能会丢弃输入(通常直到并包括下一个换行符)!呜呜呜!
不幸的是,scanf
两者同时使以这种方式丢弃输入变得困难(不直观)和容易(最少的击键)。面对这种丢弃用户输入的现实,一些人尝试过,没有意识到scanf("%*[^\n]%*c");
%*[^\n]
当代理只遇到换行符时会失败,因此换行符仍将留在流中。
稍作调整,通过分离两种格式代表,我们在这里看到了一些成功:scanf("%*[^\n]"); getchar();
. 尝试使用其他工具用很少的按键来做到这一点;)