第八章 字符输入/输出和输入验证

GitHub地址,欢迎 star


在涉及计算机的话题时,我们经常会提到 输入(input)输出(output) 。I / O 函数(如 printf()、scanf()、getchar()、putchar() 等)负责把信息传送到程序中。前几章简单介绍过这些函数,本章将详细介绍它们的基本概念。同时,还会介绍如何设计与用户交互的界面。

8.1 单字符 I/O:getchar() 和 putchar()

getchar() 和 putchar() 每次只处理一个字符。你可能认为这种方法实在太笨拙了,毕竟与我们的阅读方式相差甚远。但是,这种方法很适合计算机。而且,这是绝大多数文本处理程序所用的核心方法。写个简单的程序回忆一下

#include <stdio.h>
int main(void)
{
    char ch;
    while((ch = getchar() != '#'))
        putchar(ch);
    return 0;
}

8.2 缓冲区

回显示用户输入的字符后立即重复打印该字符是属于 无缓冲 输入,即正在等待的程序可立即使用输入的字符。大部分系统在用户按下 Enter 键之前不会重复打印刚输入的字符,这种输入形式属于 缓冲 输入。用户输入的字符被收集并储存在一个被称为 缓冲区(buffer) 的临时存储区,按下 Enter 键后,程序才可以使用用户输入的字符。如图所示:
缓冲输入和无缓冲输入
为什么要有缓冲区?首先,把若干字符作为一个块进行传输比逐个发送这些字符节约时间。其次,如果用户打错字符,可以直接通过键盘修正错误。当最后按下 Enter 键时,传输的是正确的输入。

虽然缓冲输入好处很多,但是某些交互式程序也需要无缓冲输入。例如,在游戏中。因此,缓冲输入和无缓冲输入都有用武之地。

缓冲分为两类:完全缓冲 I/O行缓冲 I/O。完全缓冲输入指的书当缓冲区被填满时才刷新缓冲区,通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是 512 字节和 4096 字节。行缓冲 I/O 指的是在出现换行符时刷新缓冲区。键盘输入通常书行缓冲输入,所以在按下 Enter 键后才刷新缓冲区。

ANSI C 决定把缓冲输入作为标准的原因是:一些计算机不允许无缓冲输入。如果你的计算机允许无缓冲输入,那么你所用的 C 的编译器很可能会提供一个无缓冲输入的选项。ANSI 没有提供调用无缓冲输入的标准方式,这意味着是否能进行无缓冲输入取决于计算机系统。

8.3 结束键盘输入

在 echo.c 程序中,只要输入的字符中不含 # ,那么程序在读到 # 时才会结束。但是,# 也是一个普通的字符,有时不可避免要用到。应该用一个在文本中用不到的字符来标记输入完成,这样的字符不会无意间出现在输入中,在你不希望结束程序的时候终止程序。C 的确提供了这样的字符,不过在此之前,先来了解一下 C 处理文件的方式。

8.3.1 文件、流和键盘输入

文件(file)是存储器中储存信息的区域。通常,文件都保存在某种永久存储器中。毫无疑问,文件对于计算机系统相当重要。某些程序需要访问指定的文件。当编译储存在名为 echo.c 文件中的程序时,编译器打开 echo.c 文件并读取其中的内容。当编译器处理完后,会关闭该文件。

C 是一门强大、灵活的语言,有许多用于打开、读取、写入和关闭文件的库函数。从较低层面上,C 可以使用主机操作系统的基本文件工具直接处理文件,这些直接电筒操作系统的函数被称为底层 I/O。由于计算机系统各不相同,所以不可能为普通的底层 I/O 函数创建标准库,ANSI C 也不打算这样做。然而从较高层面上,C 还可以通过标准 I/O 包来处理文件。这涉及创建用于处理文件的标准模型和一套标准 I/O 函数。在这一层面上,具体的 C 实现负责处理不同系统的差异,以便用户使用统一的界面。

从概念上看,C 程序处理的是流而不是直接处理文件。流(stream)是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程解释把流与文件相关联,而且读写都通过流来完成。

8.3.2 文件结尾

计算机操作系统要以某种方式判断文件的开始和结束。检测文件结尾的一种方法是,在文件末尾放一个特殊的字段标记文件结尾。无论操作系统实际使用何种方法检测文件结尾,在 C 语言中,用 getchar() 读取文件检测到文件结尾时将返回一个特殊的值,即 EOF(end of file 的缩写)。scanf() 函数检测到文件结尾时也返回 EOF。通常 EOF 定义在 stdio.h 文件中:#define EOF (-1)

为什么是 -1?因为 getchar() 函数的返回值通常都介于 0 ~ 127,这些值对应标准字符集。但是,如果系统能识别扩展字符集,该函数的返回值可能在 0 ~ 255 之间。无论哪种情况,-1 都不对应任何字符,所以,该值可用于标记文件结尾。

某些系统也许把 EOF 定义为 -1 以外的值。但是定义的值一定与输入字符所产生的返回值不同。如果包含 stdio.h 文件,并使用 EOF 符号,就不必担心 EOF 值不同的问题。这里关键要理解 EOF 是一个值,标志着检测到文件结尾,并不是在文件中找得到的符号。如下程序:

#include <stdio.h>
int main(void)
{
    char ch;
    while((ch = getchar() != EOF))
        putchar(ch);
    return 0;
}

注意下面几点。

  • 不用定义 EOF,因为 stdio.h 中已经定义过了。
  • 不用担心 EOF 的实际值,因为 EOF 在 stdio.h 中用 #define 预处理指令定义,可直接使用,不必再编写代码假定 EOF 为某值。
  • 变量 ch 的类型从 char 变为 int,因为 char 类型的变量只能表示 0 ~ 255 的无符号整数,但是 EOF 的值是 -1。还好,getchar() 函数实际返回值的类型是 int,所以它可以读取 EOF 字符。如果实现使用有符号的 char 类型,也可以把 ch 声明为 char 类型,但最后还是用更通用的形式。
  • 由于 getchar() 函数的返回类型是 int,如果把 getchar() 的返回值赋给 char 类型的变量,一些编译器会警告可能丢失数据。
  • ch 是整数不会影响 putchar(),该函数仍然会打印等价的字符。
  • 使用该程序进行键盘输入,要设法输入 EOF 字符。不能只输入字符 EOF,也不能只输入 -1(输入 -1 会传送两个字符:一个连字符和一个数字 1)。正确的方法是,必须找出当前系统的要求。

8.4 重定向和文件

输入和输出涉及函数、数据和设备。程序可以通过两种方式使用文件。第 1 中方法是,显示使用特定的函数打开文件、关闭文件、读取文件、写入文件。第 2 种方法是,设计能与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和从文件输出。换言之,把 stdin 流重新赋给文件。继续使用 getchar() 函数从输入流中获取数据,但它并不关心从流的什么位置获取数据。虽然这种重定向的方法在某些方面有些限制,但是用起来比较简单,而且能让读者熟悉普通的文件处理技术。

重定向的一个主要问题与操作系统有关,与 C 无关。尽管如此,许多 C 环境中(包括 UNIX、Linux 和 Windows 命令提示模式)都有重定向特性,而且一些 C 实现还在某些缺乏重定向的系统中模拟它。在 UNIX 上运行苹果 OS X,可以用 UNIX 命令行模式启动 Terminal 应用程序。

8.4.1 UNIX、Linux 和 DOS 重定向

UNIX(运行命令行模式)、Linux(ditto)和 Windows 命令行提示都能重定向输入、输出。重定向输入让程序使用文件而不是键盘来输入,重定向输入让程序输出值文件而不是屏幕。

1、重定向输入

假设已经编译了 echo_eof.c 程序,并把可执行版本放入一个名为 echo_eof(或者在 Windows 系统中名为 echo_eof.exe)的文件中。

该程序的运行情况和前面描述的一样,获取用户从键盘输入的输入。现在,假设你要用该程序处理名为 words 的文本文件。文本文件(text file) 是内含文本的文件,其中储存的数据是我们可识别的字符。文件的内容可以是一篇散文或者 C 程序。内含机器语言指令的文件(如储存可执行程序的文件)不是文本文件。由于该程序的操作对象是字符,所以要使用文本文件。只需用下面的命令代替上面的命令即可:echo_eof<words

< 符号是 UNIX 和 DOS/Windows 的重定向运算符。该运算符是 words 文件与 stdin 流相关联,把文件中的内容导入 echo_eof 程序。echo_eof 程序本身并不知道输入的内容是来自文件还是键盘,它只知道这是需要导入的字符流,所以它读取这些内容并把字符逐个打印在屏幕上,直至读到文件结尾。因为 C 把文件和 I/O 设备放在一个层面,所以文件就是现在的 I/O设备。

注意 重定向

对于 UNIX、Linux 和 Windows 命令提示:<两侧的空格是可选的。一些系统,如AmigaDOS,支持重定向,但是在重定向符号和文件名之间不允许有空格。

下面是一个特殊的 words 文件的运行示例, $ 是 UNIX 和 Linux 的标准提示符。在 Windows/DOS 系统中见到的 DOS 提示可能是 A> 或 C>。
$ echo_eof < words
The world is too much with us: late and soon,
Getting and spending, we lay waste our powers:
Little we see in Nature that is ours;
We have given our hearts away, asordid boon!
$

2、重定向输出

现在假设要用 echo_eof 把键盘输入的内容发送到名为 mywords 的文件中。然后,输入一下命令并开始输入:echo_eof>mywords

> 符号是第 2 个重定向运算符。它创建了一个名为 mywords 的新文件,然后把 echo_eof 的输出(即,你输入字符的副本)重定向至该文件中。重定向把 stdout 从显示设备赋给 mywords 文件。如果已经有一个名为 mywords 的文件,通常会擦除该文件的内容,然后替换新的内容(但是,许多操作系统有保护现有文件的选项,使其成为只读文件)。所以出现在屏幕的字母都是你刚才输入的,其副本储存在文件中。在下一行的开始出按下 Ctrl+D(UNIX)或Ctrl+Z(DOS)即可结束该程序。如果不知道输入什么内容,可参照下面的示例。
$ echo_eof>mywords
You should no problem recalling which redirection
operator does what. Just remember that each operator points
in the direction the information flows. Think of it as
a funnel.
[Ctrl+D]
$
按下 Ctrl+D 或 Ctrl+Z 后,程序会结束,你得系统会提示返回。程序是否起作用了?UNIX 的 ls 命令或 Windows 命令行提示模式的 dir 命令可以列出文件名,会显示 mywords 文件已存在。可以使用 UNIX 或 Linux 的 cat 或 DOS 的 type 命令检查文件中的内容,或者再次使用 echo_eof,这次把文件重定向到程序:
$ echo_eof<mywords
You should have no problem recalling which redirection
operator does what. Just remember that each operator points
in the direction the information flows. Think of it as a
funnel.
$

3、组合重定向

现在,假设你希望制作一份 mywords 文件的副本,并命名为 savewords。只需输入一下命令即可:echo_eof < mywords > savewords

下面的命令也起作用,因为命令与重定向运算符的顺序无关:echo_eof > savewords < mywords

注意:在一条命令中,输入文件名和输出文件名不能相同。原因是 > mywords 在输入之前已导致原 mywords 的长度被截断为 0。

总之,在UNIX、Linux 或 Windows/DOS 系统中使用两个重定向运算符(< 和 >)时,要遵循一下原则。

  • 重定向运算符链接一个可执行程序(包括标准操作系统命令)和一个数据文件,不能用于连接一个数据文件和另一个数据文件,也不能用于连接一个程序和另一个程序。
  • 使用重定向运算符不能读取多个文件的输入,也不能把输出定向至多个文件。
  • 通常,文件名和运算符之间的空格不是必须的,除非是偶尔在 UNIX shell、Linux shell 或 Windows 命令行提示模式中使用的特殊含义的字符。

以上介绍的都是正确的例子,下面来看一下错误的例子,addup 和 count 是两个可执行程序,fish 和 beets 是两个文本文件:
fish > beets(违反第1条规则)
addup < count(违反第1条规则)
addup < fish < beets(违反第2条规则)
count > beets fish(违反第2条规则)

UNIX Linux 或 Windows/DOS 还有 >> 运算符,该运算符可以把数据添加到现有文件的末尾,而 | 运算符能把一个文件的输出连接到另一文件的输入。

4、注释

重定位让你能使用键盘输入程序文件。要完成这一任务,程序要测试文件的末尾。例如,第 7 章演示的统计单词程序,计算单词个数直至遇到第 1 个 | 字符。把 ch 的 char 类型改成 int 类型,把循环测试中的 | 替换成 EOF,便可用该程序来计算文本文件的单词量。

重定向是一个命令行概念,因为我们要在命令行输入特殊的符号发出指令。如果不使用命令行环境,也可以使用重定向。首先,一些集成开发环境提供了菜单选项,让用户指定重定向。其次,对于 Windows 系统,可以打开命令提示窗口,并在命令行运行科执行文件。Microsoft Visual Studio 的默认设置是把可执行文件放在项目文件夹的子文件夹,称为 Debug。文件名和项目名的基本名相同,文件名的扩展名为 .exe。默认情况下,Xcode 在给项目命名后才能命名可执行文件,并将其放在 Debug 文件夹中。在 UNIX 系统中,可以通过 Terminal 工具运行可执行文件。从使用上看,Terminal 比命令行编译器(GCC 或 Clang)简单。

如果用不了重定向,可以用程序直接打开文件。如下程序,待读取文件应该与可执行文件位于同一目录。

#include <stdio.h>
#include <stdlib.h> // 为了使用 exit()
int main(void)
{
    int ch;
    FILE *fp;
    char fname[50]; // 储存文件名

    printf("Enter the name of the file: ");
    scanf("%s",fname);
    fp = fopen(fname,"r"); // 打开待读取文件
    if(fp == NULL) // 如果失败
    {
        printf("Failed to open file. Bye\n");
        exit(1); // 退出程序
    }
    while((ch = getc(fp)) != EOF)
        putchar(ch);
    fclose(fp); // 关闭文件
    return 0;
}
小结:如何重定向输入和输出

绝大部分 C 系统都可以使用重定向,可以通过操作系统重定向所有程序,或只在 C 编译器允许的情况下重定向 C 程序。假设 prog 是可执行程序名,file1 和 file2 是文件名。

把输出重定向至文件:>

prog > file1

把输入重定向至文件:

prog < file2

组合重定向:

prog < file2 > file1
prog > file1 < file2
这两种形式都是把 file2 作为输入、file1 作为输出。
留白:
一些系统要求重定向运算符左侧有一个空格,右侧没有空格。而其他系统(如、UNIX)允许在重定向运算符两侧有空格或没有空格。

8.5 创建更友好的用户界面

大部分人偶尔会写一些中看不中用的程序。还好,C 提供了大量工具让输入更顺畅,处理过程更顺利。不过,学习这些工具会导致新的问题。本节的目标是,知道读者解决这些问题并创建更友好的用户界面,让交互数据输入更方便,减少错误输入的影响。

8.5.1 使用缓冲输入

缓冲输入用起来比较方便,因为在把输入发送给程序之前,用户可以编辑输入。但是,在使用输入的字符是,它也会给程序员带来麻烦。前面示例中看到的问题是,缓冲输入要求用户按下 Enter 键发送输入。这一动作也传送了换行符,程序必须妥善处理这个麻烦的换行符。我们以一个猜谜程序为例。用户选择一个数字,程序猜用户选中的数字是多少。该程序使用的方法单调乏味,先不要在意算法,我们关注的重点在输入和输出。程序 8.4,这是猜谜程序的最初版本,后面我们会改进。

#include <stdio.h>
int main(void)
{
    int guess = 1;

    printf("Pick an integer from 1 to 100. I will tru to guess ");
    printf("it.\nRespond with a y if my guess is right and with");
    printf("\nan n if it is wrong.\n");
    printf("Uh...is your number %d?\n",guess);
    while(getchar() != 'y')
        printf("Well, then, is it %d?\n",++guess);
    printf("I knew I could do it!\n");

    return 0;
}

Pick an integer from 1 to 100. I will tru to guess it.
Respond with a y if my guess is right and with
an n if it is wrong.
Uh…is your number 1?
n
Well, then, is it 2?
Well, then, is it 3?
n
Well, then, is it 4?
Well, then, is it 5?
y
I knew I could do it!

在编写交互式程序时,应该事先预料到用户可能会输入错误,然后设计程序处理用户的错误输入。在用户出错是通知用户再次输入。

当然,无论你得提示写得多么清楚,总会有人误解,然后抱怨这个程序设计得多么糟糕。

8.5.2 混合数值和字符输入

假设程序要求用 getchar() 处理字符输入,用 scanf() 处理数值输入,这两个函数都能很好地完成任务,但是不能把它们混用。因为 getchar() 读取每个字符,包括空格、制表符和换行符;而 scanf() 在读取数字时会跳过空格、制表符和换行符。

我们通过程序来解释这种情况导致的问题。改程序读入一个字符和两个数字,然后根据输入的两个数字指定的行数和列数打印该字符。

/** 有较大 I/O 问题的程序 */
#include <stdio.h>
void display(char cr,int lines,int width);
int main(void)
{
    int ch; /* 待打印字符 */
    int rows,cols; /* 行数和列数 */
    printf("Enter a character and two integers:\n");
    while((ch = getchar()) != '\n')
    {
        scanf("%d %d",&rows,&cols);
        display(ch,rows,cols);
        printf("Enter another character and two integers;\n");
        printf("Enter a newline to quit.\n");
    }
    printf("Bye.\n");
    return 0;
}

void display(char cr,int lines,int width)
{
    int row,col;
    for(row = 1; row <= lines; row++)
    {
        for(col = 1; col <= width; col++)
        {
            putchar(cr);
        }
        putchar('\n'); /* 结束一行并开始新的一行 */
    }
}

注意,该程序以 int 类型读取字符(这样做可以检测 EOF),但是却以 char 类型把字母传递给 display() 函数。因为 char 比 int 小,一些编译器会给出类型转换的警告。可以忽略这些警告,或者用下面的强制类型转换消除警告:display(char(ch),rows,cols);

在该程序中,main() 负责获取数据,display() 函数负责打印数据。下面是该程序的一个运行示例,看看有什么问题:
Enter a character and two integers:
c 2 3
ccc
ccc
Enter another character and two integers;
Enter a newline to quit.
Bye.

该程序开始时运行良好。你输入 c 2 3 ,程序打印 c 字符 2 行 3 列。然后,程序提示输入第 2 组数据,还没等你输入数据程序就退出了!这是什么情况?有数换行符在捣乱,这次是输入中紧跟在 3 后面的换行符。scanf() 函数把这个换行符留在输入队列中。和 scanf() 不同。getchar() 不会跳过换行符,所以在进入下一轮迭代时,你还没来得及输入字符,它就读取了换行符,然后将其赋给 ch 。而 ch 是换行符正式终止循环的条件。

要解决这个问题,程序要跳过一轮输入结束与下一轮输入开始之间的所有换行符或空格。另外,如果该程序不在 getchar() 测试时,而在 scanf() 阶段终止程序会更好。修改后的版本如下:

/** 按指定的行列打印字符 */
#include <stdio.h>
void display(char cr,int lines,int width);
int main(void)
{
    int ch; /* 待打印字符 */
    int rows,cols; /* 行数和列数 */
    printf("Enter a character and two integers:\n");
    while((ch = getchar()) != '\n')
    {
        if( scanf("%d %d",&rows,&cols) != 2)
            break;
        display(ch,rows,cols);
        while(getchar() != '\n')
            continue;
        printf("Enter another character and two integers;\n");
        printf("Enter a newline to quit.\n");
    }
    printf("Bye.\n");
    return 0;
}

void display(char cr,int lines,int width)
{
    int row,col;
    for(row = 1; row <= lines; row++)
    {
        for(col = 1; col <= width; col++)
        {
            putchar(cr);
        }
        putchar('\n'); /* 结束一行并开始新的一行 */
    }
}

Enter a character and two integers:
c 1 2
cc
Enter another character and two integers;
Enter a newline to quit.
! 3 6
!!!
!!!
!!!
Enter another character and two integers;
Enter a newline to quit.

Bye.

在 if 语句中使用一个 break 语句,可以在 scanf() 的返回值不等于 2 时终止程序,即如果一个或两个输入值不是整数或者遇到文件结尾就终止程序。

8.6 输入验证

程序使用了上面的两个函数为一个进行算术的函数提供整数,该函数计算特定范围内所有整数的平方和。程序限制了范围的上限是 10000000,下线是 -10000000。

/** 输入验证 */
#include <stdio.h>
#include <stdbool.h>
// 验证输入是一个整数
long get_long(void);
// 验证范围的上下限是否有效
bool bad_limits(long begin,long end,long low,long high);
// 计算 a ~ b 之间的整数平方和
double sum_squares(long a,long b);
int main(void)
{
    const long MIN = -10000000L; // 范围的下限
    const long MAX = +10000000L; // 范围的上限
    long start; // 用户指定的范围最小值
    long stop; //用户指定的范围最大值
    double answer;

    printf("This program computes the sum of the squares of "
           "integers in a range.\nThe lower bound should not "
           "be less than -10000000 and\nthe upper bound "
           "should not be more than +10000000.\mEnter the "
           "limits (enter o for both limits to quit):\n"
           "lower limit: ");
    start = get_long();
    printf("upper limit: ");
    stop = get_long();
    while(start != 0 || stop != 0)
    {
        if(bad_limits(start,stop,MIN,MAX))
            printf("Please try again.\n");
        else
        {
            answer = sum_squares(start,stop);
            printf("The sum of the squares of the integers ");
            printf("form %ld to %ld is %g\n", start, stop, answer);
        }
        printf("ENter the limits (enter 0 for both "
                   "limits to quit):\n");
        printf("lower limit: ");
        start = get_long();
        printf("upper limit: ");
        stop = get_long();

    }
    printf("Done.\n");

    return 0;
}

long get_long(void)
{
    long input;
    char ch;
    while(scanf("%ld",&input) != 1)
    {
        while((ch = getchar()) != '\n')
            putchar(ch); // 处理错误输入
        printf(" in not an integer.\nPlease enter an ");
        printf("integer value, such as 25, -178, or 3: ");
    }
    return input;
}

bool bad_limits(long begin,long end,long low,long high)
{
    bool not_good = false;
    if(begin > end)
    {
        printf("%ld isn't smaller than %ld.\n", begin, end);
        not_good = true;
    }
    if(begin < low || end < low)
    {
        printf("Values must be %ld or greater.\n", low);
        not_good = true;
    }
    if(begin > high || end > high)
    {
        printf("Values must be %ld or less.\n", high);
        not_good = true;
    }
    return not_good;
}

double sum_squares(long a,long b)
{
    double total = 0;
    long i;

    for(i = a; i <= b; i++)
        total += (double)i * (double)i;
    return total;
}

This program computes the sum of the squares of integers in a range.
The lower bound should not be less than -10000000 and
the upper bound should not be more than +10000000.mEnter the limits (enter o for both limits to quit):
lower limit: low
low in not an integer.
Please enter an integer value, such as 25, -178, or 3: 3
upper limit: a big number
a big number in not an integer.
Please enter an integer value, such as 25, -178, or 3: 12
The sum of the squares of the integers form 3 to 12 is 645
ENter the limits (enter 0 for both limits to quit):
lower limit: 80
upper limit: 10
80 isn’t smaller than 10.
Please try again.
ENter the limits (enter 0 for both limits to quit):
lower limit: 0
upper limit: 0
Done.

8.6.1 分析程序

虽然程序的核心计算部分(sum_squares() 函数)很短,但是输入验证部分比以往程序示例要复杂。接下来分析其中的一些要素,先着重讨论程序的整体结构。

程序遵循模块化的编程思想,使用独立函数(模块)来验证输入和管理显示。程序越大,使用模块化编程就越重要。

main() 函数管理程序流,为其他函数委派任务。它使用 get_long() 获取值、while 循环处理值、badlimits() 函数检查是否有效、sum_squres() 函数处理实际的计算。

8.6.2 输入流和数字

在编写处理错误输入的代码时,应该很清楚 C 是如何处理输入的。考虑下面的输入:is 28 12.4

在我们眼中,这就像是一个有字符、整数和浮点数组成的字符串。但是对 C 程序而言,这是一个字节流。第 1 个字节是字母 i 的字符编码,第 2 个字节是字母 s 的字符编码,第 3 个字节是空格字符的字符编码,第 4 个字符是数字 2 的字符编码,等等。所以,如果 get_long() 函数处理这一行输入,第 1 个字符是非数字,那么整行输入都会被丢弃,包括其中的数字,因为这些数字只是该输入行中的其他字符。

虽然输入流由字符组成,但是也可以设置 scanf() 函数把它们转换成数值。例如,考虑下面的输入:42

如果在 scanf() 函数中石油 %c 转换说明,他只会读取字符 4 并将其储存在 char 类型的变量中。如果使用 %s 转换说明,它会读取字符 4 和字符 2 这两个字符,并将其储存在字符数组中。如果使用 %d 转换说明,scanf() 同样会读取两个字符,但是随后会计算出它们对应的整数值: 4 X 10 + 2,即 42,然后将表示该整数的二进制数储存在 int 类型的变量中。如果使用 %f 转换说明,scanf() 也会读取两个字符,计算出它们对应的数值 42.0,用内部的浮点表示法表示该值,并将结果储存在 float 类型的变量中。

简而言之,输入有字符组成,但是 scanf() 可以把输入转换成整数值或浮点数值。使用转换说明限制了可接受输入的字符类型,而 getchar() 和使用 %c 的 scanf() 接受所有的字符。

8.7 菜单浏览

许多计算机程序都把菜单作为用户界面的一部分。菜单给用户提供方便的同时,却给程序员带来了一些麻烦。

理想状态是,用户输入程序所列选项之一,然后程序根据用户所选项完成任务。作为一名程序员,自然希望这一过程能顺利进行。因此,第 1 个目标是:当用户遵循指令时程序顺利运行;第 2 个目标是:当用户没有遵循指令时,程序也能顺利运行。显而易见,要实现第 2 个目标难度较大,因为很难预料用户在使用程序时的所有错误情况。程序如下:

/** 菜单程序 */
#include <stdio.h>
char get_choice(void);
char get_first(void);
int get_int(void);
void count(void);
int main(void)
{
    int choice;

    while((choice = get_choice()) != 'q')
    {
        switch(choice)
        {
            case 'a': printf("Buy low, sell high.\n");
                    break;
            case 'b': putchar('\a');
                    break;
            case 'c': count();
                    break;
            default: printf("Program error!\n");
                    break;
        }
    }
    printf("Bye.\n");

    return 0;
}

void count(void)
{
    int n,i;
    printf("Count how far? Enter an integer:\n");
    n = get_int();
    for(i = 1; i <= n; i++)
        printf("%d\n",i);
    while(getchar() != '\n')
        continue;
}

char get_choice(void)
{
    int ch;

    printf("Enter the letter of your choice:\n");
    printf("a. advice         b. bell\n");
    printf("c. count          q. quit\n");
    ch = get_first();
    while((ch < 'a' || ch > 'c') && ch != 'q')
    {
        printf("Please respond with a, b, c, or q.\n");
        ch = get_first();
    }
    return ch;
}

char get_first(void)
{
    int ch;

    ch = getchar();
    while(getchar() != '\n')
        continue;

    return ch;
}

int get_int(void)
{
    int input;
    char ch;

    while(scanf("%d", &input) != 1)
    {
        while((ch = getchar()) != '\n')
            putchar(ch); // 处理错误输出
        printf(" is not an integer.\nPlease enter an ");
        printf("integer value, such as 25, -178, or 3: ");
    }

    return input;
}

Enter the letter of your choice:
a. advice b. bell
c. count q. quit
a
Buy low, sell high.
Enter the letter of your choice:
a. advice b. bell
c. count q. quit
count
Count how far? Enter an integer:
two
two is not an integer.
Please enter an integer value, such as 25, -178, or 3: 5
1
2
3
4
5
Enter the letter of your choice:
a. advice b. bell
c. count q. quit
d
Please respond with a, b, c, or q.
q
Bye.

要写出一个自己十分满意的菜单界面并不容易。但是,在开发了一种可行的方案后,可以在其他情况下服用这个菜单界面。

8.8 关键概念

C 程序把输入作为传染的字节流。getchar() 函数把每个字符解释成一个字符编码。scanf() 函数以同样的方式看待输入,但是根据转换说明,他可以把字符输入转换成数值。许多操作系统都提供重定向,允许用文件代替键盘输入,用文件代替显示器输出。

程序通常接收特殊形式的输入。可以在设计程序时考虑用户在输入时可能犯的错误,在输入验证部分处理这些错误情况,让程序更强健更友好。

对于一个小型程序,输入验证可能是代码中最复杂的部分。处理这类问题有多种方案。例如,如果用户输入错误类型的信息,可以终止程序,也可以给用户提供有限次或无限次机会重新输入。

8.9 本章小结

许多程序使用 getchar() 逐字符读取输入。通常,系统使用行缓冲输入,即当用户按下 Enter 键后输入才被传送给程序。按下 Enter 键也传送了一个换行符,编程时要注意吹这个换行符。ANSI C把缓冲输入作为标准。

通过标准 I/O 包中的一系列函数,以统一的方式处理不同系统中的不同文件形式,是 C 语言的特性之一。getchar() 和 scanf() 函数也属于这一系列。当检测到文件结尾时,这两个函数都返回 EOF。在不同系统中模拟文件结尾条件的方式稍有不同。在 UNIX 系统中,在一行开始处按下 Ctrl + D 可以模拟文件结尾条件:而在 DOS 系统中则使用 Ctrl + Z。

许多操作系统都有重定向的特性,因此可以用文件代替键盘和屏幕进行输入和输出。读到 EOF 即停止读取的程序可用于键盘输入和模拟文件结尾信号,或者用于重定向文件。

混合使用 getchar() 和 scanf() 时,如果在调用 getchar() 之前,scanf() 在输入行留下一个换行符,会导致一些问题。不过,意识到这个问题就可以在程序中妥善处理。

编写程序时,要认真设计用户界面。事先预料一些用户可能会犯的错误,然后设计程序妥善处理这些错误情况。

8.10 复习题

1、putchar(getchar()) 是一个有效表达式,它实现什么功能?getchar(putchar()) 是否也是有效表达式?
答案:表达式 putchar(getchar()) 使程序读取下一个输入字符并打印出来。getchar() 的返回值是 putchar() 的参数。但是 getchar(putchar()) 是无效的表达式,因为 getchar() 不需要参数,而 putchar() 需要一个参数。

2、下面的语句分别完成什么任务?
a、putchar(‘H’);
b、putchar(’\007’);
c、putchar(’\n’);
d、putchar(’\b’);
答案:a、显示字符 H;b、如果系统使用 ASCII,则发出一声警报;c、把光标移至下一行的开始;d、后退一格。

3、假设有一个名为 count 的可执行程序,用于统计输入的字符数。设计一个使用 count 程序统计 essay 文件中字符数的命令行,并把统计结果保存在 essayct 文件中。
答案: count <essay >essayct 或者 count >esayct <essay

4、给定复习题 3 中的程序和文件,下面哪一条是有效的命令?
a、essayct <essay
b、count essay
c、essay >count
答案:都不是有效的命令。

5、EOF 是什么?
答案:EOF 是由 getchar() 和 scanf() 返回的信号(一个特殊值),表明函数检测到文件结尾。

6、对于给定的输出(ch 是 int 类型,而且是缓冲输入),下面各程序段的输出分别是什么?
a、输入如下:
If you quit, I will.[enter]
程序段如下:
while((ch = getchar()) != ‘i’)
putchar(ch);
b、输入如下:
Harhar[enter]
程序段如下:
while((ch = getchar() != ‘\n’))
{
putchar(ch++);
putchar(++ch);
}
答案:a、If you qu;b、HJacrthjacrt。

7、C 如何处理不同计算机系统中的不同文件和换行约定?
答案:C 的标准 I/O 库把不同的文件映射为统一的流来统一处理。

8、在使用缓冲输入的系统中,把数值和字符混合输入会遇到什么潜在的问题?
答案:数值输入会跳过空格和换行符,但是字符输入不会。

8.11 编程练习

下面的一些程序要求输入以 EOF 终止。如果你得操作系统很难或根本无法使用重定向,请使用一些其他的测试来终止输入,如读到 & 字符是停止。

1、设计一个程序,统计在读到文件结尾之前读取的字符数。

2、编写一个程序,在遇到 EOF 之前,吧输入作为字符流读取。程序要打印每个输入的字符及其相应的 ASCII 十进制值。注意,在 ASCII 序列中,空格字符前面的字符都是非打印字符,要特殊处理这些字符。如果非打印字符是换行符或制表符,则分别打印 \n 或 \t。否则,使用控制字符表示法。例如,ASCII 的 1 是 Ctrl + A,可显示为 ^A。注意,A 的 ASCII 值是 Ctrl + A 的值加上 64。其他非打印字符也有类似的关系。除每次遇到换行打印新的一行之外,每行打印 10 对值。(注意:不同的操作系统其控制字符可能不同)

3、编写一个程序,在遇到 EOF 之前,吧输入作为字符流读取。改程序要报告输入中的大写字母和小写字母的个数。假设大小写字母数值是连续的。或者使用 ctype.h 库中合适的分类函数更方便。

4、编写一个程序,在遇到 EOF 之前,把输入作为字符流读取。改程序要报告平局每个单词的字母数。不要把空白统计为单词的字母。实际上,表带符号也不应该统计,但是现在暂时不同考虑这么多(如果你比较在意这点,考虑使用 ctype.h 系列中的 ispunct() 函数)。

5、修改程序清单 8.4 的才数字程序,使用更智能的猜测策略。例如,程序最初猜 50,询问用户是猜大了、猜小了还是猜对了。如果猜小了,那么下一次猜测的值应是 50 和 100 中值,也就是 75.如果这次猜大了,那么下一次猜测的值应是 50 和 75 的中值,等等。使用二分查找策略,如果用户没有欺骗程序,那么程序很快就会猜到正确的答案。

6、修改程序清单 8.8 中的 get_first() 函数,让该函数返回读取的第 1 个非空白字符,并在一个简单的程序中测试。

7、修改第 7 章的编程练习 8,用字符代替数字标记菜单的选项。用 q 代替 5 作为结束输入的标记。

8、编写一个程序,显示一个提供加法、减法、除法的菜单。获得用户选择的选项后,程序提示用户输入两个数字,然后执行用户刚才选择的操作。该程序只接受菜单提供的选项。程序使用 float 类型的变量储存用户输入的数字,如果用户输入失败,则允许再次输入。进行除法运算时,如果用户输入 0 作为第 2 个数(除数),程序应提示用户重新输入一个新值。该程序的一个运行示例如下:
Enter the operation of your choice:
a. add s. sunbtract
m. multiply d. divide
q. quit
a
Enter first number: 22.4
Enter second number: one
one is not an number.
Please enter a number, such as 2.5, -1.78E8, or 3: 1
22.4 + 1 = 23.4
Enter the operation of your choice:
a. add s. sunbtract
m. multiply d. divide
q. quit
d
Enter first number: 18.4
Enter second number: 0
Enter a number other than 0: 0.2
18.4 / 0.2 = 92
Enter the operation of your choice:
a. add s. sunbtract
m. multiply d. divide
q. quit
q
Bye.

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值