我想知道的缺点scanf()
。
在许多站点中,我读到使用scanf
可能会导致缓冲区溢出。这是什么原因呢?还有其他缺点scanf
吗?
Answers:
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]
if (fgets (buff, sz, stdin) == NULL) return NO_INPUT;
您为什么使用它NO_INPUT
作为返回值?仅在错误时fgets
返回NULL
。
char *buf; scanf("%ms", &buf);
为您分配足够的空间malloc
(因此必须稍后释放),这将有助于防止缓冲区溢出。
getLine
with1
作为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
...来增强您的代码……
printf "\0" | exeName
以验证原始问题并修复。我想我从来没有检查过像这样的疯狂输入场景(但我该死的应该有)。感谢您的注意。
到目前为止,大多数答案似乎都集中在字符串缓冲区溢出问题上。实际上,可以与scanf
函数一起使用的格式说明符支持显式的字段宽度设置,这会限制输入的最大大小并防止缓冲区溢出。这使得人们普遍认为字符串缓冲区溢出危险scanf
几乎毫无根据。scanf
在某种程度上类似的说法是gets
完全错误的。scanf
和之间存在主要的质量差异gets
:scanf
确实为用户提供了防止字符串缓冲区溢出的功能,而gets
没有。
可以说这些scanf
功能难以使用,因为字段宽度必须嵌入格式字符串中(无法通过可变参数传递它,因为可以在中完成printf
)。确实是这样。scanf
在这方面确实设计得很差。但是,尽管如此scanf
,关于字符串缓冲区溢出安全性无可避免地被打破的任何主张都是完全虚假的,通常是由懒惰的程序员提出的。
真正的问题scanf
具有完全不同的性质,即使它也与溢出有关。当使用scanf
函数将数字的十进制表示形式转换为算术类型的值时,它无法防止算术溢出。如果发生溢出,scanf
则会产生未定义的行为。因此,在C标准库中执行转换的唯一正确方法是来自strto...
Family的函数。
因此,综上所述,问题scanf
在于难以(尽管可能)正确,安全地使用字符串缓冲区。而且,无法安全地用于算术输入。后者是真正的问题。前者只是一种不便。
PS以上内容旨在scanf
涵盖整个功能家族(包括fscanf
and sscanf
)。有了scanf
明确,明显的问题是,使用严格的格式化功能读取潜在的想法交互式输入是相当可疑的。
"123"
转换为number的内部整数表示形式123
。
scanf
在某种程度上类似于获得尊重的主张是完全错误的。” 我明白了,scanf
至少确实允许您指定最大字段大小,但是意识形态上的使用%s
肯定与存在相同的问题gets
,并且与C中的许多其他危险而有用的工具一样,它们都很容易被滥用。即使strtoul
有危险,所以不是暗示人们停止使用部分的C,我们不能只跳到暗示人们停止使用所有的C?
来自comp.lang.c常见问题解答:为什么每个人都说不使用scanf?我应该怎么用呢?
scanf
有一些问题,参见问题12.17,12.18a和12.19。同样,其%s
格式也存在相同的问题gets()
(请参见问题12.23)—很难保证接收缓冲区不会溢出。[脚注]更一般地说,
scanf
是为相对结构化的格式化输入而设计的(其名称实际上是从“扫描格式化”派生的)。如果您注意的话,它会告诉您它是成功还是失败,但是它只能告诉您它在哪里失败,而根本不告诉您如何或为什么。您几乎没有机会进行任何错误恢复。然而,交互式用户输入是其中结构最少的输入。精心设计的用户界面将允许用户键入几乎所有内容的可能性-不仅是预期数字时的字母或标点符号,而且比预期的字符更多或更少,或者根本没有字符(即,仅返回)键),或过早的EOF或其他任何内容。使用时几乎不可能优雅地处理所有这些潜在问题
scanf
;使用(fgets
或类似方法)阅读整行,然后解释它们,要容易得多sscanf
。(如strtol
,strtok
和atoi
等功能通常很有用;另请参见问题12.16和13.6。)如果确实使用任何scanf
变体,请确保检查返回值以确保找到了预期的项目数。另外,如果您使用%s
,请确保防止缓冲区溢出。顺便指出,对的批评
scanf
不一定是对fscanf
和的起诉sscanf
。scanf
从读取stdin
,通常是一个交互式键盘,因此受约束最少,导致最多的问题。另一方面,当数据文件具有已知格式时,最好使用进行读取fscanf
。解析字符串非常适合sscanf
(只要检查了返回值),因为它很容易重新获得控制权,重新启动扫描,如果输入不匹配则将其丢弃等。附加链接:
参考:K&R2第2节。7.4羽 159
scanf
做你想做的事情很难。当然可以,但是就像每个人都说过的scanf("%s", buf);
一样危险gets(buf);
。
例如,paxdiablo在阅读功能中的作用可以通过以下方式完成:
scanf("%10[^\n]%*[^\n]", buf));
getchar();
上面的代码将读取一行,将前10个非换行符存储在中buf
,然后丢弃所有内容,直到(包括)换行为止。因此,可以使用scanf
以下方式编写paxdiablo的功能:
#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
。
是的,你是对的。有一个重大的安全漏洞scanf
家庭(scanf
,sscanf
,fscanf
...等),ESP阅读字符串时,因为不拿缓冲区的长度(进入他们正在阅读)考虑在内。
例:
char buf[3];
sscanf("abcdef","%s",buf);
显然,缓冲区buf
可以容纳MAX 3
char。但sscanf
将尝试把"abcdef"
它,导致缓冲区溢出。
scanf
的问题,而不是scanf
。这与所讨论的问题完全无关。毕竟这是C,而不是Java。
scanf()
必须在转换说明符中对字段宽度in进行硬编码。使用printf()
,您可以*
在转换说明符中使用并将长度作为参数传递。但是由于*
表示中的内容有所不同scanf()
,因此不起作用,因此您基本上必须像Alok在他的示例中那样为每个读取生成新的格式。它只会增加更多的工作和混乱;不妨使用fgets()
并完成它。
这里的许多答案,讨论使用的潜在溢出的问题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()
家人有问题:
printf()
,您不能在scanf()
调用中将其设为参数;必须在转化说明符中进行硬编码。scanf("%d", &value);
将成功转换并分配12至value
,从而使“ w4”停留在输入流中,以阻止将来的读取。理想情况下,应拒绝整个输入字符串,但scanf()
不会给您提供简便的机制。 如果您知道您的输入将始终使用固定长度的字符串和不会被溢出调情的数值构成,那么它scanf()
就是一个很好的工具。如果您要处理交互式输入或不能保证格式正确的输入,请使用其他方法。
scanf
类功能有一个大问题-缺乏任何类型的安全性。也就是说,您可以编写以下代码:
int i;
scanf("%10s", &i);
地狱,即使这是“罚款”:
scanf("%10s", i);
它比printf
类功能差,因为scanf
需要一个指针,所以崩溃的可能性更大。
当然,这里有一些格式说明符检查器,但是这些检查器并不完美,而且还不是语言或标准库的一部分。
的优势scanf
在于,一旦您学会了如何使用该工具(就像您在C中一样),它就会具有非常有用的用例。您可以scanf
通过阅读和理解本手册来学习如何使用和与朋友。如果没有严重的理解问题无法通读该手册,则可能表明您不太了解C。
scanf
和其他朋友一样,他们遭受了不幸的设计选择,这使得难以(有时甚至是不可能)不阅读文档而正确使用(如其他答案所示)。不幸的是,这会在整个C中发生,因此,如果我建议您不要使用C,scanf
那么我可能会建议您不要使用C。
最大的劣势之一似乎纯粹是它在没有经验的人中赢得的声誉。。与C的许多有用功能一样,在使用它之前我们应该充分了解它。关键是要意识到,与C的其余部分一样,它看起来简洁明了,但是这可能会引起误解。这在C语言中很普遍;对于初学者来说,很容易编写他们认为有意义的代码,甚至可能一开始就可以为他们工作,但是却没有意义,并且可能导致灾难性的失败。
例如,没有经验的人通常期望%s
委托会导致一行被读取,虽然看起来很直观,但不一定是正确的。将字段描述为单词更合适。强烈建议您阅读每种功能的手册。
不提安全性不足和缓冲区溢出的风险,对这个问题有何反应?正如我们已经介绍的那样,C不是安全的语言,它将使我们走捷径,可能以牺牲正确性为代价来应用优化,或者更可能是因为我们是懒惰的程序员。因此,当我们知道系统将永远不会收到大于固定字节数的字符串时,我们就可以声明一个经过大小和前移边界检查的数组。我并不认为这是一个失败。这是一个选择。同样,强烈建议您阅读本手册,并将其告知我们。
懒惰的程序员并不是唯一受困的人scanf
。例如,看到人们尝试使用读取值float
或double
值的情况并不少见%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();
。尝试使用其他工具进行很少的按键操作;)
scanf()
。