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

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

原始代码

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。

' 核心' :安全读取整数,输入无效时清空缓冲区并退出。

要点:

  1. clear_input():循环 getchar() 直到 '\n' 或 EOF,丢弃当前行剩余垃圾字符。

    c 用 int,因为 getchar() 可能返回 EOF。

  2. scanf("%d", &x) 返回值:

    • 1:成功读到一个整数
    • 0:输入不是合法整数
    • EOF:输入结束或错误
      所以 != 1 表示失败。
  3. 失败后必须清缓冲区:非法字符仍留在输入流中,不清会影响后续读取。

  4. return 1 表示异常退出,return 0 表示正常结束。

  5. 局限:输入 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

关键点:

  1. 循环读取直到合法 :while (scanf("%d", &x) != 1) 表示只要没成功读到一个整数,就继续重试。

  2. 必须清空缓冲区 :clear_input() 用 getchar() 读到 '\n' 或 EOF,丢弃非法字符。

    少了这行,scanf 会反复读同一个非法字符,造成死循环。

  3. scanf 返回值含义:

    • 1:成功读到整数
    • 0:输入格式不匹配
    • EOF:输入结束或出错
      != 1 把后两种情况都当成失败。
  4. getchar() 用 int 接收 :因为要能判断 EOF。

  5. 潜在风险 :遇到 EOF 时,scanf 一直返回 EOF,循环不会退出。

    更安全可写成:

    c 复制代码
    int ret = scanf("%d", &x);
    if (ret == EOF) break;
    if (ret != 1) { clear_input(); ... }
  6. 成功后 :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 安全读取字符串。

  1. scanf("%d", &age) 的陷阱

    读完整数后,换行符 '\n' 仍留在输入缓冲区。

    如果直接 fgets,会先读到这个空行,导致姓名为空。

  2. clear_input() 的作用

    循环 getchar() 直到 '\n' 或 EOF,把残留的换行和多余字符吃掉,保证后面 fgets 读到真正的姓名。

  3. fgets(name, sizeof name, stdin)

    安全读取一行,最多读 sizeof name - 1 个字符,自动补 '\0',不会像 scanf("%s") 那样溢出,也能读入空格。

  4. strcspn(name, "\r\n") 去换行

    找到 name 中第一个 \r 或 \n 的位置,替换成 '\0',去掉行尾换行符。

    如果没有换行,strcspn 返回字符串长度,替换末尾 '\0' 也无害。

  5. 头文件

    string.h 用于 strcspn。

  6. 潜在缺陷

    • 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
再见

关键点:

  1. do-while 菜单循环 :至少显示一次菜单,choice == 0 时退出。

  2. 检查 scanf 返回值 :

    scanf("%d", &choice) != 1 表示没读到合法整数。

  3. 必须调用 clear_input() :

    输入无效时,非法字符仍留在缓冲区,不清掉会导致 scanf 反复读同一字符,形成死循环。

  4. choice = -1 是关键 :

    scanf 失败时不会修改 choice,它可能保留上一次的值。如果上次是 0,不重置就会直接退出循环。

    赋值为 -1 后再 continue,保证循环继续并跳过 switch。

  5. switch 处理选项 :

    合法选项正常处理,default 提示无效选项。

  6. 隐患 :遇到 EOF 时,scanf 会一直返回 EOF,程序可能无限循环。

    更安全可加:

    c 复制代码
    int 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

关键点:

  1. 检查 scanf 返回值 :!= 1 表示没成功读到整数。

    • 1:成功
    • 0:格式错误
    • EOF:输入结束或出错
  2. 失败时清空缓冲区 :clear_input() 丢弃当前行剩余字符,防止垃圾字符影响后续输入。虽然这里失败后直接退出,但这是良好习惯。

  3. clear_input() 的 EOF 判断 :

    while ((c = getchar()) != '\n' && c != EOF)

    c 用 int,且判断 EOF,所以输入流已结束时不会死循环。

  4. 失败后立即 return 1 :不再使用未成功赋值的 x,表示异常退出。

  5. 成功时输出 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

关键点:

  1. 封装 read_int :提示 + 输出参数 out,把"读整数并验证"的逻辑独立出来,主程序更简洁。

  2. 用 fgets 读整行 :

    一次读入一整行到 line[128],避免 scanf 遗留换行符或垃圾字符的问题。

  3. 用 sscanf 解析整数 :

    sscanf(line, "%d", out) == 1 表示成功解析出一个整数。失败则提示重试。

  4. EOF 处理 :

    fgets 返回 NULL 表示输入结束或出错,函数返回 0;主程序据此退出。

  5. 循环重试 :

    输入非法时不会退出,而是不断提示"输入无效,请重试",直到合法或遇到 EOF。

  6. 返回值约定:

    • 1:成功读到整数
    • 0:EOF 或读取失败
      主程序用 if (!read_int(...)) return 1; 统一处理。
  7. 潜在局限:

    • 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

关键点:

  1. 用 fgets 读整行 :避免 scanf 遗留换行符/垃圾字符,输入更可控。

  2. 用 sscanf 解析,并用 %c 检测多余字符

    c 复制代码
    sscanf(line, "%d %c", out, &extra) == 1
    • %d:读整数到 out。
    • 格式中的空格:跳过任意空白(包括换行)。
    • %c:尝试读一个非空白字符到 extra。
  3. 返回值判断逻辑

    • 返回 1:只成功读到整数,%c 没匹配到非空白字符 → 输入合法。
    • 返回 2:整数后面还有非空白字符 → 有多余内容,拒绝。
    • 返回 0:开头就不是合法整数 → 拒绝。
  4. 严格性

    相比 sscanf(line, "%d", out) == 1,它能拒绝 123abc、123 456 这类"整数后带垃圾"的输入。

  5. 循环重试

    输入非法时提示并重新读一行,直到合法或 fgets 返回 NULL(EOF/错误)。

  6. EOF 处理

    fgets 返回 NULL 时,read_int_strict 返回 0,主程序可选择退出或处理。

  7. 潜在局限

    • %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

关键点:

  1. 分层封装

    • read_int:只负责读一个整数。
    • read_int_range:在 read_int 基础上加范围检查。
  2. fgets + sscanf 读整数

    一次读整行,避免 scanf 遗留换行符或垃圾字符;再用 sscanf 解析。

  3. 返回值约定

    • read_int 返回 1:成功;返回 0:EOF 或读取失败。
    • read_int_range 同样返回 1 成功,0 失败。
  4. 范围校验循环

    read_int_range 先调 read_int,若值在 [lo, hi] 才写入 *out 并返回;否则提示重试。

  5. 主程序检查返回值

    只有 read_int_range 成功才打印成绩;失败则静默返回。

  6. 局限

    • 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]

关键点:

  1. 封装 read_line :提示、读取一行、去掉换行,成功返回 1,失败返回 0。

  2. 用 fgets 安全读取 :

    fgets(buf, n, stdin) 最多读 n-1 个字符,自动补 '\0',不会像 scanf("%s") 那样越界,且能读入空格。

  3. 检查 fgets 返回值 :

    返回 NULL 表示 EOF 或读取错误,此时直接 return 0,不碰 buf。

  4. strcspn(buf, "\r\n") 去换行 :

    找到第一个 \r 或 \n 的位置,替换为 '\0',去掉行尾换行。

    兼容 Unix 的 \n 和 Windows 的 \r\n。

  5. size_t n 与 sizeof name :

    主程序传 sizeof name,即数组总字节数,保证 fgets 不越界。

  6. 返回值统一 :

    read_line 成功才打印;失败时主程序静默结束。

  7. 局限:

    • 输入超过 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]

关键点:

  1. read_line 负责底层读取

    • 提示后调用 fgets 安全读一行。
    • 失败返回 0(EOF 或错误)。
    • 用 strcspn(buf, "\r\n") 去掉行尾换行,兼容 \n 和 \r\n。
  2. read_nonempty_line 增加非空校验

    • 循环调用 read_line。
    • 若 buf[0] == '\0',说明输入为空行,提示并重试。
    • 非空才返回 1。
  3. 返回值统一

    • 1:成功读到非空行。
    • 0:遇到 EOF 或读取错误。
      主程序据此决定是否输出。
  4. fgets 的优势

    比 scanf("%s") 安全:不越界、能读空格、自动补 '\0'。

  5. 局限

    • 输入超过 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)。


下篇文章会用到这篇博客提到的内容

相关推荐
奋发向前wcx1 小时前
CSP-J复赛模拟赛1 王晨旭补题 2026.10.2
java·开发语言
周杰伦fans1 小时前
轻量模型高并发推理优化技巧
开发语言·人工智能·c#
Frank_refuel1 小时前
【C++项目】手撕JRpc框架(单机怎么优化到分布式思想)
开发语言·c++
wuminyu2 小时前
JEP491中synchronized关键字引发的平台线程钉住解决方法简介
java·linux·c语言·jvm·c++
可乐鸡翅yeah_2 小时前
fmp4 碎片 MP4 格式 HLS 流,和传统 TS 分片有什么不一样
开发语言·javascript·ios·音视频·safari·m3u8·m3u8在线播放
流形填表2 小时前
多校区题库标准化:Excel模板与校验脚本
开发语言·python
步行cgn2 小时前
AOP 通知的类型详解
开发语言
weixin_307779132 小时前
从“人操作仪器”到“Agent 自主发现”:OPL 金属材料自驱动实验室 MVP 技术论述
开发语言·人工智能·算法·架构
笑鸿的学习笔记2 小时前
C++笔记之SIMD与SSE指令集
开发语言·c++·笔记