AnT*_*AnT 56
到目前为止,大多数答案似乎都集中在字符串缓冲区溢出问题上.实际上,可以与scanf
函数一起使用的格式说明符支持显式字段宽度设置,这限制了输入的最大大小并防止缓冲区溢出.这使得字符缓冲区溢出危险的流行指控scanf
几乎毫无根据.声称scanf
在某种程度上类似于gets
尊重是完全错误的.scanf
和之间存在一个主要的质量差异gets
:scanf
确实为用户提供了字符串缓冲区溢出防止功能,而gets
不是.
可以说这些scanf
特征难以使用,因为字段宽度必须嵌入到格式字符串中(没有办法通过可变参数传递它,因为它可以在其中完成printf
).这确实是事实.scanf
在这方面确实设计得很差.但是scanf
,对于字符串缓冲区溢出安全性而言,任何在某种程度上无可救药地破坏的声明都是完全虚假的,通常是由懒惰程序员完成的.
真正的问题scanf
具有完全不同的性质,即使它也是溢出的.当scanf
函数用于将数字的十进制表示转换为算术类型的值时,它不提供算术溢出的保护.如果发生溢出,则scanf
产生未定义的行为.因此,在C标准库中执行转换的唯一正确方法是来自strto...
family的函数.
因此,总结一下上面的问题scanf
是,使用字符串缓冲区很难(虽然可能)正确安全地使用.并且不可能安全地用于算术输入.后者是真正的问题.前者只是给您带来不便.
PS以上内容旨在介绍整个scanf
功能系列(包括fscanf
和sscanf
).有了scanf
明确,明显的问题是,使用严格的格式化功能读取潜在的想法交互式输入是相当可疑的.
pax*_*blo 53
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;
// 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;
}
Run Code Online (Sandbox Code Playgroud)
而且,它的测试驱动程序:
// 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;
}
Run Code Online (Sandbox Code Playgroud)
最后,测试运行以显示它的运行情况:
$ ./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]
Run Code Online (Sandbox Code Playgroud)
jam*_*lin 13
来自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 p.159
scanf
要做你想做的事很难.当然,你可以,但是像所有人都说的scanf("%s", buf);
那样危险gets(buf);
.
作为一个例子,paxdiablo在他的阅读函数中所做的事情可以通过以下方式完成:
scanf("%10[^\n]%*[^\n]", buf));
getchar();
Run Code Online (Sandbox Code Playgroud)
上面将读取一行,存储前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;
}
Run Code Online (Sandbox Code Playgroud)
其中一个问题scanf
是它在溢出时的行为.例如,阅读时int
:
int i;
scanf("%d", &i);
Run Code Online (Sandbox Code Playgroud)
如果溢出,上述情况不能安全使用.即使对于第一种情况,读取字符串也更简单,fgets
而不是使用scanf
.
是的,你是对的.有一个重大的安全漏洞scanf
家庭(scanf
,sscanf
,fscanf
ESP阅读字符串时,因为不拿缓冲区的长度(进入他们正在阅读)进去..等).
例:
char buf[3];
sscanf("abcdef","%s",buf);
Run Code Online (Sandbox Code Playgroud)
显然缓冲区buf
可以容纳MAX 3
char.但是sscanf
会试图"abcdef"
加入它导致缓冲区溢出.
优点是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
或在使用scanf
和朋友时检查返回值来检查是否成功,并且发现使用 读取了不完整的行fgets
或使用 读取字段失败scanf
,那么我们面临同样的现实:我们可能会丢弃输入(通常直到并包括下一个换行符)!呜呜呜!
不幸的是,scanf
两者同时使得以这种方式丢弃输入既困难(不直观)又容易(最少的击键)。面对丢弃用户输入的现实,有些人尝试过,但没有意识到scanf("%*[^\n]%*c");
%*[^\n]
当委托只遇到换行符时就会失败,因此换行符仍将保留在流中。
稍作调整,通过分离两种格式委托,我们在这里看到了一些成功:scanf("%*[^\n]"); getchar();
。尝试使用其他工具通过很少的击键来完成此操作;)
归档时间: |
|
查看次数: |
47482 次 |
最近记录: |