在 C 中读取用户输入的陷阱:关于 scanf 和 Stdin 的故事
Source: Dev.to
C 中读取用户输入的陷阱:关于 scanf 与 stdin 的故事
我最近需要写一段 C 代码,从 stdin 读取输入,忽略换行符,丢弃超出缓冲区的内容,并在循环中重复此操作。
初始尝试
#include <stdio.h>
void take_input(void)
{
char buf[20];
printf("> ");
scanf("%19[^\n]", buf);
printf("input=`%s`\n", buf);
}
int main(void)
{
for (int i = 0; i < 5; ++i) {
take_input();
}
return 0;
}
$ ./main
> hello world↵
input=`hello world`
> input=`hello world`
> input=`hello world`
> input=`hello world`
> input=`hello world`
$ █
它只读取了一次字符串,却把同一个值打印了五次。为什么?
栈行为
虽然 char buf[20] 是局部变量,但每次调用 take_input() 时使用的正是同一个栈内存地址。
/* ... */
printf("address=%p, input=`%s`\n", (void*)buf, buf);
/* ... */
$ ./main
> hello world
address=0x7ffcb96c49d0, input=`hello world`
> address=0x7ffcb96c49d0, input=`hello world`
> address=0x7ffcb96c49d0, input=`hello world`
...
因为缓冲区没有被初始化,后续的 scanf 调用未能覆盖它,旧内容(“幽灵数据”)保持不变。用零初始化缓冲区可以解决这个特定问题:
char buf[20] = {0};
现在每次缓冲区都会被重置,但 scanf 仍然表现异常:
$ ./main
> hello world
address=0x7ffea5c2d840, input=`hello world`
> address=0x7ffea5c2d840, input=``
> address=0x7ffea5c2d840, input=``
...
真正的问题:%[^\n] 并不消费换行符
扫描集 %19[^\n] 读取最多 19 个 不是 换行符的字符,但它会把换行符留在输入流中。
用 GDB 检查 stdin 时可以看到换行符仍然待处理:
$ gcc -g3 -O0 main.c -o main
$ gdb main
(gdb) break 8
Breakpoint 1 at 0x11d3: file main.c, line 8.
(gdb) run
...
> hello world↵
...
(gdb) p *stdin
$1 = {
_flags = -72539512,
_IO_read_ptr = 0x55555555972b "\n",
_IO_read_end = 0x55555555972c "",
_IO_read_base = 0x555555559720 "hello world\n",
}
stdin->_IO_read_ptr 仍指向换行符。当循环再次运行时,scanf("%19[^\n]", buf) 立刻看到换行符,匹配到零字符并中止。缓冲区保持为空(或全零),循环重复相同的输出。
可行的解决方案
1. 使用额外的 scanf 调用
强制消费剩余的行以及换行符:
void take_input(void)
{
char buf[20];
printf("> ");
scanf("%19[^\n]", buf);
/* 丢弃行的其余部分(如果超过 19 个字符) */
scanf("%*[^\n]");
/* 丢弃换行符 */
scanf("%*c");
printf("input=`%s`\n", buf);
}
看起来有点乱,但它在空白、短输入以及被截断的长输入情况下都能可靠工作:
$ ./main
> hey
input=`hey`
> hello world
input=`hello world`
> a b c d e f g h i j k l m o p q r s t u v w x y z
input=`a b c d e f g h i j k l m o p q r s t u v w x y z`
> /* 5 个空格 */
input=` `
> abcdefghijklmnpoqrstuvwxyz
input=`abcdefghijklmnpoqrs`
2. fgets —— 标准做法
更可预测、更明确的办法是使用 fgets 读取整行,然后去除换行符:
#include <stdio.h>
#include <string.h>
void take_input_2(void)
{
char buf[20];
printf("> ");
if (fgets(buf, sizeof(buf), stdin)) {
char *newline_ptr = strchr(buf, '\n');
if (newline_ptr) {
/* 用 \0 替换 \n 进行修剪 */
*newline_ptr = '\0';
} else {
/* 未找到换行符 —— 输入被截断;
继续消费该行的其余部分 */
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
}
printf("input=`%s`\n", buf);
}
3. getline —— 堆上方式
getline(POSIX,非 ISO C)读取整行,并在堆上分配(或重新分配)缓冲区。使用完后必须自行释放内存:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void take_input_3(void)
{
char *line = NULL; /* 初始化指针 */
size_t cap = 0; /* 容量,由 getline 更新 */
ssize_t n;
printf("> ");
n = getline(&line, &cap, stdin);
if (n > 0) {
/* 删除尾部换行符 */
if (line[n-1] == '\n') {
line[n-1] = '\0';
}
printf("input=`%s`\n", line);
}
free(line); /* 始终释放堆内存 */
}
结论
这次练习提醒我们,C 中的输入处理出奇地棘手。scanf 看似方便,但在遇到空白和换行符的边界情况时,“便利”会变成负担。
- 对于可预测、可移植、以行为单位且需要截断处理的输入,
fgets几乎总是更好的选择。 - 如果你可以使用 POSIX 并且愿意在堆上分配,
getline提供了更大的灵活性。 - 将
scanf留给那些你完全了解输入格式的场景。