字符I/O与输入验证
掌握 getchar/putchar 字符I/O、缓冲I/O机制、输入验证与缓冲区清理,能编写健壮的交互式菜单程序
学习目标
- 能用 getchar/putchar 实现逐字符读写,并解释为什么 getchar 的返回值应存到 int 而不是 char
- 能区分行缓冲、全缓冲与无缓冲,并在 printf 提示后 scanf 读入前用 fflush(stdout) 保证提示及时显示
- 能编写带「读入→验证→清缓冲区→重试」循环的健壮输入程序,并回答:scanf("%d", &n) 后立刻 getchar() 为什么常读到换行符?
机制总览
字符I/O与输入验证:机制路径
- 1
为什么 scanf 有时「不吃」你刚打的字?
想象快递柜取件:你输入取件码 42 然后按确认,柜门只认数字部分, 确认键的「咔嗒」还留在槽里没清走 。下一次有人来取件,柜门先把上次残留的「咔嗒」当输入处理了——你以为程序在等你打字,其实它早就读到了一个看不见的换行符。
- 2
getchar 与 putchar:一次只搬一个字符
printf("输入字符(Ctrl+D 结束):\n"); while ((ch = getchar()) != EOF) putchar(ch); / 原样回显 / return 0;
- 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 然后按确认,柜门只认数字部分,确认键的「咔嗒」还留在槽里没清走。下一次有人来取件,柜门先把上次残留的「咔嗒」当输入处理了——你以为程序在等你打字,其实它早就读到了一个看不见的换行符。
程序和标准输入之间也有这样一个「快递槽」——↡标准输入流 stdin 在程序和键盘之间暂存字符数据的区域。scanf 和 getchar 都从这里取数据。。上一章学了 scanf 和 switch 菜单;这一章降到字符层看清数据怎么一进一出,并学会验证输入、清理残留,写出不会「莫名其妙跳过等待」的交互程序。
getchar 与 putchar:一次只搬一个字符
↡从标准输入 stdin 读取下一个字符的标准库函数,返回该字符的 int 值或 EOF。声明在 stdio.h。 和 ↡向标准输出 stdout 写一个字符的标准库函数,成功返回该字符,失败返回 EOF。声明在 stdio.h。 是 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 要能返回 ↡End Of File 的负值整型宏,表示输入结束或读错误,不是普通字符。。标准只保证 EOF 是负值整型常量,不保证它恰好为 -1。若用 char 存储,EOF 可能与有效字节混淆,循环判断会出错。
putchar 把单个字符送到 stdout;getchar 从 stdin 取单个字符。它们底下仍走标准 I/O 的缓冲机制——不是每个字符都立刻跟键盘硬件打交道。
猜一猜:上面循环里用户按回车,putchar 会输出几个字符?换行符算不算「一个字符」?
↡在程序和 I/O 设备之间暂存数据、批量读写的机制。分全缓冲、行缓冲、无缓冲三种。 让实现不必为每个字符都执行底层读写。stdout 连接交互终端时常采用 ↡输出遇到换行、缓冲区满或显式刷新时送往目标的缓冲模式。交互流常采用该模式,但具体策略由实现和运行环境决定。;连接文件时常采用全缓冲。具体模式由 C 实现和运行环境决定,不能仅凭流名称断言。写交互程序时,提示后调用 fflush(stdout) 能明确要求已写数据送往目标。
第一步:行缓冲——终端上的 stdout
stdout 连接交互终端时通常采用行缓冲。换行、缓冲区满或显式 fflush 会触发刷新;没有换行的提示可能暂时不可见。
文件流和键盘输入:标准流与重定向
C 程序面对的是 ↡标准库用 FILE 对象表示的有序字节通道;同一套字符和格式化 I/O 可连接终端、普通文件或管道。,不是直接面对键盘或磁盘。程序启动时已有 stdin、stdout、stderr 三个标准文本流;fopen 则返回指向其他 FILE 对象的指针。getchar() 等价于从 stdin 调用 getc,putchar(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 缓冲区。
用户输入 42 然后按回车,缓冲区里是 '4' '2' '\n'。scanf("%d", &n) 吃掉 42,换行符 \n 还在。紧接着 getchar() 读到的就是 \n(ASCII 10),不是你以为的「下一个按键」。
↡检查用户输入是否合法、在非法时提示并重试,同时清理输入缓冲区残留字符的编程模式。 要保证:读入失败时不仅打印错误,还要把缓冲区吃到干净,否则重试时读到的是垃圾。
猜一猜:用户输入
abc而程序期望整数,如果不清理缓冲区就 while 重试,会发生什么?
第一步:整行进缓冲区
键盘驱动把用户敲的一行(含回车)送入 stdin 缓冲区。getchar 和 scanf 都从这里取数据,不是直接问键盘。
输入验证:读入、检验、清理、重试
健壮的数字输入不是「scanf 一次就信」,而是区分成功、匹配失败和 EOF,再决定接受、清理或结束。先写一个不会在 EOF 卡住的记录清理函数:
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 会读取什么?
第一步:提示并读入
printf 提示后 fflush,确保用户看见。scanf 或 getchar 从缓冲区取数据——此时还不知道是否合法。
菜单驱动程序:把分支语句变成交互骨架
上一章的 switch 菜单在本章加上输入验证和字符层选项,就是完整的 ↡通过循环显示选项、读入用户选择、用 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。
第一步:显示菜单
每次循环开头用 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 是启动时已有的标准流,可由运行环境连接到终端、文件或管道。
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
缓冲输入:机制、边界与证据
在《字符I/O与输入验证》的官方单元 cpr-08 中,缓冲输入连接本章第 4 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。