字符I/O与输入验证

掌握 getchar/putchar 字符I/O、缓冲I/O机制、输入验证与缓冲区清理,能编写健壮的交互式菜单程序

学习目标

  • 能用 getchar/putchar 实现逐字符读写,并解释为什么 getchar 的返回值应存到 int 而不是 char
  • 能区分行缓冲、全缓冲与无缓冲,并在 printf 提示后 scanf 读入前用 fflush(stdout) 保证提示及时显示
  • 能编写带「读入→验证→清缓冲区→重试」循环的健壮输入程序,并回答:scanf("%d", &n) 后立刻 getchar() 为什么常读到换行符?

机制总览

字符I/O与输入验证:机制路径

  1. 1

    为什么 scanf 有时「不吃」你刚打的字?

    想象快递柜取件:你输入取件码 42 然后按确认,柜门只认数字部分, 确认键的「咔嗒」还留在槽里没清走 。下一次有人来取件,柜门先把上次残留的「咔嗒」当输入处理了——你以为程序在等你打字,其实它早就读到了一个看不见的换行符。

  2. 2

    getchar 与 putchar:一次只搬一个字符

    printf("输入字符(Ctrl+D 结束):\n"); while ((ch = getchar()) != EOF) putchar(ch); / 原样回显 / return 0;

  3. 3

    文件流和键盘输入:标准流与重定向

    C 程序面对的是 流(stream) ,不是直接面对键盘或磁盘。程序启动时已有 stdin 、 stdout 、 stderr 三个标准文本流; fopen 则返回指向其他 FILE 对象的指针。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

字符I/O与输入验证:机制与证据

切换《字符I/O与输入验证》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么 scanf 有时「不吃」你刚打的字?

想象快递柜取件:你输入取件码 42 然后按确认,柜门只认数字部分, 确认键的「咔嗒」还留在槽里没清走 。下一次有人来取件,柜门先把上次残留的「咔嗒」当输入处理了——你以为程序在等你打字,其实它早就读到了一个看不见的换行符。

可核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么 scanf 有时「不吃」你刚打的字?」的实际行为。

学完《字符I/O与输入验证》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

字符I/O与输入验证:失效与核验

为什么 scanf 有时「不吃」你刚打的字?

典型失效

若只记语法而忽略「为什么 scanf 有时「不吃」你刚打的字?」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么 scanf 有时「不吃」你刚打的字?」的实际行为。

getchar 与 putchar:一次只搬一个字符

典型失效

若只记语法而忽略「getchar 与 putchar:一次只搬一个字符」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「getchar 与 putchar:一次只搬一个字符」的实际行为。

文件流和键盘输入:标准流与重定向

典型失效

若只记语法而忽略「文件流和键盘输入:标准流与重定向」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「文件流和键盘输入:标准流与重定向」的实际行为。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么 scanf 有时「不吃」你刚打的字?

想象快递柜取件:你输入取件码 42 然后按确认,柜门只认数字部分,确认键的「咔嗒」还留在槽里没清走。下一次有人来取件,柜门先把上次残留的「咔嗒」当输入处理了——你以为程序在等你打字,其实它早就读到了一个看不见的换行符。

程序和标准输入之间也有这样一个「快递槽」——。上一章学了 scanf 和 switch 菜单;这一章降到字符层看清数据怎么一进一出,并学会验证输入、清理残留,写出不会「莫名其妙跳过等待」的交互程序。

getchar 与 putchar:一次只搬一个字符

是 C 里最「原始」的文本 I/O——不解析格式,一次处理一个字符:

#include <stdio.h>
int main(void) {
    int ch;
 
    printf("输入字符(Ctrl+D 结束):\n");
    while ((ch = getchar()) != EOF)
        putchar(ch);    /* 原样回显 */
    return 0;
}

注意 ch 的类型是 int 而不是 char。因为 getchar 要能返回 。标准只保证 EOF 是负值整型常量,不保证它恰好为 -1。若用 char 存储,EOF 可能与有效字节混淆,循环判断会出错。

putchar 把单个字符送到 stdout;getchar 从 stdin 取单个字符。它们底下仍走标准 I/O 的缓冲机制——不是每个字符都立刻跟键盘硬件打交道。

猜一猜:上面循环里用户按回车,putchar 会输出几个字符?换行符算不算「一个字符」?

让实现不必为每个字符都执行底层读写。stdout 连接交互终端时常采用 ;连接文件时常采用全缓冲。具体模式由 C 实现和运行环境决定,不能仅凭流名称断言。写交互程序时,提示后调用 fflush(stdout) 能明确要求已写数据送往目标。

分步1 / 3

第一步:行缓冲——终端上的 stdout

stdout 连接交互终端时通常采用行缓冲。换行、缓冲区满或显式 fflush 会触发刷新;没有换行的提示可能暂时不可见。

文件流和键盘输入:标准流与重定向

C 程序面对的是 ,不是直接面对键盘或磁盘。程序启动时已有 stdinstdoutstderr 三个标准文本流;fopen 则返回指向其他 FILE 对象的指针。getchar() 等价于从 stdin 调用 getcputchar(ch) 等价于向 stdout 调用 putc

#include <stdio.h>
 
int main(void) {
    int ch;
    while ((ch = getchar()) != EOF)
        putchar(ch);
 
    if (ferror(stdin) || ferror(stdout)) {
        fputs("I/O error\n", stderr);
        return 1;
    }
    return 0;
}

同一程序无需修改,就能由命令环境改变流的连接:

./copy < input.txt > output.txt
producer | ./copy

第一条把文件连接到 stdin,并把 stdout 连接到另一个文件;第二条把前一个进程的输出连接到本程序输入。重定向通常由 shell 或宿主环境在程序启动前完成,不是 <| 被 C 程序解析。getchar() 返回 EOF 后,用 feof(stdin) 可判断正常到达输入末尾,用 ferror(stdin) 判断读取失败;EOF 这个返回值本身不区分两者。

缓冲输入与混合数字字符输入:stdin 残留

上一章讲过 scanf 从缓冲区按格式「解码」数据。关键细节:它只消费匹配成功的部分,其余字符原封不动留在 stdin 缓冲区。

stdin 缓冲区与残留字符⌨ 用户输入42↵① 整行进入缓冲区stdin 输入缓冲区42\n② scanf 不消费,残留!scanf("%d", &n) → n=42只匹配整数部分ch = getchar(); → '\n'③ 读到残留换行,不是新输入!修法:while ((ch = getchar()) != '\n' && ch != EOF) ;或 scanf("%*c") 吃掉一个字符;混合 scanf 与 getchar 时尤其要注意。
scanf 按格式说明「挑食」——不匹配的部分留在 stdin 缓冲区。下一次字符读取会优先消费这些残留,造成「程序跳过等待输入」的错觉。

用户输入 42 然后按回车,缓冲区里是 '4' '2' '\n'scanf("%d", &n) 吃掉 42换行符 \n 还在。紧接着 getchar() 读到的就是 \n(ASCII 10),不是你以为的「下一个按键」。

要保证:读入失败时不仅打印错误,还要把缓冲区吃到干净,否则重试时读到的是垃圾。

猜一猜:用户输入 abc 而程序期望整数,如果不清理缓冲区就 while 重试,会发生什么?

分步1 / 3

第一步:整行进缓冲区

stdin 缓冲区与残留字符⌨ 用户输入42↵① 整行进入缓冲区stdin 输入缓冲区42\n② scanf 不消费,残留!scanf("%d", &n) → n=42只匹配整数部分ch = getchar(); → '\n'③ 读到残留换行,不是新输入!修法:while ((ch = getchar()) != '\n' && ch != EOF) ;或 scanf("%*c") 吃掉一个字符;混合 scanf 与 getchar 时尤其要注意。
scanf 按格式说明「挑食」——不匹配的部分留在 stdin 缓冲区。下一次字符读取会优先消费这些残留,造成「程序跳过等待输入」的错觉。

键盘驱动把用户敲的一行(含回车)送入 stdin 缓冲区。getchar 和 scanf 都从这里取数据,不是直接问键盘。

输入验证:读入、检验、清理、重试

健壮的数字输入不是「scanf 一次就信」,而是区分成功、匹配失败和 EOF,再决定接受、清理或结束。先写一个不会在 EOF 卡住的记录清理函数:

输入验证循环① 提示并读入② 验证合法?③ 使用数据④ 清缓冲区 + 重试健壮交互程序的核心:永远不信任用户输入,验证失败时吃掉残留字符再重新提示。
输入验证不是「读一次就完」——失败路径必须清理 stdin 残留,否则下一次读入会读到垃圾字符,陷入死循环或静默错误。
static int discard_line(void) {
    int ch;
    while ((ch = getchar()) != '\n' && ch != EOF)
        ;
    return ch;
}
 
int main(void) {
    int n, status;
 
    for (;;) {
        printf("请输入 1-10 的整数: ");
        fflush(stdout);
        status = scanf("%d", &n);
 
        if (status == EOF)
            return 0;
        if (status == 1 && n >= 1 && n <= 10) {
            discard_line();
            break;
        }
 
        if (status == 0)
            puts("需要整数。");
        else
            printf("%d 不在 1-10 范围内。\n", n);
        if (discard_line() == EOF)
            return 0;
    }
 
    printf("你输入了 %d\n", n);
    return 0;
}

scanf 返回成功赋值的项数;期望 1 个整数却在首个非空白字符遇到字母时返回 0,到达输入末尾或读取失败且尚未赋值时返回 EOF。格式成功后还要做 1-10 的范围验证。discard_line 同时检查换行和 EOF,因此无论一行多长、文件是否以换行结尾,都能终止。

猜一猜:匹配失败后不调用 discard_line(),用户输入 hello 时下一次 scanf 会读取什么?

分步1 / 4

第一步:提示并读入

输入验证循环① 提示并读入② 验证合法?③ 使用数据④ 清缓冲区 + 重试健壮交互程序的核心:永远不信任用户输入,验证失败时吃掉残留字符再重新提示。
输入验证不是「读一次就完」——失败路径必须清理 stdin 残留,否则下一次读入会读到垃圾字符,陷入死循环或静默错误。

printf 提示后 fflush,确保用户看见。scanf 或 getchar 从缓冲区取数据——此时还不知道是否合法。

菜单驱动程序:把分支语句变成交互骨架

上一章的 switch 菜单在本章加上输入验证字符层选项,就是完整的

菜单驱动程序状态机① 显示菜单② 读入 choice③ switch 分发case 1/2/...case 0: 退出④ do-while 回到菜单(choice != 0)菜单驱动 = 循环 + 显示 + 验证输入 + switch 分发。上一章的分支语句在本章落地成交互式程序骨架。
典型菜单程序用 do-while 保证至少显示一次菜单,switch 处理各选项,0 退出时 break 出 switch 并由循环条件结束程序。
#include <stdio.h>
 
static int discard_line(void) {
    int ch;
    while ((ch = getchar()) != '\n' && ch != EOF)
        ;
    return ch;
}
 
int main(void) {
    int choice, status;
 
    do {
        printf("\n=== 计算器 ===\n");
        printf("1. 加法  2. 减法  0. 退出\n");
        printf("请选择: ");
        fflush(stdout);
 
        status = scanf("%d", &choice);
        if (status == EOF)
            break;
        if (status != 1) {
            printf("请输入数字选项\n");
            if (discard_line() == EOF)
                break;
            continue;
        }
        discard_line();
 
        switch (choice) {
            case 1:
                printf("执行加法\n");
                break;
            case 2:
                printf("执行减法\n");
                break;
            case 0:
                printf("再见!\n");
                break;
            default:
                printf("无效选项 %d\n", choice);
        }
    } while (choice != 0);
 
    return 0;
}

单字符菜单可以用 getchar 读 'y'/'n',但记得前面若有 scanf 残留,应按记录边界处理。不要直接写 toupper(getchar()):先用 int 保存返回值,排除 EOF,再把非 EOF 值转换为 unsigned char 后传给 toupper

分步1 / 4

第一步:显示菜单

菜单驱动程序状态机① 显示菜单② 读入 choice③ switch 分发case 1/2/...case 0: 退出④ do-while 回到菜单(choice != 0)菜单驱动 = 循环 + 显示 + 验证输入 + switch 分发。上一章的分支语句在本章落地成交互式程序骨架。
典型菜单程序用 do-while 保证至少显示一次菜单,switch 处理各选项,0 退出时 break 出 switch 并由循环条件结束程序。

每次循环开头用 printf 列出选项。do-while 保证至少显示一次;末尾加空行 \n 提升可读性。

代码详解:字符过滤与行读取

场景一:统计一行中的字母个数

#include <stdio.h>
#include <ctype.h>
int main(void) {
    int ch, count = 0;
 
    printf("输入一行文字:\n");
    while ((ch = getchar()) != '\n' && ch != EOF) {
        if (isalpha(ch))
            count++;
    }
    printf("字母个数: %d\n", count);
    return 0;
}

循环读到换行或 EOF 为止;isalpha 判断字母。ch 用 int,EOF 安全。

场景二:吃掉空白后读字符选项

#include <stdio.h>
int main(void) {
    int ch;
 
    printf("确认?(y/n): ");
    fflush(stdout);
    while ((ch = getchar()) == ' ' || ch == '\t' || ch == '\n')
        ;   /* 跳过前导空白 */
    if (ch == 'y' || ch == 'Y')
        printf("已确认\n");
    else
        printf("已取消\n");
    return 0;
}

混合 scanf 后常需类似循环清掉 \n 再读 y/n。

场景三:用 getchar 实现「读一行」

#define MAX 80
char line[MAX];
int i = 0, ch;
 
while (i < MAX - 1 && (ch = getchar()) != '\n' && ch != EOF)
    line[i++] = (char)ch;
line[i] = '\0';

留一位给 \0;但若输入超过 79 个字符,当前片段会把余下字符留给下一次读取。实际代码应在数组填满后丢弃到换行或 EOF,并检查读取错误。生产环境通常直接使用 fgets,再判断是否读到换行来识别截断;gets 无法限制长度,C11 已将它从标准库移除。

场景四:验证后的菜单(完整骨架)

#include <stdio.h>
 
static int discard_line(void) {
    int ch;
    while ((ch = getchar()) != '\n' && ch != EOF)
        ;
    return ch;
}
 
int main(void) {
    int op, a, b, status;
 
    do {
        printf("1.加 2.减 0.退出\n选: ");
        fflush(stdout);
        status = scanf("%d", &op);
        if (status == EOF)
            break;
        if (status != 1) {
            puts("需要整数");
            if (discard_line() == EOF)
                break;
            continue;
        }
        discard_line();
        if (op == 0)
            break;
        if (op < 1 || op > 2) {
            puts("选项 1 或 2");
            continue;
        }
        printf("两整数: ");
        status = scanf("%d %d", &a, &b);
        if (status == EOF)
            break;
        if (status != 2) {
            puts("需要两个整数");
            if (discard_line() == EOF)
                break;
            continue;
        }
        discard_line();
        printf("结果: %d\n", op == 1 ? a + b : a - b);
    } while (1);
    return 0;
}

每次 scanf 后检查返回值;匹配失败清到记录边界,EOF 则结束交互。内层 continue 回到菜单,不嵌套过深。

容易踩的坑

小结

  • getchar/putchar 逐字符读写;返回值用 int 以正确区分 EOF 与普通字节
  • 标准 I/O 面向流;重定向可把同一 stdin/stdout 程序连接到终端、文件或管道
  • 缓冲策略取决于实现与连接目标;交互提示后 fflush(stdout) 明确要求刷新
  • scanf 只消费匹配部分,换行等残留留 stdin;混合 I/O 必须清缓冲区
  • 健壮输入 = 区分赋值数和 EOF + 范围验证 + 清到换行或 EOF 后再决定重试
  • 菜单驱动 = do-while + 验证读入 + switch 分发 + default 兜底

练习

问题 1(改代码型) 下面程序想读整数再读 y/n 确认,但确认总像被跳过。找出问题并修正。

int n;
char c;
scanf("%d", &n);
c = getchar();
if (c == 'y')
    printf("确认 %d\n", n);

问题 2(问答型) scanf("%d", &x) 用户输入 3.14 时,scanf 返回值是多少?x 的值是多少?缓冲区里还剩什么?

问题 3(综合实现型) 写程序:循环提示输入 1-5 的评分,用 scanf 读入并验证;非法输入提示并清缓冲区;合法输入打印「谢谢」后结束;若输入结束则正常退出。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

getchar()

从标准输入读取下一个字符的标准库函数。返回 int(字符值或 EOF)。声明在 stdio.h。与 putchar 配对实现字符级 I/O。

putchar()

向标准输出写一个字符。成功返回该字符,失败返回 EOF。声明在 stdio.h。常用于循环回显或配合 getchar 过滤字符。

EOF

End Of File 的负值整型宏,表示输入结束或读错误,不是普通字符。标准不保证它等于 -1;getchar 返回值须存 int,并用 feof 或 ferror 区分结束原因。

输入缓冲区(input buffer)

stdin 在程序和键盘之间的暂存区。用户按回车后整行先进缓冲区,scanf/getchar 再从中取。不匹配 scanf 格式的字符会残留。详见 StdinBufferDiagram。

缓冲 I/O(buffered I/O)

程序与设备之间用内存块批量读写,减少系统调用。分全缓冲、行缓冲、无缓冲。详见 BufferedIODiagram。

行缓冲(line buffered)

遇到换行符或缓冲区满才刷新到设备。终端 stdout 常用此模式。无换行的 printf 可能延迟显示。

输入验证(input validation)

读入后先检查转换结果,再检查业务范围;匹配失败时清理到换行或 EOF,只有仍有输入时才重试。详见 InputValidationFlowDiagram。

流(stream)

标准库用 FILE 对象表示的有序字节通道。stdin、stdout、stderr 是启动时已有的标准流,可由运行环境连接到终端、文件或管道。

菜单驱动程序(menu-driven program)

循环显示选项、读入选择、switch 分发、直到退出。结合 do-while、输入验证与 default。详见 MenuDriverDiagram。

原版目录概念补充核对

以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。

缓冲输入:机制、边界与证据

在《字符I/O与输入验证》的官方单元 cpr-08 中,缓冲输入连接本章第 4 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。

讨论

评论区加载中…