工具函数------清空输入缓冲区

原始代码
c
void clear_input(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
逐行要点
int c:必须用int,getchar()返回EOF(-1),用char接会被截断 → 死循环;(c = getchar()):括号不能省,!=优先级高于=;!= '\n' && != EOF:两个都要判,文件重定向时只判\n会死循环;- 循环体为空:目的就是"吃掉"字符。
使用场景
场景 1:scanf 失败后的必做动作
c
#include <stdio.h>
void clear_input(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
int main(void)
{
int x;
printf("请输入一个整数:");
if (scanf("%d", &x) != 1) {
clear_input(); /* 关键:清掉垃圾输入 */
printf("输入无效,程序退出。\n");
return 1;
}
printf("你输入的是:%d\n", x);
return 0;
}


运行演示
请输入一个整数:abc
输入无效,程序退出。
请输入一个整数:42
你输入的是:42
注释掉 clear_input() 会怎样?
单独运行看不出问题(程序直接退出了)。但把它放进循环,就会立刻暴露------见场景 2。
' 核心' :安全读取整数,输入无效时清空缓冲区并退出。
要点:
-
clear_input():循环getchar()直到'\n'或EOF,丢弃当前行剩余垃圾字符。c用int,因为getchar()可能返回EOF。 -
scanf("%d", &x)返回值:1:成功读到一个整数0:输入不是合法整数EOF:输入结束或错误
所以!= 1表示失败。
-
失败后必须清缓冲区:非法字符仍留在输入流中,不清会影响后续读取。
-
return 1表示异常退出,return 0表示正常结束。 -
局限:输入
123abc时,scanf会成功读走123,留下abc。更稳健可用fgets+sscanf或strtol。
场景 2:循环读取直到成功
c
#include <stdio.h>
void clear_input(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
int main(void)
{
int x;
printf("请输入一个整数:");
while (scanf("%d", &x) != 1) {
printf("输入无效,请重新输入:");
clear_input(); /* 少了这行 = 死循环 */
}
printf("你输入的是:%d\n", x);
return 0;
}


运行演示
请输入一个整数:abc
输入无效,请重新输入:12xyz
输入无效,请重新输入:hello
输入无效,请重新输入:42
你输入的是:42
关键点:
-
循环读取直到合法 :
while (scanf("%d", &x) != 1)表示只要没成功读到一个整数,就继续重试。 -
必须清空缓冲区 :
clear_input()用getchar()读到'\n'或EOF,丢弃非法字符。少了这行,
scanf会反复读同一个非法字符,造成死循环。 -
scanf返回值含义:1:成功读到整数0:输入格式不匹配EOF:输入结束或出错
!= 1把后两种情况都当成失败。
-
getchar()用int接收 :因为要能判断EOF。 -
潜在风险 :遇到
EOF时,scanf一直返回EOF,循环不会退出。更安全可写成:
cint ret = scanf("%d", &x); if (ret == EOF) break; if (ret != 1) { clear_input(); ... } -
成功后 :
x保存合法整数,最后正常输出。
把 clear_input() 注释掉再试一次 ,你会看到无限刷屏的 输入无效,请重新输入:------这就是"缓冲区未清"的经典死循环。

场景 3:数字后接字符串(吃掉残留 \n)
scanf("%d", ...) 读完后缓冲区还剩一个 \n,紧接着 fgets 会读到这个空行。必须先清。
c
#include <stdio.h>
#include <string.h>
void clear_input(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
int main(void)
{
int age;
char name[64];
printf("请输入年龄:");
scanf("%d", &age);
clear_input(); /* ← 吃掉残留的 '\n' */
printf("请输入姓名:");
fgets(name, sizeof name, stdin);
name[strcspn(name, "\r\n")] = '\0'; /* 去掉行尾换行 */
printf("年龄=%d,姓名=%s\n", age, name);
return 0;
}

运行演示
请输入年龄:25
请输入姓名:Zhang San
年龄=25,姓名=Zhang San
关键点:处理 scanf 读整数后残留的换行符,再用 fgets 安全读取字符串。
-
scanf("%d", &age)的陷阱读完整数后,换行符
'\n'仍留在输入缓冲区。如果直接
fgets,会先读到这个空行,导致姓名为空。 -
clear_input()的作用循环
getchar()直到'\n'或EOF,把残留的换行和多余字符吃掉,保证后面fgets读到真正的姓名。 -
fgets(name, sizeof name, stdin)安全读取一行,最多读
sizeof name - 1个字符,自动补'\0',不会像scanf("%s")那样溢出,也能读入空格。 -
strcspn(name, "\r\n")去换行找到
name中第一个\r或\n的位置,替换成'\0',去掉行尾换行符。如果没有换行,
strcspn返回字符串长度,替换末尾'\0'也无害。 -
头文件
string.h用于strcspn。 -
潜在缺陷
scanf没检查返回值,输入非法时age未初始化。fgets没检查是否读取成功。- 输入过长时,
fgets会截断,剩余字符留在缓冲区。
总结 :混合使用 scanf 和 fgets 时,必须在 scanf 后清空缓冲区,否则字符串读取会出错。
把 clear_input() 注释掉再运行 :

请输入年龄:25
请输入姓名:年龄=25,姓名=
fgets 直接读到了残留的 \n,姓名变成空串。这就是最容易被忽略的 bug。
场景 4:菜单主循环的输入保护
c
#include <stdio.h>
void clear_input(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
int main(void)
{
int choice;
do {
printf("\n1. 选项 A\n2. 选项 B\n0. 退出\n");
printf("请选择:");
if (scanf("%d", &choice) != 1) {
clear_input();
printf("输入无效,重新选择。\n");
choice = -1; /* 必须赋值,否则可能沿用旧值直接退出 */
continue;
}
switch (choice) {
case 1: printf("你选了 A\n"); break;
case 2: printf("你选了 B\n"); break;
case 0: printf("再见\n"); break;
default: printf("无效选项\n"); break;
}
} while (choice != 0);
return 0;
}


运行演示
1. 选项 A
2. 选项 B
0. 退出
请选择:abc
输入无效,重新选择。
1. 选项 A
2. 选项 B
0. 退出
请选择:1
你选了 A
1. 选项 A
2. 选项 B
0. 退出
请选择:0
再见
关键点:
-
do-while菜单循环 :至少显示一次菜单,choice == 0时退出。 -
检查
scanf返回值 :scanf("%d", &choice) != 1表示没读到合法整数。 -
必须调用
clear_input():输入无效时,非法字符仍留在缓冲区,不清掉会导致
scanf反复读同一字符,形成死循环。 -
choice = -1是关键 :scanf失败时不会修改choice,它可能保留上一次的值。如果上次是0,不重置就会直接退出循环。赋值为
-1后再continue,保证循环继续并跳过switch。 -
switch处理选项 :合法选项正常处理,
default提示无效选项。 -
隐患 :遇到
EOF时,scanf会一直返回EOF,程序可能无限循环。更安全可加:
cint ret = scanf("%d", &choice); if (ret == EOF) break; if (ret != 1) { clear_input(); choice = -1; continue; }
核心 :输入失败后要清缓冲区,并重置 choice,否则菜单循环会出错。
为什么 choice = -1 不能省?
假设用户上一轮输入了 1,这一轮输入 abc,scanf 失败时 choice 保持为 1 (scanf 不修改它)。如果不赋值,do-while 会继续循环,看似没问题。但假设上一轮用户选的是 0(先选 0 退出),或者 choice 初始值就是 0------那 scanf 失败后 choice 仍然是 0,循环条件立即为假,程序没等用户重新输入就退出了 。赋值 -1 是防御性写法。
场景 5:从管道 / 文件读入
echo "abc" | ./program 或 ./program < input.txt 时,stdin 不是键盘,文件末尾 getchar() 会一直返回 EOF。
c
#include <stdio.h>
void clear_input(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
int main(void)
{
int x;
printf("请输入一个整数:");
if (scanf("%d", &x) != 1) {
clear_input(); /* 有 EOF 判断,这里不会死循环 */
printf("输入无效\n");
return 1;
}
printf("读到:%d\n", x);
return 0;
}
运行演示
请输入一个整数:输入无效
请输入一个整数:读到:42
关键点:
-
检查
scanf返回值 :!= 1表示没成功读到整数。1:成功0:格式错误EOF:输入结束或出错
-
失败时清空缓冲区 :
clear_input()丢弃当前行剩余字符,防止垃圾字符影响后续输入。虽然这里失败后直接退出,但这是良好习惯。 -
clear_input()的 EOF 判断 :while ((c = getchar()) != '\n' && c != EOF)c用int,且判断EOF,所以输入流已结束时不会死循环。 -
失败后立即
return 1:不再使用未成功赋值的x,表示异常退出。 -
成功时输出
x,return 0:程序正常结束。
核心:输入验证 + 安全清缓冲区 + 正确处理 EOF + 明确退出码。
验证 EOF 判断的必要性 :把循环条件改成 while (getchar() != '\n');(去掉 && c != EOF),再运行 echo "abc" | ./demo5------程序会卡死,CPU 跑满。getchar() 在文件末尾一直返回 EOF,EOF != '\n' 恒为真,循环永不退出。
进阶用法
进阶 1:封装成"重试读整数"
c
#include <stdio.h>
int read_int(const char *prompt, int *out)
{
char line[128];
while (1) {
printf("%s", prompt);
if (!fgets(line, sizeof line, stdin))
return 0; /* EOF */
if (sscanf(line, "%d", out) == 1)
return 1; /* 成功 */
printf("输入无效,请重试。\n");
}
}
int main(void)
{
int age, score;
if (!read_int("请输入年龄:", &age)) return 1;
if (!read_int("请输入成绩:", &score)) return 1;
printf("年龄=%d,成绩=%d\n", age, score);
return 0;
}

运行演示
请输入年龄:abc
输入无效,请重试。
请输入年龄:25
请输入成绩:abc
输入无效,请重试。
请输入成绩:92
年龄=25,成绩=92
关键点:
-
封装
read_int:提示 + 输出参数out,把"读整数并验证"的逻辑独立出来,主程序更简洁。 -
用
fgets读整行 :一次读入一整行到
line[128],避免scanf遗留换行符或垃圾字符的问题。 -
用
sscanf解析整数 :sscanf(line, "%d", out) == 1表示成功解析出一个整数。失败则提示重试。 -
EOF 处理 :
fgets返回NULL表示输入结束或出错,函数返回0;主程序据此退出。 -
循环重试 :
输入非法时不会退出,而是不断提示"输入无效,请重试",直到合法或遇到 EOF。
-
返回值约定:
1:成功读到整数0:EOF 或读取失败
主程序用if (!read_int(...)) return 1;统一处理。
-
潜在局限:
sscanf("%d")会接受123abc,只取123,不检测尾部垃圾。- 输入超过 127 字符时会被截断,剩余字符留在缓冲区影响下一次读取。
更严格可用strtol并检查endptr。
优势对比
| 对比项 | scanf + clear_input |
fgets + sscanf |
|---|---|---|
| 缓冲区 | 会脏,需要清 | 永远干净 |
| 溢出风险 | %s 会溢出 |
fgets 有长度保护 |
| 错误信息 | 只知道失败 | 能打印用户原文 |
| 可测试性 | 难 | 可以传入字符串测试 |
进阶 2:严格解析(拒绝 12abc)
sscanf("%d") 对 12abc 会返回成功并得到 12。要严格只接受纯数字:
c
#include <stdio.h>
int read_int_strict(const char *prompt, int *out)
{
char line[128], extra;
while (1) {
printf("%s", prompt);
if (!fgets(line, sizeof line, stdin)) return 0;
/* "%d %c":%c 前的空格跳过空白。
整行只有数字+换行时,%c 匹配失败,返回值为 1 */
if (sscanf(line, "%d %c", out, &extra) == 1)
return 1;
printf("输入无效(含多余字符),请重试。\n");
}
}
int main(void)
{
int n;
if (read_int_strict("请输入整数:", &n))
printf("读到:%d\n", n);
return 0;
}

运行演示
请输入整数:12abc
输入无效(含多余字符),请重试。
请输入整数:12
读到:12
关键点:
-
用
fgets读整行 :避免scanf遗留换行符/垃圾字符,输入更可控。 -
用
sscanf解析,并用%c检测多余字符csscanf(line, "%d %c", out, &extra) == 1%d:读整数到out。- 格式中的空格:跳过任意空白(包括换行)。
%c:尝试读一个非空白字符到extra。
-
返回值判断逻辑
- 返回
1:只成功读到整数,%c没匹配到非空白字符 → 输入合法。 - 返回
2:整数后面还有非空白字符 → 有多余内容,拒绝。 - 返回
0:开头就不是合法整数 → 拒绝。
- 返回
-
严格性
相比
sscanf(line, "%d", out) == 1,它能拒绝123abc、123 456这类"整数后带垃圾"的输入。 -
循环重试
输入非法时提示并重新读一行,直到合法或
fgets返回NULL(EOF/错误)。 -
EOF 处理
fgets返回NULL时,read_int_strict返回0,主程序可选择退出或处理。 -
潜在局限
%d遇到整数溢出时行为未定义,严格场景应用strtol并检查errno/endptr。line[128]可能截断超长输入,剩余字符留在缓冲区,影响下一次读取。- 主程序未区分 EOF 和成功,
read_int_strict返回0时只是静默结束。
核心 :fgets + sscanf("%d %c") == 1 是一种简洁的"只允许一个整数,不许有多余非空白字符"的严格读取方式。
对比进阶 1 :read_int 对 12abc 会返回成功并得到 12;read_int_strict 会拒绝。
进阶 3:带范围的整数读取
c
#include <stdio.h>
int read_int(const char *prompt, int *out)
{
char line[128];
while (1) {
printf("%s", prompt);
if (!fgets(line, sizeof line, stdin)) return 0;
if (sscanf(line, "%d", out) == 1) return 1;
printf("输入无效,请重试。\n");
}
}
int read_int_range(const char *prompt, int lo, int hi, int *out)
{
int x;
while (1) {
if (!read_int(prompt, &x)) return 0;
if (x >= lo && x <= hi) { *out = x; return 1; }
printf("请输入 %d ~ %d 之间的整数。\n", lo, hi);
}
}
int main(void)
{
int score;
if (read_int_range("请输入成绩(0~100):", 0, 100, &score))
printf("成绩:%d\n", score);
return 0;
}

运行演示
请输入成绩(0~100):150
请输入 0 ~ 100 之间的整数。
请输入成绩(0~100):-5
请输入 0 ~ 100 之间的整数。
请输入成绩(0~100):92
成绩:92
关键点:
-
分层封装
read_int:只负责读一个整数。read_int_range:在read_int基础上加范围检查。
-
fgets+sscanf读整数一次读整行,避免
scanf遗留换行符或垃圾字符;再用sscanf解析。 -
返回值约定
read_int返回1:成功;返回0:EOF或读取失败。read_int_range同样返回1成功,0失败。
-
范围校验循环
read_int_range先调read_int,若值在[lo, hi]才写入*out并返回;否则提示重试。 -
主程序检查返回值
只有
read_int_range成功才打印成绩;失败则静默返回。 -
局限
sscanf("%d")会接受123abc,只取123,不检测多余字符。- 未处理整数溢出。
line[128]可能截断超长输入。- 未区分
EOF和普通错误。
进阶 4:读字符串并去掉换行
c
#include <stdio.h>
#include <string.h>
int read_line(const char *prompt, char *buf, size_t n)
{
printf("%s", prompt);
if (!fgets(buf, n, stdin)) return 0;
buf[strcspn(buf, "\r\n")] = '\0'; /* 一行去换行,兼容 Windows */
return 1;
}
int main(void)
{
char name[32];
if (read_line("请输入姓名:", name, sizeof name))
printf("姓名=[%s]\n", name);
return 0;
}

运行演示
请输入姓名:Zhang San
姓名=[Zhang San]
关键点:
-
封装
read_line:提示、读取一行、去掉换行,成功返回1,失败返回0。 -
用
fgets安全读取 :fgets(buf, n, stdin)最多读n-1个字符,自动补'\0',不会像scanf("%s")那样越界,且能读入空格。 -
检查
fgets返回值 :返回
NULL表示EOF或读取错误,此时直接return 0,不碰buf。 -
strcspn(buf, "\r\n")去换行 :找到第一个
\r或\n的位置,替换为'\0',去掉行尾换行。兼容 Unix 的
\n和 Windows 的\r\n。 -
size_t n与sizeof name:主程序传
sizeof name,即数组总字节数,保证fgets不越界。 -
返回值统一 :
read_line成功才打印;失败时主程序静默结束。 -
局限:
- 输入超过
31字符时,fgets只读一部分,剩余字符留在缓冲区,可能影响下一次读取。 - 空行会得到空字符串。
- 严格场景应检测是否读到换行,没读到则清空缓冲区。
- 输入超过
strcspn(buf, "\r\n") 做了什么
- 返回
buf中第一个\r或\n的下标; - 把该位置改成
'\0',字符串到此结束; - 兼容 Unix(
\n)和 Windows(\r\n)。
对比 strtok / 手写循环 :strcspn 一行搞定,不修改 buf 之外的东西,不会因为找不到分隔符而越界。
进阶 5:跳过空行直到有内容
交互式程序里,用户可能连按几次回车。跳过所有空行:
c
#include <stdio.h>
#include <string.h>
int read_line(const char *prompt, char *buf, size_t n)
{
printf("%s", prompt);
if (!fgets(buf, n, stdin)) return 0;
buf[strcspn(buf, "\r\n")] = '\0';
return 1;
}
int read_nonempty_line(const char *prompt, char *buf, size_t n)
{
while (1) {
if (!read_line(prompt, buf, n)) return 0;
if (buf[0] != '\0') return 1; /* 非空行 */
printf("(空输入,请重新输入)\n");
}
}
int main(void)
{
char name[32];
if (read_nonempty_line("请输入姓名:", name, sizeof name))
printf("姓名=[%s]\n", name);
return 0;
}

运行演示
请输入姓名:
(空输入,请重新输入)
请输入姓名:
(空输入,请重新输入)
请输入姓名:Li Si
姓名=[Li Si]
关键点:
-
read_line负责底层读取- 提示后调用
fgets安全读一行。 - 失败返回
0(EOF 或错误)。 - 用
strcspn(buf, "\r\n")去掉行尾换行,兼容\n和\r\n。
- 提示后调用
-
read_nonempty_line增加非空校验- 循环调用
read_line。 - 若
buf[0] == '\0',说明输入为空行,提示并重试。 - 非空才返回
1。
- 循环调用
-
返回值统一
1:成功读到非空行。0:遇到 EOF 或读取错误。
主程序据此决定是否输出。
-
fgets的优势比
scanf("%s")安全:不越界、能读空格、自动补'\0'。 -
局限
- 输入超过
n-1个字符时会被截断,剩余字符留在缓冲区,可能影响下一次读取。 - 只判断了"空行",没有判断"全空白字符"是否算空。
- 未区分 EOF 和普通读取错误。
- 输入超过
核心 :在 read_line 基础上封装出 read_nonempty_line,实现"安全读一行 + 拒绝空输入"的常用输入模式。
进阶 6:批量跳过直到某字符
clear_input() 是 skip_until('\n') 的特例。CSV 解析里经常需要读到逗号为止:
c
/* demo12.c ------ CSV 简单解析:读取到逗号或换行为止 */
#include <stdio.h>
#include <string.h>
/* 跳过字符直到遇到 delim 或 EOF */
void skip_until(int delim)
{
int c;
do {
c = getchar();
} while (c != delim && c != EOF);
}
/* 读取一个字段(直到逗号或换行) */
int read_field(char *buf, size_t n)
{
int c;
size_t i = 0;
while ((c = getchar()) != ',' && c != '\n' && c != EOF) {
if (i + 1 < n) buf[i++] = (char)c;
}
buf[i] = '\0';
return (c != EOF) ? 1 : 0;
}
int main(void)
{
char name[32], age[16], city[32];
printf("请输入 CSV(格式:姓名,年龄,城市):\n");
if (!read_field(name, sizeof name)) return 1;
if (!read_field(age, sizeof age)) return 1;
if (!read_field(city, sizeof city)) return 1;
printf("姓名=%s\n年龄=%s\n城市=%s\n", name, age, city);
return 0;
}
运行演示
请输入 CSV(格式:姓名,年龄,城市):
ZhangSan,25,Beijing
姓名=ZhangSan
年龄=25
城市=Beijing
skip_until 与 clear_input 的关系
c
void clear_input(void) { skip_until('\n'); } /* 完全等价 */
skip_until 更通用:读分号、读逗号、读 Tab 都靠它。但注意它用 getchar(),只能作用于 stdin,要作用于任意流得改成 fgetc(fp)。
下篇文章会用到这篇博客提到的内容