我应该在 c 中使用有限输入的 fgets 还是 scanf?

Should I use fgets or scanf with limited input in c?

我应该使用 fgets 还是像 scanf("%10s", foo) 那样格式化 scanf

除了scanf不读取空白字符,这可以解决并用scanset做更多的东西,那为什么我应该使用fgets而不是scanf

如有任何帮助,我们将不胜感激。


编辑

我还想问一件事:即使我们使用 fgets 如果用户输入的字符超过边界(我的意思是很多字符)会发生什么情况,是否会导致缓冲区溢出?那怎么处理呢?

这是一个经常讨论的话题,意见很多但有趣 none 较少。我观察到,大多数已经在本网站上回答过类似问题的人都站在 fgets() 一边。我是他们其中的一员。除了少数例外,我发现 fgets()scanf() 更适合用于用户输入。 scanf() 被许多人认为是 sub-optimal method for handling user input。例如

"...it will tell you whether it succeeded or failed, but can tell you only approximately where it failed, and not at all how or why. You have very little opportunity to do any error recovery."
(jamesdlin). But in the interest of attempting balance, will start off citing this discussion.

对于来自 stdin 的用户输入,即键盘输入,fgets() 将是更好的选择。更宽容的是,它读取的字符串可以在尝试转换之前得到完全验证

使用 scanf() 形式的少数情况之一:fscanf() 可以在转换时使用来自非常受控的源的输入,即来自读取具有重复可预测字段的严格格式化的文件。

为了进行更多讨论,this comparison 突出显示了两者的其他优点和缺点。

编辑:解决 OP 关于溢出的附加问题:

"One more thing I want to ask is: even when we use fgets what happen if user enter characters more than boundary (I mean a lot of characters), does it lead to buffer overflow? Then how to deal with it?"

[fgets()](https://www.tutorialspoint.com/c_standard_library/c_function_fgets.htm 的设计很好,可以防止缓冲区溢出,只需正确使用其参数即可,例如:

char buffer[100] = {0};
...
while fgets(buffer, sizeof buffer, stdin);  

这可以防止处理大于缓冲区大小的输入,从而防止溢出。

即使使用 scanf() 防止缓冲区溢出也非常简单:在格式字符串中使用 width specifier。例如,如果您想读取输入并将用户的输入大小限制为最多 100 个字符,则代码将包括以下内容:

char buffer[101] = {0};// includes space for 100 + 1 for NULL termination

scanf("%100s", buffer);
        ^^^  width specifier 

但是对于数字,使用 scanf() 溢出不是很好。为了演示,请使用这个简单的代码,输入每个 运行:

注释中指示的两个值
int main(void)
{
    int val = 0;
    // test with 2147483647 & 2147483648
    scanf("%d", &val);
    printf("%d\n", val);
    
    return 0;
}

对于第二个值,我的系统抛出以下内容:

NON-FATAL RUN-TIME ERROR: "test.c", line 11, col 5, thread id 22832: Function scanf: (errno == 34 [0x22]). Range error `

这里您需要读入一个字符串,然后使用 strto_() 函数之一进行字符串到数字的转换:strtol(), strtod(), ...)。两者都包括在 之前 导致 运行 次警告或错误的溢出测试的能力。请注意,使用 atoi()atod() 也不会防止溢出。

在大多数操作系统上,默认情况下用户输入是基于行的。这样做的原因之一是允许用户在将输入发送到程序之前按退格键更正输入。

对于基于行的用户输入,程序一次读取一行输入是有意义和直观的。这就是函数 fgets 所做的(前提是缓冲区足够大以存储整行输入)。

函数scanf, on the other hand, normally does not read one line of input at a time. For example, when you use the %s or %d conversion format specifier with scanf, it will not consume an entire line of input. Instead, it will only consume as much input as matches the conversion format specifier. This means that the newline character at the end of the line will normally not be consumed (which can easily lead to programming bugs)。此外,使用 %d 转换格式说明符调用的 scanf 会将 6sldf23dsfh2 等输入视为数字 6 的有效输入,但对 scanf 的任何进一步调用使用相同的说明符将失败,除非您从输入流中丢弃该行的其余部分。

在处理基于行的用户输入时,scanf 的这种行为是违反直觉的,而 fgets 的行为是直观的。

使用fgets后,可以在字符串上使用函数sscanf,解析单行的内容。这将允许您继续使用扫描集。或者您可以通过其他方式解析该行。无论哪种方式,只要您使用 fgets 而不是 scanf 来读取输入,您将一次处理一行输入,这是处理 line- 的自然而直观的方式基于用户输入。

When we use fgets what happen if user enter characters more than boundary (I mean a lot of characters), does it lead to buffer overflow? Then how to deal with it?

如果用户输入的字符多于第二个 fgets 函数参数指定的缓冲区中的字符数,则它不会溢出缓冲区。相反,它只会从输入流中提取适合缓冲区的尽可能多的字符。您可以通过检查字符串末尾是否包含换行符'\n'来确定是否读取了整行。

例如,如果您有一个声明为

的字符数组
char s[100];

并想读取包含嵌入 space 的字符串,那么您可以按以下方式使用 scanf

scanf( "%99[^\n]", s );

fgets喜欢:

fgets( s, sizeof( s ), stdin );

这两个调用的区别在于scanf的调用不从输入缓冲区中读取换行符'\n'。而 fgets 如果字符数组中有足够的 space 则读取换行符 '\n'

要删除使用fgets后存储在字符数组中的换行符'\n',您可以这样写:

s[ strcspn( s, "\n" ) ] = '[=13=]';

如果输入字符串超过 99 个字符,则两个调用都只读取 99 个字符,并在字符序列后附加终止零字符 '[=26=]'。所有剩余的字符将仍在输入缓冲区中。

fgets 有问题。例如,如果在 fgets 之前使用 scanf,例如:

scanf( "%d", &x );
fgets( s, sizeof( s ), stdin );

并且用户输入是:

10
Hello World

那么fgets的调用将只读取换行符'\n',当scanf的调用中的整数值按下回车键后存储在缓冲区中已阅读。

在这种情况下,您需要编写一段代码,在调用 fgets.

之前删除换行符 '\n'

您可以通过以下方式执行此操作:

scanf( "%d", &x );
scanf( " " );
fgets( s, sizeof( s ), stdin );

如果你使用的是scanf那么在这种情况下你可以这样写:

scanf( "%d", &x );
scanf( " %99[^\n]", s );
       ^^ 

到目前为止,这里的所有答案都呈现了 scanffgets 的复杂性,但我认为值得一提的是,这两个函数在当前的 C 标准中都已弃用。 Scanf 特别危险,因为它存在缓冲区溢出等各种安全问题。 fgets 问题不大,但根据我的经验,它在实践中往往有点笨拙且不太有用。

事实是,您通常并不知道用户输入的时间有多长。您可以通过将 fgets 与 一起使用来解决这个问题,我希望这将足够大 缓冲区,但这并不是很优雅。相反,您通常想要做的是拥有动态缓冲区,该缓冲区将增长到足够大以存储将提供的任何用户输入。这就是 getline 函数发挥作用的时候。它用于从用户那里读取任意数量的字符,直到遇到\n。本质上,它将整行作为字符串加载到您的内存中。

size_t getline(char **lineptr, size_t *n, FILE *stream);

此函数将指向动态分配字符串的指针作为第一个参数,和指向已分配缓冲区大小的指针作为第二个参数和 stream 作为第三个参数。 (您基本上会将 stdin 放在那里用于命令行输入)。并且 returns 读取字符数,包括末尾的 \n,但不包括终止 null。

在这里,您可以看到这个函数的用法示例:

int main() {

printf("Input Something:\n");  // asking user for input

size_t length = 10;                   // creating "base" size of our buffer
char *input_string = malloc(length);  // allocating memory based on our initial buffer size
size_t length_read = getline(&input_string, &length, stdin);  // loading line from console to input_string
// now length_read contains how much characters we read
// and length contains new size of our buffer (if it changed during the getline execution)

printf("Characters read (including end of line but not null at the end)"
       ": %lu, current size of allocated buffer: %lu string: %s"
       , length_read, length, input_string);

free(input_string);    // like any other dynamically-allocated pointer, you must free it after usage
return 0;
}

当然,使用此函数需要 C 语言中指针和动态内存的基本知识,但是 getline 稍微复杂的性质绝对值得,因为它提供了安全性和灵活性。

您可以在以下网站上阅读有关此函数以及 C 中可用的其他输入函数的更多信息:https://www.studymite.com/blog/strings-in-c 我相信它很好地总结了 C 输入的复杂性。