scanf的缺点

我想知道scanf()的缺点。

在很多网站上,我读过使用scanf可能导致缓冲区溢出。 这是什么原因? scanf还有其他的缺点吗?

scanf的问题是(至less):

  • 使用%s从用户获取一个string,这可能导致string可能比您的缓冲区更长,导致溢出。
  • 扫描失败的可能性将文件指针留在不确定的位置。

我非常希望使用fgets来读取整行,以便限制读取的数据量。 如果你有一个1K的缓冲区,并且你用fgets读入一行,你可以通过这个事实来判断这个行是否过长,没有终止的换行符(文件的最后一行没有换行符)。

然后你可以向用户投诉,或者为剩余的线路分配更多的空间(如果有足够的空间,可以连续进行)。 无论哪种情况,都不存在缓冲区溢出的风险。

一旦你阅读了这一行,就知道你已经在下一行了,所以这里没有问题。 然后,您可以sscanf您的string到您的心脏的内容,而不必保存和恢复文件指针重新阅读。

下面是我经常用来确保在向用户询问信息时没有缓冲区溢出的一段代码。

如果需要的话,可以很容易地使用标准input以外的文件进行调整,您也可以在将其返回给调用者之前分配自己的缓冲区(并且继续增加它,直到足够大)(但调用者将负责为了解放它,当然)。

 #include <stdio.h> #include <string.h> #define OK 0 #define NO_INPUT 1 #define TOO_LONG 2 static int getLine (char *prmpt, char *buff, size_t sz) { int ch, extra; // Get line with buffer overrun protection. if (prmpt != NULL) { printf ("%s", prmpt); fflush (stdout); } 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. if (buff[strlen(buff)-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[strlen(buff)-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; } 

testing运行:

 $ ./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] 

到目前为止,大部分答案似乎都集中在string缓冲区溢出问题上。 实际上,可以与scanf函数一起使用的格式说明符支持显式字段宽度设置,这会限制input的最大大小并防止缓冲区溢出。 这使得scanf存在的string缓冲区溢出危险的stream行指责几乎毫无根据。 声称scanf在某种程度上类似于gets方面是完全不正确的。 scanfgets之间有着本质的区别: scanf为用户提供了防止string缓冲区溢出的function,而gets不能。

有人可能会说这些scanffunction很难使用,因为字段宽度必须embedded到格式string中(无法通过可变parameter passing,因为它可以在printf完成)。 那其实是真的 在这方面, scanfdevise确实相当糟糕。 但是,任何关于scanf在string缓冲区溢出安全性方面被破坏的声明完全是假的,通常由懒惰的程序员做出。

scanf的真正问题具有完全不同的性质,即使它也是溢出的 。 当使用scanf函数将数字的十进制表示转换为算术types的值时,它不提供算术溢出的保护。 如果发生溢出, scanf会产生未定义的行为。 出于这个原因,在C标准库中执行转换的唯一正确方法是从strto... family中的函数。

所以,总结一下上面的问题, scanf的问题在于使用string缓冲区很难(尽pipe可能)正确和安全的使用。 对于算术input是不可能的。 后者是真正的问题。 前者只是一个不便。

PS上面的意图是关于整个scanf函数族(包括fscanfsscanf )。 特别是使用scanf ,显而易见的问题是使用严格格式的函数读取潜在的交互式input的想法是相当可疑的。

从comp.lang.c FAQ: 为什么大家都说不要使用scanf? 我应该用什么来代替?

scanf有许多问题,请参阅问题12.17,12.18a和12.19 。 另外,它的%s格式与gets()有相同的问题(见问题12.23 ) – 它很难保证接收缓冲区不会溢出。 [脚注]

更一般地说, scanf被devise用于相对结构化,格式化的input(其名称实际上是从“扫描格式化”导出的)。 如果你注意,它会告诉你它是成功还是失败,但它可以告诉你只有大约失败的地方,而不是如何或为什么。 你几乎没有机会做任何错误恢复。

然而交互式用户input是最没有结构化的input。 一个精心devise的用户界面将允许用户input几乎任何东西的可能性 – 不仅仅是字母或标点符号,而且还包括比期望的更多或更less的字符,或者根本没有字符( ,仅仅是返回关键),或过早EOF,或任何东西。 使用scanf时几乎不可能处理所有这些潜在的问题; 读整行(用fgets或类似的东西)要容易得多,然后用sscanf或其他技术来解释它们。 (像strtolstrtokatoi这样的strtol通常是有用的;参见问题12.16和13.6) 。如果你使用任何scanf变体,一定要检查返回值以确保find预期的项目数。 另外,如果你使用%s ,一定要防止缓冲区溢出。

请注意,顺便说一句,对scanf批评不一定是fscanfsscanf起诉书。 scanfstdin读取,这通常是一个交互式键盘,因此是最less的限制,导致最多的问题。 当数据文件具有已知的格式时,另一方面,可以使用fscanf来读取它。 使用sscanfparsingstring(只要检查返回值)是非常合适的,因为重新获得控制权很容易,重新开始扫描,如果input不匹配则丢弃input等。

其他链接:

  • Chris Torek更长的解释
  • 你真正的更长的解释

参考文献:K&R2 Sec。 7.4 p。 159

你是对的。 在阅读一个string时, scanf家族( scanfsscanffscanf ..etc)尤其存在一个主要的安全漏洞,因为他们没有考虑到他们正在读取的缓冲区的长度。

例:

 char buf[3]; sscanf("abcdef","%s",buf); 

显然缓冲区buf可以保存MAX 3字符。 但sscanf将尝试将"abcdef"放入它导致缓冲区溢出。

scanf去做你想要的东西是非常困难的。 当然,你可以,但是像scanf("%s", buf); gets(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); 

上述情况下不能安全使用溢出。 即使是第一种情况,读取string对于fgets而不是使用scanf来说要简单得多。

我有*scanf()系列的问题:

  • 使用%s和%[转换说明符进行缓冲区溢出的可能性。 是的,您可以指定最大字段宽度,但与printf() ,您不能在scanf()调用中将其作为参数; 它必须在转换说明符中进行硬编码。
  • %d,%i等算术溢出的可能性
  • 有限的能力检测和拒绝形成不良的投入。 例如,“12w4”不是一个有效的整数,但是scanf("%d", &value); 将成功转换并赋值12,将“w4”留在inputstream中,以备未来阅读。 理想情况下,整个inputstring应该被拒绝,但是scanf()并没有给你一个简单的机制来做到这一点。

如果你知道你的input通常是固定长度的string和不会溢出的数值,那么scanf()就是一个很好的工具。 如果您处理的交互式input或input不能保证格式正确,请使用其他方法。

scanf的function存在一个大问题 – 缺less任何types的安全性。 也就是说,你可以这样编码:

 int i; scanf("%10s", &i); 

地狱,即使这是“好”:

 scanf("%10s", i); 

它比类似printf的函数更糟,因为scanf需要一个指针,所以崩溃更有可能。

当然,这里有一些格式说明检查器,但是这些检查器并不完美,而且它们不是语言或标准库的一部分。

这里有许多答案讨论了使用scanf("%s", buf)的潜在溢出问题,但是最新的POSIX规范或多或less的解决了这个问题,提供了一个m分配分配字符,可以在格式说明符中使用cs[格式。 这将允许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的优点是一旦你学习了如何使用这个工具,就像在C中一样,它有非常有用的用例。 您可以通过阅读和理解手册来学习如何使用scanf和朋友。 如果在没有认真理解的情况下你不能通读这本手册,这可能表明你不太了解C语言。


scanf和朋友遭受了不幸的deviseselect ,使其在阅读文档时无法正确使用(偶尔不可能),正如其他答案所显示的那样。 这发生在整个C,不幸的是,所以如果我build议不要使用scanf那么我可能会build议不要使用C.

最大的缺点之一似乎纯粹是在外行人员中赢得的声誉 ; 与C的许多有用的特性一样,我们在使用之前应该被告知。 关键是要认识到,与C的其余部分一样,它看起来简洁而习惯,但这可能会带来误导。 这在C中是普遍的; 初学者很容易编写他们认为有意义的代码,甚至可能最初为他们工作,但是没有意义,可能会造成灾难性的后果。

例如,不熟悉的人通常期望%s委托会引起一条线被读取,虽然这可能看起来很直观,但并不一定是正确的。 将字段描述为单词是比较合适的。 强烈build议每个function都阅读手册。

如果没有提到这个问题的答案是缺乏安全性和缓冲区溢出风险的话,会有什么回应呢? 正如我们已经介绍的那样,C语言并不是一种安全的语言,并且会让我们偷工减料,可能会以牺牲正确性为代价来应用优化,或者更可能因为我们是懒惰的程序员。 因此,当我们知道系统将永远不会收到大于固定数量的字节的string时,我们就可以声明一个数组,并放弃边界检查。 我真的不认为这是一个倒塌; 这是一个选项。 再次强烈build议阅读手册,并向我们揭示这一选项。

懒惰的程序员不是唯一被scanf蜇伤的程序员 。 例如,人们试图使用%d读取floatdouble值并不罕见。 他们通常会错误地认为,实现将在幕后进行某种转换,因为类似的转换会在整个语言中发生,但在这里并不是这样。 正如我刚才所说, scanf和朋友(事实上C的其余部分)都是骗人的。 他们看起来简洁而习惯,但却不是。

没有经验的程序员不会被迫考虑操作的成功 。 假设当我们告诉scanf使用%d读取和转换一个十进制数字序列时,用户input的内容完全是非数字的。 我们拦截这种错误数据的唯一方法是检查返回值,以及我们多久检查一次返回值?

就像fgets ,当scanf和朋友不能读取他们要读的内容时,这个stream将会处于一个不寻常的状态。 – 在fgets的情况下,如果没有足够的空间来存储一个完整的行,那么剩下的未读行可能会被错误地当作是一个新行。 – 在scanf和朋友的情况下,如上文所述,转换失败,错误的数据在stream上未被读取,并可能被错误地视为它是不同字段的一部分。

使用scanf和朋友比使用fgets要容易得多 。 如果我们在使用fgets时查找'\n' ,或者在使用scanf和朋友时检查返回值,我们发现我们已经使用fgets读取了一个不完整的行或未能读取我们面临着同样的现实:我们可能会放弃input (通常直到包括下一个换行符)。 Yuuuuuuck!

不幸的是, scanf同时以这种方式使input变得困难(非直观)和简单(最less击键)。 面对这种丢弃用户input的现实,有人曾尝试过scanf("%*[^\n]%*c"); ,没有意识到%*[^\n]委托在遇到除了换行符之外的任何东西时都会失败,因此换行符仍将留在stream中。

通过分离两个格式代表稍作修改,我们在这里看到一些成功: scanf("%*[^\n]"); getchar(); scanf("%*[^\n]"); getchar(); 。 尝试使用一些其他工具这么less的按键操作;)