清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 第8章:字符输入输出和输入验证

用户输入的数字、命令和句子,到达程序时首先都是字符流。程序再根据规则,把字符解释成数据,并决定下一步如何处理。

文章目录 39 个章节

第8章:字符输入输出和输入验证

用户输入的数字、命令和句子,到达程序时首先都是字符流。程序再根据规则,把字符解释成数据,并决定下一步如何处理。

本章的重点是建立完整的输入流程:读取、判断、处理、消费剩余内容,再准备下一次读取;遇到输入结束时,有明确的退出路径。

1. 从输入流理解字符与数字

1.1 输入先是文本,再被解释

假设用户输入:

text
42

按下回车后,程序通常会陆续读到 '4'、'2' 和 '\n'。

读取方式 如何解释这些输入
getchar() 读取下一个字符,第一次得到 '4'
scanf("%c", &ch) 读取一个字符,不自动跳过空白
scanf("%s", text) 按非空白字符序列读取字符串,实际使用时应限制宽度
scanf("%d", &number) 把数字文本转换成整数 42
scanf("%f", &value) 把文本转换成浮点值 42.0f

输入流本身不会预先给每段内容贴上“整数”“浮点数”的标签。转换规则来自程序。

1.2 文件与流是两个层面的概念

  • 文件是实际保存信息的对象,例如磁盘上的文本文件。
  • 流是程序访问输入输出时使用的抽象接口。

同一套标准 I/O 函数可以通过流处理键盘输入或文件内容,不必在每个程序中重新处理底层设备差异。

标准流 常见用途 常见默认去向
stdin 标准输入 终端输入
stdout 标准输出 终端显示
stderr 错误或诊断信息 终端显示

默认去向不是流的固定身份。标准输入可以来自文件,标准输出也可以写入文件。

2. 单字符 I/O 的接口

2.1 getchar 读取,putchar 输出

c
int ch = getchar();

if (ch != EOF) {
    putchar(ch);
}

两者的接口都由 <stdio.h> 提供:

  • getchar 从标准输入读取下一个字符,以 int 返回;失败或输入结束时返回 EOF。
  • putchar 把字符写入标准输出,成功时返回写出的字符值,写入失败时返回 EOF。

它们每次处理一个字符,但不意味着每次调用都会触发一次真实的设备读取。标准库可能一次取入一批数据,再逐次交给程序。

2.2 字符值应先用 int 保存

char 用来保存字符数据;int 则用于接收字符输入函数的返回值,保留区分普通字符与 EOF 的能力。

不要先把 getchar() 的结果缩窄到 char,再尝试判断文件是否结束。无论普通 char 默认有符号还是无符号,都可能因此丢失正确区分的信息。

使用组合表达式时,赋值部分需要括号:

c
while ((ch = getchar()) != EOF) {
    putchar(ch);
}

否则,ch = getchar() != EOF 会把比较结果 0 或 1 赋给 ch。

3. 缓冲、终端回显与按键响应

3.1 为什么常常要按回车

典型的终端输入过程中,用户先编辑一行,按下回车后,这一行才变成程序可以读取的数据。回车还会产生行结束信息,程序通常读到 \n。

缓冲使数据可以成批传输,也允许用户在提交之前修正输入。

text
键盘输入与行编辑 → 终端输入机制 → 标准输入流 → 程序读取

终端本身的行处理与 C 标准库的流缓冲不是完全相同的一层机制,理解时不要把它们全部归结为某个函数的行为。

3.2 回显不等于程序已经读取

终端可能在用户按键时就把文字显示出来,这是终端回显。程序之后再用 putchar 输出一遍,是另一件事。

因此,屏幕上已经出现输入文字,并不能证明程序已经完成 getchar 调用。类似地,文件重定向时没有键盘回显,也不影响读取内容。

3.3 标准库中的缓冲模式

模式 基本认识
无缓冲 标准库不按较大数据块积累该流的内容
行缓冲 与换行等事件相关地传输内容
全缓冲 以较大的缓冲块组织传输,文件操作中很常见

不能把全缓冲理解为“必须等文件内容刚好填满整个缓冲区,程序才允许读取”。到达输入末尾时,已有的数据仍然可以被交给程序。

输出可能暂存在缓冲区中。没有换行的交互提示,可以明确刷新:

c
printf("Enter a value: ");
fflush(stdout);

fflush(stdout) 用于输出刷新,不应把 fflush(stdin) 当作标准 C 中清理键盘输入的通用方法。

3.4 无回显、即时按键不是换一个普通读取函数就能保证

getchar 不保证按下一个键就立即返回。游戏按键、密码输入等需求,可能需要终端或操作系统专用接口。

某些实现提供 getch、getche 等非标准接口,不能把它们当成所有 C 环境都存在的库函数。仅调整标准库缓冲,也不必然改变终端的行编辑规则。

4. EOF 是读取状态,不是普通字符

4.1 不用占用正文中的特殊符号

用 # 作为结束标记很简单,但会让正文中的 # 无法正常输入。检测输入结束,则不必为程序保留一个正文字符。

内容或状态 含义
字符 '#' 普通字符,只有程序约定后才代表停止
字符 '\0' 数值为零的字符,不等于文件结束
字符序列 EOF 用户输入的三个字母,不会自动结束输入
文本 -1 两个字符,不等于函数返回的特殊状态
宏 EOF <stdio.h> 定义的负整数常量,表示相关输入输出失败状态

应使用宏名,不把它写死为 -1。文件结束也不要求磁盘文件的最后一定保存一个特殊“EOF 字符”。

4.2 结束与错误需要进一步区分

getchar 返回 EOF,可能是读到末尾,也可能是发生读取错误。

读取结束后,可以检查:

  • feof(stdin):流的文件结束指示器是否已置位。
  • ferror(stdin):流的错误指示器是否已置位。

这些函数报告已经发生的状态,不负责预测下一次读取是否成功。因此,不应先用 while (!feof(stdin)) 再无条件处理读取结果。

正常写法是先执行读取,根据返回值判断是否获得数据,再处理。

4.3 从终端产生输入结束条件

在常见的 macOS、UNIX、Linux 终端中,可以在空行处按 Ctrl+D,让等待的读取看到输入结束;若当前行已有内容,可能先把已有内容交给程序。

传统 Windows 控制台环境常使用 Ctrl+Z 后按回车。具体按键和行为取决于终端、IDE 和运行环境,不属于 C 语言统一规定。

Ctrl+C 通常用于中断程序,和正常读取到输入结束是不同的路径。

5. 一个可以用于重定向的字符复制程序

c
#include <stdio.h>

int main(void)
{
    int ch;

    while ((ch = getchar()) != EOF) {
        if (putchar(ch) == EOF) {
            return 1;
        }
    }

    if (ferror(stdin)) {
        fprintf(stderr, "Input error.\n");
        return 1;
    }

    if (fflush(stdout) == EOF) {
        return 1;
    }

    return 0;
}

保存为 stream_echo.c,在已经提供 cc 的 macOS 或类 UNIX 命令行环境中构建:

sh
cc -std=c11 -Wall -Wextra stream_echo.c -o stream_echo

它把标准输入的数据送到标准输出,不额外输出交互提示。输入错误信息则写到标准错误流。

最后检查输出刷新结果,是因为部分写入错误可能直到缓冲区真正提交时才暴露。

这个程序按标准文本流工作。在会转换文本换行等表示的环境中,不能直接把它当作保证逐字节不变的任意二进制文件复制工具。

6. 重定向:让同一个程序处理不同输入输出

6.1 重定向由命令行环境处理

以下以支持相应语法的类 UNIX shell 为例:

命令 效果
./stream_echo 使用当前标准输入、标准输出
./stream_echo < input.txt 从文件读取,结果显示到标准输出
./stream_echo > output.txt 把标准输出写入文件
./stream_echo < input.txt > copy.txt 从一个文件读取,写到另一个文件
./stream_echo >> output.txt 把标准输出追加到文件末尾

<、> 等符号由 shell 解释,不是传给 getchar 的字符串。程序只需要遵守标准输入输出接口。

单独写 ./stream_echo input.txt,并不会自动等价于输入重定向;除非程序另外实现了命令行参数处理。

6.2 > 通常会先截断已有文件

不要把输入和输出设为同一实际文件,例如:

text
错误示例:./stream_echo < input.txt > input.txt

shell 通常在程序开始读取前就为输出打开文件并截断内容。即使两个路径名称不同,若它们实际指向同一文件,也可能遇到相同问题。

对于分别作用于输入和输出的简单重定向,调换书写顺序通常不影响目的;但更一般的重定向存在顺序规则,不能推导为所有重定向都与顺序无关。

6.3 管道连接程序

sh
./stream_echo < input.txt | wc -c

管道把前一个程序的标准输出连接到后一个程序的标准输入。这里由 wc -c 统计接收到的字节数。

< 通常为程序选择输入文件,> 为程序选择输出文件,管道则连接两个程序的数据流。

提示文字若也写入标准输出,会混入被处理的数据。设计可用于管道的工具时,应区分正常数据与交互提示、诊断信息。

7. 在程序里打开文件

除了外部重定向,程序也可以显式打开一个文件,取得关联的流,再通过这个流读取。

c
#include <stdio.h>

int main(void)
{
    FILE *input = fopen("input.txt", "r");
    if (input == NULL) {
        fprintf(stderr, "Cannot open input.txt.\n");
        return 1;
    }

    int ch;
    int status = 0;

    while ((ch = fgetc(input)) != EOF) {
        if (putchar(ch) == EOF) {
            status = 1;
            break;
        }
    }

    if (ferror(input)) {
        status = 1;
    }
    if (fclose(input) == EOF) {
        status = 1;
    }
    if (fflush(stdout) == EOF) {
        status = 1;
    }

    return status;
}

这里先了解几个接口即可,文件处理的细节在后续章节继续展开:

部分 作用
FILE * 标准 I/O 用来引用流的接口类型
fopen(..., "r") 以文本读取模式打开文件
NULL 打开失败时返回的空指针值
fgetc(input) 从指定流读取字符,getc 也提供相应读取接口
fclose(input) 关闭程序打开的流

getchar 对应从 stdin 读取;fgetc 则允许明确指定流。

相对路径 input.txt 通常根据进程的当前工作目录解析,不是必然根据可执行文件所在目录解析。

8. 混合数值输入与字符输入

8.1 换行留下来,不等于读取函数出错

c
int age = 0;
scanf("%d", &age);
int next = getchar();

如果输入 20 后按回车:

text
最初输入:'2' '0' '\n'
scanf 后:          '\n'
getchar:读取这个换行

数值转换读取了数字,换行不是该整数的一部分,因而留给后续读取。getchar 又不自动跳过空白,所以会立即取得它。

这不是两个函数不能混用,而是必须清楚每次调用消费了哪些字符。

8.2 一个按键回复可能被误读成两次回答

如果用户输入 n 再回车,流里有 'n' 和 '\n'。若程序每读一个字符就当作一次否定回复,就可能把猜测连续推进两次。

输入 no 时,还会多出 'o'。所以,无条件再调用一次 getchar,不能普遍解决整行残留问题。

应该先确定交互约定:读取整行中的第一个有效命令后,是保留剩余内容,还是明确丢弃本行其余内容。

8.3 三种处理方式

需求 处理思路
只要下一个非空白字符 可以用 scanf(" %c", &ch),并检查返回值
每行只接受一个回复 取出回复后,消费到换行或输入结束
需要验证整行内容 先按行读取,再解析这一行

scanf(" %c", &ch) 会跳过前导空白,但不会自动丢弃这个字符后面的其他输入。它也不能用于把空格本身作为有效回复的场景。

在数值格式后追加空格或 \n,也不等于“只吃掉一个回车”;那可能继续等待后面的非空白内容。

9. 消费本行剩余内容

9.1 同时检查换行和 EOF

c
void discard_line(void)
{
    int ch;

    while ((ch = getchar()) != '\n' && ch != EOF) {
        /* 消费当前行剩余字符 */
    }
}

这个函数会消费换行本身;如果最后一行没有换行,也能在输入结束处停止。

只写 while (getchar() != '\n'),在输入已结束时可能永远等不到换行,形成重复读取失败的循环。

9.2 什么时候可以调用

适合调用的前提是:程序已经决定不再使用本行剩下的内容。

例如,菜单约定每行只取一个命令,或者某行数值格式错误,需要丢弃整行后让用户重试。

如果用户允许在同一行输入多个待处理值,无条件丢弃整行就会丢失有效数据。恢复方式必须与输入协议一致。

这个函数不是清空所有未来输入,更不是操作系统层面的键盘重置。它只是按当前输入流顺序消费到行边界。

10. 输入验证分成几层

层次 要检查什么 例子
是否得到输入 有数据、输入结束还是读取错误 读取函数是否返回成功状态
能否解析 字符是否能按指定形式转换 hello 不能作为 %d 的整数
是否符合完整格式 是否允许多余字符、多个值 72abc 是否应当拒绝
是否满足范围 数值是否落在业务区间 分数应在 0 到 100
数据间关系 多个值能否共同构成有效任务 下限不能大于上限

仅仅读取成功,不代表其他层次全部成立。

10.1 scanf 的状态判断

对一次普通单整数读取:

  • 返回 1:完成该项转换并赋值。
  • 返回 0:发生匹配失败,没有完成赋值。
  • 返回 EOF:首次转换尚未完成,就遇到输入结束或读取错误。

多项读取还可能返回部分成功数量。不能简单认为“只要非零,就读到了全部数据”。

10.2 数值前缀不等于整行有效

输入 72abc,%d 可以成功取得整数 72,把 abc 留在流中。

如果后面直接丢弃余下内容,就相当于接受了数值前缀,而不是证明用户输入了一整行纯整数。

同样,数值过大而无法由目标类型表示时,不能指望用 scanf 返回值来完成可靠的范围检测。需要严格解析时,可以采用 fgets 读取一行,再配合 strtol 等函数检查范围、尾部内容与转换状态。

这里先建立区别,相关接口的完整用法在后续继续展开。

10.3 失败后必须改变输入状态

text
读取失败
  ↓
判断是否输入结束或错误
  ├─ 是 → 退出或交给上层处理
  └─ 否 → 消费不匹配内容 → 给出提示 → 重新读取

如果第一次读取在 'x' 处失败,既不消费 'x',也不结束,就会在同一位置再次失败。

输入结束也不能被当成“请重试的普通错误”。没有新数据可读时反复提示,只会制造无效循环。

11. 数值本身与多个数值之间的约束

假设任务接受一个闭区间,允许端点落在 [-1000, 1000] 内:

c
bool valid_limits(long first, long last)
{
    return first >= -1000L && first <= last && last <= 1000L;
}

包含 <stdbool.h> 后,这个函数返回区间是否满足要求。由于已经要求 first <= last,这些条件也限制了两个端点的另一侧范围。

如果区间有效,再交给计算函数:

c
double sum_squares(long first, long last)
{
    double total = 0.0;

    for (long value = first; value <= last; ++value) {
        total += (double)value * value;
    }

    return total;
}

这个实现按前述范围和顺序约定使用。例如,区间 [2, 4] 返回 29.0。

(double)value 在乘法之前转换操作数,避免先按整数类型完成平方,再把结果转换为浮点数。限定端点也避免让循环更新走到整数类型极限之外。

如果用 (0, 0) 表示退出,需要判断“两者同时为零”,或者把继续条件写成 first != 0 || last != 0。只要求其中一个为零时也退出,是另一种协议。

输入函数负责取得数据,验证函数负责检查约束,计算函数负责按约定完成任务。把这些工作分开,能避免菜单与具体计算互相缠绕。

12. 输入函数也需要返回“没有数据”的信息

只设计一个“不断重试、最终返回整数”的函数,容易遗漏输入结束的情况。

若正常结果允许是任意整数,就不能拿某个同样可能有效的数值表示失败。可以采用独立状态与输出参数等接口,后续学习指针后会更方便实现。

如果函数的合法值域明确受限,则可以选择值域外的状态。例如下面的菜单只接受 0 到 100 的分数,所有合法结果都非负;负值 EOF 可以明确表示输入已经不能继续。

这是一项函数接口约定,不应推广为所有整数输入函数都能安全用 EOF 代替结果。

13. 综合示例:菜单与数值输入相互配合

13.1 先约定每一行怎样使用

  • 菜单忽略前导空白,使用每行第一个非空白字符,其余部分丢弃。
  • 菜单支持 h、s、q,大小写均可。
  • 分数输入接受能由 int 表示的数值前缀,之后丢弃本行剩余内容。
  • 成功读取后,再检查分数是否在 0 到 100。
  • 输入结束时退出,不再反复要求重试。

因此,score 可以作为以 s 开头的菜单回复;分数行 72abc 在这个协议中会接受为 72。若需求要求拒绝它,应改用整行解析,而不是声称这里已经验证了纯整数格式。

13.2 完整程序

c
#include <stdio.h>
#include <ctype.h>

void discard_line(void);
int read_first(void);
int get_choice(void);
int read_score(void);

int main(void)
{
    int choice;

    while ((choice = get_choice()) != EOF && choice != 'q') {
        switch (choice) {
        case 'h':
            printf("Choose s to enter a score, or q to quit.\n");
            break;
        case 's': {
            int score = read_score();
            if (score == EOF) {
                printf("Input ended.\n");
                return ferror(stdin) ? 1 : 0;
            }
            printf("Accepted: %d\n", score);
            break;
        }
        default:
            printf("Unexpected choice.\n");
            return 1;
        }
    }

    printf("Bye.\n");
    return ferror(stdin) ? 1 : 0;
}

void discard_line(void)
{
    int ch;

    while ((ch = getchar()) != '\n' && ch != EOF) {
        /* 本行剩余内容不再作为下一次输入 */
    }
}

int read_first(void)
{
    int ch;

    do {
        ch = getchar();
    } while (ch != EOF && isspace(ch));

    if (ch == EOF) {
        return EOF;
    }

    discard_line();
    return tolower(ch);
}

int get_choice(void)
{
    for (;;) {
        printf("h=help, s=score, q=quit:\n");
        int choice = read_first();

        if (choice == EOF || choice == 'h' ||
            choice == 's' || choice == 'q') {
            return choice;
        }

        printf("Unknown command. Try again.\n");
    }
}

int read_score(void)
{
    for (;;) {
        int score = 0;

        printf("Enter an integer score from 0 to 100:\n");
        int status = scanf("%d", &score);

        if (status == EOF) {
            return EOF;
        }

        discard_line();

        if (status != 1) {
            printf("Not an integer. Try again.\n");
            continue;
        }

        if (score < 0 || score > 100) {
            printf("Out of range. Try again.\n");
            continue;
        }

        return score;
    }
}

13.3 函数之间的分工

函数 承担的任务
discard_line 消费当前行剩余内容,处理没有换行的输入末尾
read_first 得到第一个非空白命令字符,统一为小写
get_choice 展示菜单,只交回有效选项或结束状态
read_score 处理数值匹配与分数范围,必要时重试
main 根据有效选项安排动作,管理退出路径

菜单不必猜测数值读取之后是否还留着换行,因为数值输入函数按约定已经消费该行其余内容。数值输入函数也不需要知道后面有哪些菜单项。

read_first 的字符变量是 int,能够返回 EOF。传给 isspace 和 tolower 的值,来自未缩窄的 getchar 返回结果,并已按需要判断输入结束。

13.4 一次正常与错误交织的输入

依次输入:

text
s
hello
120
72
q

程序会依次提示格式错误、范围错误,接受 72,然后回到菜单并退出。

如果在要求分数时输入结束,也会退出。输入的最后一行没有换行时,消费行尾的函数同样能够停止。

保存为 input_menu.c,可以在已有 cc 的 macOS 或类 UNIX 环境中编译运行:

sh
cc -std=c11 -Wall -Wextra input_menu.c -o input_menu
./input_menu

这个例子展示的是基本格式恢复和接口组织。更严格的整行校验、超长输入检测、数值转换范围检查,可以在保持这些职责划分的基础上继续改进。

Aron 的清风日常

作者 Aron · 发布于 2026.07.26

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:语法与标准库速查 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭