我想了解 scanf()
的缺点。
在许多网站上,我都看到过使用 scanf
可能会导致缓冲区溢出。这是什么原因?scanf
还有其他缺点吗?
我想了解 scanf()
的缺点。
在许多网站上,我都看到过使用 scanf
可能会导致缓冲区溢出。这是什么原因?scanf
还有其他缺点吗?
scanf
函数配合使用的格式说明符支持显式字段宽度设置,这限制了输入的最大大小并防止了缓冲区溢出。这使得普遍指责scanf
存在字符串缓冲区溢出危险的说法几乎毫无根据。声称scanf
在这方面与gets
类似是完全不正确的。scanf
和gets
之间存在重大的定性差异:scanf
提供了用户防止字符串缓冲区溢出的功能,而gets
则没有。scanf
特性难以使用,因为字段宽度必须嵌入到格式字符串中(无法通过变参来传递,如printf
中所做的那样)。这实际上是真的。在这方面scanf
的设计确实相当糟糕。但是任何关于scanf
在字符串缓冲区溢出安全方面已经彻底失效的说法都是完全错误的,通常是由懒惰的程序员提出的。
scanf
的真正问题完全不同,即使它也涉及到溢出。当scanf
函数用于将数字的十进制表示转换为算术类型的值时,它不提供从算术溢出中保护的保障。如果发生溢出,scanf
会产生未定义行为。因此,在C标准库中执行转换的唯一正确方式是使用strto...
系列函数。scanf
的问题在于使用字符串缓冲区时难以(尽管可能)正确且安全地使用。而对于算术输入来说,它无法安全使用。后者才是真正的问题。前者只是一个不便之处。注:上述内容是关于scanf
函数族的,包括fscanf
和sscanf
。对于scanf
函数来说,显而易见的问题是,使用一种严格格式化的函数来读取可能是互动式输入的想法本身就值得商榷。
scanf
在某些方面类似于gets
是完全不正确的。我知道,scanf
至少允许您指定最大字段大小,但是使用%s
的意识形态肯定与gets
具有相同的问题,并且与C中许多其他危险但有用的工具一样,它们都很容易被滥用。即使是strtoul
也有其危险性,因此,与其建议人们停止使用C的部分,不如建议人们停止使用C的所有部分,这样更好吧? - autistic#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]
char *buf; scanf("%ms", &buf);
,它将使用malloc
为您分配足够的空间(因此必须稍后释放),这有助于防止缓冲区溢出。 - dreamlaxsz
参数设置为1,调用getLine
会发生什么?问题出现在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
来增强... - autisticprintf "\0" | exeName
进行测试以验证原始问题和修复。我想我从未检查过像那样疯狂的输入场景(但我应该做到)。感谢您的提醒。 - paxdiablo来自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 Sec. 7.4 p. 159
要让scanf
做你想要的事情非常困难。当然,你可以这样做,但是像scanf(“%s”,buf);
这样的东西和gets(buf);
一样危险,正如每个人所说的那样。
例如,paxdiablo在他的读取函数中所做的事情可以使用类似以下代码来完成:
scanf("%10[^\n]%*[^\n]", buf));
getchar();
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
更简单。
是的,您说得对。在scanf
家族(scanf
、sscanf
、fscanf
等)中存在一个严重的安全漏洞,特别是在读取字符串时,因为它们没有考虑缓冲区的长度(它们要读入的缓冲区)。
例如:
char buf[3];
sscanf("abcdef","%s",buf);
显然缓冲区buf
最多只能容纳3
个字符。但是sscanf
将尝试将"abcdef"
放入其中,导致缓冲区溢出。
scanf
时没有使用字段宽度,这是该人的问题,而不是scanf
的问题。这与问题本身完全无关。毕竟这是C语言,而不是Java。 - AnT stands with Russiascanf()
中的字段宽度必须在转换说明符中硬编码;而在printf()
中,您可以在转换说明符中使用*
并将长度作为参数传递。但由于*
在scanf()
中意味着不同的东西,所以这种方法行不通,因此您基本上必须像Alok在他的示例中那样为每个读取生成一个新格式。这只会增加更多的工作和混乱;最好使用fgets()
并完成它。 - John Bodescanf
的优点是,一旦你学会了如何使用它——正如在C语言中应该做的那样——它有非常实用的用途。 你可以通过阅读和理解手册来学习如何使用scanf
及其相关工具。如果你无法顺利理解该手册,那可能意味着你对C语言并不十分熟悉。
scanf
和其它类似函数的设计存在不幸的选择,这使得在没有阅读文档的情况下正确使用它变得困难(有时甚至是不可能的),正如其他答案所示。不幸的是,这种情况在C中普遍存在,因此如果我建议不要使用scanf
,那么我可能会建议不要使用C。
最大的缺点之一似乎纯粹是它在未接受培训的人中所获得的声誉;与C的许多有用特性一样,在使用它之前我们应该了解清楚。关键是要认识到,与C的其余部分一样,它看起来简洁而习惯,但这可能会产生微妙的误导。这在C中是无处不在的;初学者很容易编写他们认为有意义并且可能在最初能够工作的代码,但这些代码实际上是没有意义的,并且可能会发生灾难性的错误。%s
指令会导致读取一行,虽然这似乎很直观,但这并不一定是正确的。更恰当的描述是读取一个单词。强烈建议每个函数都阅读其手册。懒惰的程序员不是唯一被scanf
伤害的人。常见的情况是看到人们试图使用%d
来读取float
或double
的值,他们通常错误地认为实现会在幕后执行某种转换,这是有道理的,因为类似的转换在语言的其他部分中也会发生,但在这里不是这样。正如我之前所说的,scanf
和它的伙伴们(以及C的其余部分)都具有欺骗性;它们看起来简洁而且符合惯例,但实际上并不是这样的。
scanf
使用%d
读取和转换一系列十进制数字。我们唯一能拦截这种错误数据的方法是检查返回值,但我们有多少次会费心检查返回值呢?fgets
类似,当scanf
等函数无法读取所需内容时,流将处于异常状态。
fgets
,如果没有足够的空间来存储完整行,则未读取的剩余部分可能会被错误地视为新行。scanf
及其相关函数,如上所述,转换失败后,错误数据将保留在流中,可能会被错误地视为不同字段的一部分。scanf
和相关函数并不比使用fgets
更容易。如果我们在使用fgets
时通过查找'\n'
或者在使用scanf
及其相关函数时检查返回值发现读取了不完整的行或无法读取字段,则面临同样的现实:我们很可能会丢弃输入(通常是直到下一个换行符为止)!太恶心了!scanf
既使得这种方式的丢弃输入变得困难(不直观),又使它变得容易(最少的按键)。面对这种丢弃用户输入的现实,有些人尝试过scanf("%*[^\n]%*c");
%*[^\n]
委托遇到只有换行符时会失败,因此换行符仍然会留在流中。scanf("%*[^\n]"); getchar();
。尝试使用其他工具进行如此少的按键操作 ;)
scanf
等函数存在一个大问题-缺乏任何类型安全。也就是说,你可以编写以下代码:
int i;
scanf("%10s", &i);
scanf("%10s", i);
与 printf
类似的函数相比,scanf
更糟糕,因为它期望一个指针,所以崩溃的可能性更大。
当然,有一些格式说明符检查器存在,但是它们并不完美,并且它们不属于语言或标准库的一部分。
我对*scanf()
系列有以下问题:
printf()
不同的是,在scanf()
调用中,你不能将其作为参数传递;必须在转换说明符中硬编码。scanf("%d", &value);
会成功地转换并分配12给value
,留下“w4”卡在输入流中,以污染未来的读取。理想情况下,整个输入字符串应该被拒绝,但scanf()
没有提供简单的机制来实现这一点。如果你知道你的输入总是格式良好的固定长度字符串和不会引起溢出的数字值,那么scanf()
是一个很好的工具。如果你处理交互式输入或不能保证格式良好的输入,则使用其他方法。
scanf("%s", buf)
可能会导致溢出问题,但最新的POSIX规范基本上通过为c
、s
和[
格式说明符提供一个m
赋值分配字符来解决了这个问题。这将允许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()
。 - Jonathan Leffler