文章目录
加粗与下面的点是用来标记我的知识盲区,无特殊含义。
- 重定向和文件及之前的点由加粗标记
- 之后的知识应反复学习
字符输入/输出和输入验证
本章内容
本章介绍以下内容:
-
更详细地介绍输入、输出以及缓冲输入和无缓冲输入的区别;
-
如何通过键盘模拟文件结尾条件;
-
如何使用重定向把程序和文件相连接;
-
创建更友好的用户界面。
-
最初,输入/输出函数不是C定义的一部分,C把开发这些函数的任务留给编译器的实现者来完成。
-
在实际应用中,UNIX系统中的C实现为这些函数提供了一个模型。ANSI C库吸取成功的经验,把大量的UNIX I/O函数囊括其中,包括一些我们曾经用过的。
-
由于必须保证这些标准函数在不同的计算机环境中能正常工作,所以它们很少使用某些特殊系统才有的特性。因此,许多C供应商会利用硬件的特性,额外提供一些I/O函数。
-
自从ANSI C标准发布以后,C就把stdio.h头文件与使用getchar()和putchar()相关联,这就是为什么程序中要包含这个头文件的原因(其实,getchar()和putchar()都不是真正的函数,它们被定义为供预处理器使用的宏,我们在第16章中再详细讨论)。
缓冲区
如果在老式系统运行程序清单8.1,你输入文本时可能显示如下:
HHeelllloo, tthheerree… II wwoouulldd[enter]
lliikkee aa #
这样回显用户输入的字符后立即重复打印该字符是属于无缓冲(或直接)输入,即正在等待的程序可立即使用输入的字符。
/* echo.c -- 重复输入 */
#include <stdio.h>
int main(void)
{
char ch;
while ((ch = getchar()) != '#')
putchar(ch);
return 0;
}
Hello, there. I would[enter]
Hello, there. I wouldlike a #3 bag of potatoes.[enter]
like a
- 大部分系统在用户按下Enter键之前不会重复打印刚输入的字符,这种输入形式属于缓冲输入。用户输入的字符被收集并存储在一个被称为缓冲区(buffer)的临时存储区,按下Enter键后,程序才可使用用户输入的字符。
- 缓冲分为两类:完全缓冲I/O和行缓冲I/O。
- 完全缓冲输入指的是当缓冲区被填满时才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是512字节和4096字节。
- 行缓冲I/O指的是在出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入,所以在按下Enter键后才刷新缓冲区。
- ANSI C决定把缓冲输入作为标准的原因是:一些计算机不允许无缓冲输入。如果你的计算机允许无缓冲输入,那么你所用的C编译器很可能会提供一个无缓冲输入的选项。
- 例如,许多IBM PC兼容机的编译器都为支持无缓冲输入提供一系列特殊的函数,其原型都在conio.h头文件中。这些函数包括用于回显无缓冲输入的getche()函数和用于无回显无缓冲输入的getch()函数(回显输入意味着用户输入的字符直接显示在屏幕上,无回显输入意味着击键后对应的字符不显示)。
- UNIX系统使用另一种不同的方式控制缓冲。在UNIX系统中,可以使用ioctl()函数(该函数属于UNIX库,但是不属于C标准)指定待输入的类型,然后用getchar()执行相应的操作。在ANSI C中,用setbuf()和setvbuf()函数(详见第13章)控制缓冲,但是受限于一些系统的内部设置,这些函数可能不起作用。
- 总之,ANSI没有提供调用无缓冲输入的标准方式,这意味着是否能进行无缓冲输入取决于计算机系统。在这里要对使用无缓冲输入的读者说声抱歉,本书假设所有的输入都是缓冲输入。
结束键盘输入
文件、流和键盘输入
- 文件(file)是存储器中存储信息的区域。通常,文件都保存在某种永久存储器中(例如,硬盘、U盘或DVD等)。
- 这些直接调用操作系统的函数被称为底层I/O
- 较高层面上,C还可以通过标准I/O包(standard I/O package)来处理文件。这涉及创建用于处理文件的标准模型和一套标准I/O函数。在这一层面上,具体的C实现负责处理不同系统的差异,以便用户使用统一的界面。
- 有些系统把文件的内容存储在一处,而文件相关的信息存储在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记行末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。有些系统用最小字节来衡量文件的大小,有些系统则以字节块的大小来衡量。
- 如果使用标准I/O包,就不用考虑这些差异。因此,可以用if (ch == ‘\n’)检查换行符。即使系统实际用的是回车符和换行符的组合来标记行末尾,I/O函数会在两种表示法之间相互转换。
- 从概念上看,C程序处理的是流而不是直接处理文件。流(stream)是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。
- 第13章将更详细地讨论文件。本章着重理解C把输入和输出设备视为存储设备上的普通文件,尤其是把键盘和显示设备视为每个C程序自动打开的文件。stdin流表示键盘输入,stdout流表示屏幕输出。getchar()、putchar()、printf()和scanf()函数都是标准I/O包的成员,处理这两个流。
以上讨论的内容说明,可以用处理文件的方式来处理键盘输入。 - 例如,程序读文件时要能检测文件的末尾才知道应在何处停止。因此,C的输入函数内置了文件结尾检测器。既然可以把键盘输入视为文件,那么也应该能使用文件结尾检测器结束键盘输入。
文件结尾
-
检测文件结尾的一种方法是,在文件末尾放一个特殊的字符标记文件结尾。
-
CP/M、IBM-DOS和MS-DOS的文本文件曾经用过这种方法。如今,这些操作系统可以使用内嵌的Ctrl+Z字符来标记文件结尾。这曾经是操作系统使用的唯一标记,不过现在有一些其他的选择,例如记录文件的大小。所以现代的文本文件不一定有嵌入的Ctrl+Z,但是如果有,该操作系统会将其视为一个文件结尾标记。
-
操作系统使用的另一种方法是存储文件大小的信息。如果文件有3000字节,程序在读到3000字节时便达到文件的末尾。 MS-DOS及其相关系统使用这种方法处理二进制文件,因为用这种方法可以在文件中存储所有的字符,包括Ctrl+Z。新版的DOS也使用这种方法处理文本文件。UNIX使用这种方法处理所有的文件。
-
无论操作系统实际使用何种方法检测文件结尾,在C语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF(end of file的缩写)。scanf()函数检测到文件结尾时也返回EOF。
-
#define EOF (-1)
-
为什么是-1?因为getchar()函数的返回值通常都介于0~127,这些值对应标准字符集。但是,如果系统能识别扩展字符集,该函数的返回值可能在0~255。无论哪种情况,-1都不对应任何字符,所以,该值可用于标记文件结尾。
-
这里关键要理解EOF是一个值,标志着检测到文件结尾,并不是在文件中找得到的符号。
while ((ch = getchar()) != EOF)
-
getchar()的返回值和EOF作比较。如果两值不同,就说明没有到达文件结尾。
-
绝大部分系统(不是全部)都有办法通过键盘模拟文件结尾条件。
/* echo_eof.c -- 重复输入,直到文件结尾 */
#include <stdio.h>
int main(void)
{
int 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)。正确的方法是,必须找出当前系统的要求。例如,在大多数UNIX和Linux系统中,在一行开始处按下Ctrl+D会传输文件结尾信号。许多微型计算机系统都把一行开始处的Ctrl+Z识别为文件结尾信号,一些系统把任意位置的Ctrl+Z解释成文件结尾信号。
- 注意 模拟EOF和图形界面
模拟EOF的概念是在使用文本界面的命令行环境中产生的。在这种环境中,用户通过击键与程序交互,由操作系统生成EOF信号。但是在一些实际应用中,却不能很好地转换成图形界面(如Windows和Macintosh),这些用户界面包含更复杂的鼠标移动和按钮点击。程序要模拟EOF的行为依赖于编译器和项目类型。例如,Ctrl+Z可以结束输入或整个程序,这取决于特定的设置。
重定向和文件
- 在默认情况下,C程序使用标准I/O包查找标准输入作为输入源。这就是前面介绍过的stdin流,它是把数据读入计算机的常用方式。它可以是一个过时的设备,如磁带、穿孔卡或电传打印机,或者(假设)是键盘,甚至是一些先进技术,如语音输入。然而,现代计算机非常灵活,可以让它到别处查找输入。尤其是,可以让一个程序从文件中查找输入,而不是从键盘。
- 程序可以通过两种方式使用文件。
- 第1种方法是,显式使用特定的函数打开文件、关闭文件、读取文件、写入文件,诸如此类。我们在第13章中再详细介绍这种方法。
- 第2种方法是,设计能与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和从文件输出。换言之,把stdin流重新赋给文件。继续使用getchar()函数从输入流中获取数据,但它并不关心从流的什么位置获取数据。 虽然这种重定向的方法在某些方面有些限制,但是用起来比较简单,而且能让读者熟悉普通的文件处理技术。
UNIX、Linux和DOS重定向
-
UNIX(运行命令行模式时)、Linux(ditto)和Window命令行提示(模仿旧式DOS命令行环境)都能重定向输入、输出。重定向输入让程序使用文件而不是键盘来输入,重定向输出让程序输出至文件而不是屏幕。
文本文件(text file)是内含文本的文件,其中存储的数据是我们可识别的字符。文件的内容可以是一篇散文或者C程序。内含机器语言指令的文件(如存储可执行程序的文件)不是文本文件。
-
由于该程序的操作对象是字符,所以要使用文本文件。只需用下面的命令代替上面的命令即可:
./echo_eof < words
-
<符号是UNIX和DOS/Windows的重定向运算符。该运算符使words文件与stdin流相关联,把文件中的内容导入echo_eof程序。echo_eof程序本身并不知道(或不关心)输入的内容是来自文件还是键盘,它只知道这是需要导入的字符流,所以它读取这些内容并把字符逐个打印在屏幕上,直至读到文件结尾。因为C把文件和I/O设备放在一个层面,所以文件就是现在的I/O设备。
-
现在假设要用echo_eof把键盘输入的内容发送到名为mywords的文件中。然后,输入以下命令并开始输入:
./echo_eof>mywords
-
>符号是第2个重定向运算符。它创建了一个名为mywords的新文件,然后把echo_eof的输出(即,你输入字符的副本)重定向至该文件中。重定向把stdout从显示设备(即,显示器)赋给mywords文件。如果已经有一个名为mywords的文件,通常会擦除该文件的内容,然后替换新的内容(但是,许多操作系统有保护现有文件的选项,使其成为只读文件)。所有出现在屏幕的字母都是你刚才输入的,其副本存储在文件中。在下一行的开始处按下Ctrl+D(UNIX)或Ctrl+Z(DOS)即可结束该程序。
-
现在,假设你希望制作一份mywords文件的副本,并命名为savewords。只需输入以下命令即可:
./echo_eof < mywords > savewords
-
下面的命令也起作用,因为命令与重定向运算符的顺序无关:
./echo_eof > savewords < mywords
注意:
- ·重定向运算符连接一个可执行程序(包括标准操作系统命令)和一个数据文件,不能用于连接一个数据文件和另一个数据文件,也不能用于连接一个程序和另一个程序。
- ·使用重定向运算符不能读取多个文件的输入,也不能把输出定向至多个文件。
- ·通常,文件名和运算符之间的空格不是必须的,除非是偶尔在UNIX shell、Linux shell或Windows命令行提示模式中使用的有特殊含义的字符。例如,我们用过的./echo_eof<words。
- UNIX、Linux或Windows/DOS还有>>运算符,该运算符可以把数据添加到现有文件的末尾,而|运算符能把一个文件的输出连接到另一个文件的输入。
创建更友好的用户界面
/* guess.c -- 一个拖沓且错误的猜数字程序 */
#include <stdio.h>
int main(void)
{
int guess = 1;
printf("Pick an integer from 1 to 100. I will try 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') /* 获取响应,与 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 try 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!
- 撇开这个程序糟糕的算法不谈,我们先选择一个数字。注意,每次输入n时,程序打印了两条消息。这是由于程序读取n作为用户否定了数字1,然后还读取了一个换行符作为用户否定了数字2。
- 一种解决方案是,使用while循环丢弃输入行最后剩余的内容,包括换行符。这种方法的优点是,能把no和no way这样的响应视为简单的n。
while (getchar() != 'y') /* 获取响应,与 y 做对比*/
{
printf("Well, then, is it %d?\n", ++guess);
while (getchar() != '\n')
continue; /* 跳过剩余的输入行 */
}
Pick an integer from 1 to 100. I will try 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?no
Well, then, is it 3?no sir
Well, then, is it 4?forget it
Well, then, is it 5?y
I knew I could do it!
-
这的确是解决了换行符的问题。但是,该程序还是会把f视为n。
-
我们用if语句筛选其他响应。首先,添加一个char类型的变量存储响应:
char response;
-
修改后的循环如下:
while ((response = getchar()) != 'y') /* 获取响应 */
{
if (response == 'n')
printf("Well, then, is it %d?\n", ++guess);
else
printf("Sorry, I understand only y or n.\n");
while (getchar() != '\n')
continue; /* 跳过剩余的输入行 */
}
混合数值和字符输入
- 该程序读入一个字符和两个数字,然后根据输入的两个数字指定的行数和列数打印该字符。
/* showchar1.c -- 有较大 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'); /* 结束一行并开始新的一行 */
}
}
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是换行符正式终止循环的条件。
/* showchar2.c -- 按指定的行列打印字符 */
#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'); /* 结束一行并开始新的一行 */
}
}
-
while循环实现了丢弃scanf()输入后面所有字符(包括换行符)的功能,为循环的下一轮读取做好了准备。
-
在if语句中使用一个break语句,可以在scanf()的返回值不等于2时终止程序,即如果一个或两个输入值不是整数或者遇到文件结尾就终止程序。
输入验证
-
例如,假设你编写了一个处理非负数整数的循环,但是用户很可能输入一个负数。你可以使用关系表达式来排除这种情况:
long n;
scanf("%ld", &n); // 获取第1个值
while (n >= 0) // 检测不在范围内的值
{
// 处理n
scanf("%ld", &n); // 获取下一个值
}
-
另一类潜在的陷阱是,用户可能输入错误类型的值,如字符q。排除这种情况的一种方法是,检查scanf()的返回值。回忆一下,scanf()返回成功读取项的个数。因此,下面的表达式当且仅当用户输入一个整数时才为真:
scanf("%ld", &n) == 1
-
结合上面的while循环,可改进为:
long n;
while (scanf("%ld", &n) == 1 && n >= 0)
{
// 处理n
}
long get_long(void)
{
long input;
char ch;
while (scanf("%ld", &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;
}
- 该函数要把一个long类型的值读入变量input中。如果读取失败,函数则进入外层while循环体。然后内层循环逐字符地读取错误的输入。注意,该函数丢弃该输入行的所有剩余内容。还有一个方法是,只丢弃下一个字符或单词,然后该函数提示用户再次输入。外层循环重复运行,直到用户成功输入整数,此时scanf()的返回值为1。
// checking.c -- 输入验证
#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.\nEnter the "
"limits (enter 0 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("from %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(" is not an integer.\nPlease enter an ");
printf("integer value, such as 25, -178, or 3: ");
}
return input;
}
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;
}
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;
}
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.
Enter the limits (enter 0 for both limits to quit):
lower limit: low
low is not an integer.
Please enter an integer value, such as 25, -178, or 3: 3
upper limit: a big number
a big number is not an integer.
Please enter an integer value, such as 25, -178, or 3: 12
The sum of the squares of the integers from 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.
-
程序遵循模块化的编程思想,使用独立函数(模块)来验证输入和管理显示。程序越大,使用模块化编程就越重要。
-
main()函数管理程序流,为其他函数委派任务。它使用get_long()获取值、while循环处理值、bad_limits()函数检查值是否有效、sum_squres()函数处理实际的计算:
菜单浏览
-
许多计算机程序都把菜单作为用户界面的一部分。菜单给用户提供方便的同时,却给程序员带来了一些麻烦。我们看看其中涉及了哪些问题。
-
菜单给用户提供了一份响应程序的选项。假设有下面一个例子:
Enter the letter of your choice:
a. advice b. bell
c. count q. quit
-
第1个目标是:当用户遵循指令时程序顺利运行;
-
第2个目标是:当用户没有遵循指令时,程序也能顺利运行。显而易见,要实现第2个目标难度较大,因为很难预料用户在使用程序时的所有错误情况。
-
现在的应用程序通常使用图形界面,可以点击按钮、查看对话框、触摸图标,而不是我们示例中的命令行模式。但是,两者的处理过程大致相同:给用户提供选项、检查并执行用户的响应、保护程序不受误操作的影响。除了界面不同,它们底层的程序结构也几乎相同。但是,使用图形界面更容易通过限制选项控制输入。
-
我们来更具体地分析一个菜单程序需要执行哪些任务。它要获取用户的响应,根据响应选择要执行的动作。另外,程序应该提供返回菜单的选项。C的switch语句是根据选项决定行为的好工具,用户的每个选择都可以对应一个特定的case标签。使用while语句可以实现重复访问菜单的功能。因此,我们写出以下伪代码:
获取选项
当选项不是’q’时
转至相应的选项并执行
获取下一个选项
-
当你决定实现这个程序时,就要开始考虑如何让程序顺利运行(顺利运行指的是,处理正确输入和错误输入时都能顺利运行)。例如,你能做的是让“获取选项”部分的代码筛选掉不合适的响应,只把正确的响应传入switch。这表明需要为输入过程提供一个只返回正确响应的函数。
-
定义get_choice()函数只能返回’a’、‘b’、‘c’和’q’。
-
下面的伪代码是设计这个函数的一种方案:
显示选项
获取用户的响应
当响应不合适时
提示用户再次输入
获取用户的响应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 = getchar(); while ((ch < 'a' || ch > 'c') && ch != 'q') { printf("Please respond with a, b, c, or q.\n"); ch = getchar(); } return ch; }
缓冲输入依旧带来些麻烦,程序把用户每次按下Return键产生的换行符视为错误响应。为了让程序的界面更流畅,该函数应该跳过这些换行符。
-
这类问题有多种解决方案。一种是用名为get_first()的新函数替换getchar()函数,读取一行的第1个字符并丢弃剩余的字符。这种方法的优点是,把类似act这样的输入视为简单的a,而不是继续把act中的c作为选项c的一个有效的响应。我们重写输入函数如下:
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; }
void count(void) { int n, i; printf("Count how far? Enter an integer:\n"); scanf("%d", &n); for (i = 1; i <= n; i++) printf("%d\n", i); }
-
如果输入3作为响应,scanf()会读取3并把换行符留在输入队列中。下次调用get_choice()将导致get_first()返回这个换行符,从而导致我们不希望出现的行为。
-
重写get_first(),使其返回下一个非空白字符而不仅仅是下一个字符,即可修复这个问题。
-
另一种方法是,在count()函数中清理换行符,如下所示:
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; }
/* menuette.c -- 菜单程序 */ #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'); /* ANSI */ 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; }
复习题
- putchar(getchar())是一个有效表达式,它实现什么功能?getchar(putchar())是否也是有效表达式?
- 下面的语句分别完成什么任务?
a.putchar(‘H’);
b.putchar(’\007’);
c.putchar(’\n’);
d.putchar(’\b’); - 假设有一个名为count的可执行程序,用于统计输入的字符数。设计一个使用count程序统计essay文件中字符数的命令行,并把统计结果保存在essayct文件中。
- 给定复习题3中的程序和文件,下面哪一条是有效的命令?
a.essayct <essay
b.count essay
c.essay >count - EOF是什么?
- 对于给定的输入(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);
} - C如何处理不同计算机系统中的不同文件和换行约定?
- 在使用缓冲输入的系统中,把数值和字符混合输入会遇到什么潜在的问题?
- 表达式putchar(getchar())使程序读取下一个输入字符并打印出来。getchar()的返回值是putchar()的参数。但getchar(putchar())是无效的表达式,因为getchar()不需要参数,而putchar()需要一个参数。
- a.显示字符H。
b.如果系统使用ASCII,则发出一声警报。
c.把光标移至下一行的开始。
d.退后一格。 - count <essay >essayct或者count >essayct <essay
- 都不是有效的命令。
- EOF是由getchar()和scanf()返回的信号(一个特殊值),表明函数检测到文件结尾。
- a.输出是:If you qu
注意,字符I与字符i不同。还要注意,没有打印i,因为循环在检测到i之后就退出了。
b.如果系统使用ASCII,输出是:HJacrthjacrt
while的第1轮迭代中,为ch读取的值是H。第1个putchar()语句使用的ch的值是H,打印完毕后,ch的值加1(现在是ch的值是I)。然后到第2个putchar()语句,因为是++ch,所以先递增ch(现在ch的值是J)再打印它的值。然后进入下一轮迭代,读取输入序列中的下一个字符(a),重复以上步骤。需要注意的是,两个递增运算符只在ch被赋值后影响它的值,不会让程序在输入序列中移动。 - C的标准I/O库把不同的文件映射为统一的流来统一处理。
- 数值输入会跳过空格和换行符,但是字符输入不会。假设有下面的代码:
int score;
char grade;
printf(“Enter the score.\n”);
scanf("%s", &score);
printf(“Enter the letter grade.\n”);
grade = getchar();
如果输入分数98,然后按下Enter键把分数发送给程序,其实还发送了一个换行符。这个换行符会留在输入序列中,成为下一个读取的值(grade)。如果在字符输入之前输入了数字,就应该在处理字符输入之前添加删除换行符的代码。
编程练习
-
设计一个程序,统计在读到文件结尾之前读取的字符数。
-
编写一个程序,在遇到EOF之前,把输入作为字符流读取。程序要打印每个输入的字符及其相应的ASCII十进制值。注意,在ASCII序列中,空格字符前面的字符都是非打印字符,要特殊处理这些字符。如果非打印字符是换行符或制表符,则分别打印\n或\t。否则,使用控制字符表示法。例如,ASCII的1是Ctrl+A,可显示为^A。注意,A的ASCII值是Ctrl+A的值加上64。其他非打印字符也有类似的关系。除每次遇到换行符打印新的一行之外,每行打印10对值。(注意:不同的操作系统其控制字符可能不同。)
-
编写一个程序,在遇到EOF之前,把输入作为字符流读取。该程序要报告输入中的大写字母和小写字母的个数。假设大小写字母数值是连续的。或者使用ctype.h库中合适的分类函数更方便。
-
编写一个程序,在遇到EOF之前,把输入作为字符流读取。该程序要报告平均每个单词的字母数。不要把空白统计为单词的字母。实际上,标点符号也不应该统计,但是现在暂时不同考虑这么多(如果你比较在意这点,考虑使用ctype.h系列中的ispunct()函数)。
-
修改程序清单8.4的猜数字程序,使用更智能的猜测策略。例如,程序最初猜50,询问用户是猜大了、猜小了还是猜对了。如果猜小了,那么下一次猜测的值应是50和100中值,也就是75。如果这次猜大了,那么下一次猜测的值应是50和75的中值,等等。使用二分查找(binary search)策略,如果用户没有欺骗程序,那么程序很快就会猜到正确的答案。
-
修改程序清单8.8中的get_first()函数,让该函数返回读取的第1个非空白字符,并在一个简单的程序中测试。
-
修改第7章的编程练习8,用字符代替数字标记菜单的选项。用q代替5作为结束输入的标记。
-
编写一个程序,显示一个提供加法、减法、乘法、除法的菜单。获得用户选择的选项后,程序提示用户输入两个数字,然后执行用户刚才选择的操作。该程序只接受菜单提供的选项。程序使用float类型的变量存储用户输入的数字,如果用户输入失败,则允许再次输入。进行除法运算时,如果用户输入0作为第2个数(除数),程序应提示用户重新输入一个新值。该程序的一个运行示例如下:
Enter the operation of your choice:
a. add s. subtract
m. multiply d. divide
q. quita
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. subtract
m. multiply d. divide
q. quitd
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. subtract
m. multiply d. divide
q. quitq
Bye.
#include <stdio.h>
int main(void)
{
unsigned count = 0u;
printf("Enter characters.The program is to count the numeber of characters!\n");
while (getchar() != EOF)
{
count++;
}
printf("%u character(s).\n",count);
return 0;
}
-
#include <iostream> #include <string> using namespace std; int main(void) { string str = ""; char ch; while ((ch = getchar()) != EOF) { /* code */ str.push_back(ch); } unsigned int count = 0u; for (int i = 0; i < str.length(); i++) { cout << str[i] << " " << (int)str[i] << "\t"; count++; if (str[i] == '\n') { count = 0u; } if (count == 10u) { cout << endl; count = 0u; } } return 0; }
-
#include <stdio.h> #include <ctype.h> int main(void) { unsigned upper_num = 0u, lower_num = 0u; char ch; while ((ch = getchar()) != EOF) { if (islower(ch)) { lower_num++; } else if (isupper(ch)) { upper_num++; } } printf("upper character: %u lower characters: %u\n", upper_num, lower_num); return 0; }
-
#include <stdio.h> #include <ctype.h> int main(void) { char ch; unsigned int char_num = 0u, word_num = 0u; while ((ch = getchar()) != EOF) { if (ch != ' ' && !ispunct(ch) && isprint(ch)) { char_num++; } if (ch == ' ' || ispunct(ch)) { word_num++; } } printf("%u %u %g\n", char_num, word_num, (float)char_num / word_num); return 0; }
-
#include <stdio.h> #include <ctype.h> int main(void) { int guess = (0 + 100) / 2; char ch; int up = 100, down = 0; printf("Pick an integer from 1 to 100. I will try to guess "); printf("it.\nRespond with a d if my guess is big and with"); printf("\na x if it is small.\n"); printf("and with a y if my guess is right.\n"); printf("Uh...is your number %d?\n", guess); while ((ch = getchar()) != 'y') /* 获取响应,与 y 做对比 */ { if (isalpha(ch)) { ch = tolower(ch); } switch (ch) { case 'd': up = guess; guess = (guess + down) / 2; printf("Well, then, is it %d?\n", guess); break; case 'x': down = guess; guess = (guess + up) / 2; printf("Well, then, is it %d?\n", guess); break; default: printf("Error!\n"); break; } while (getchar() != '\n') { continue; } } printf("I knew I could do it!\n"); return 0; }
-
#include <stdio.h> #include <ctype.h> char get_first(void) { int ch; // ch = getchar(); /* 读取下一个字符 */ while (ch = getchar()) { if (!isblank(ch)) break; } while (getchar() != '\n') continue; /* 跳过该行剩下的内容 */ return ch; } int main(void) { char ch = get_first(); printf("%c\n",ch); return 0; }
-
#include <stdio.h> #include <stdbool.h> #include<ctype.h> #define BASETIME 40 #define BI 1.5 #define SHUIBASE 300 #define SHUIBASE1 450 #define SHUIRATE 0.15 #define SHUIRATE2 0.2 #define SHUIRATE3 0.25 char get_first(void); int main(void) { float base; while (true) { printf("*****************************************************************\n"); printf("Enter the number corresponding to the desired pay rate or action:\n"); printf("a) $8.75/hr\n"); printf("b) $9.33/hr\n"); printf("c) $10.00/hr\n"); printf("d) $11.20/hr\n"); printf("q) quit\n"); printf("*****************************************************************\n"); printf("please enter option number: "); char option = get_first(); switch (option) { case 'a': base = 8.75; break; case 'b': base = 9.33; break; case 'c': base = 10.00; break; case 'd': base = 11.20; case 'q': printf("Exit!\n"); return 0; break; default: printf("Error!Repeat!\n"); continue; break; } unsigned time; printf("enter the number of work time: "); scanf("%u", &time); float money; money = time >= BASETIME ? (BASETIME * base + (time - BASETIME) * BI * base) : (base * time); float rate; if (money <= SHUIBASE) { rate = money * SHUIRATE; } else if (money <= SHUIBASE1) { rate = SHUIBASE * SHUIRATE + (money - SHUIBASE) * SHUIRATE2; } else { rate = SHUIBASE * SHUIRATE + (SHUIBASE1 - SHUIBASE) * SHUIRATE2 + (money - SHUIBASE1) * SHUIRATE3; } printf("shuiqian: %g,shui: %g,shuihou: %g\n", money, rate, money - rate); } } char get_first(void) { int ch; // ch = getchar(); /* 读取下一个字符 */ while (ch = getchar()) { if (!isblank(ch)) break; } while (getchar() != '\n') continue; /* 跳过该行剩下的内容 */ return ch; }
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
char get_first(void);
char get_choice(void);
float add(float num1, float num2);
float sub(float num1, float num2);
float mul(float num1, float num2);
float div(float num1, float num2);
float get_float(void);
int main(void)
{
char option;
float num1, num2;
while (true)
{
option = get_choice();
if (option == '0')
{
printf("Error!Repeat!\n");
continue;
}
if (option != 'q')
{
printf("Enter first number: \n");
num1 = get_float();
printf("Enter second number: \n");
num2 = get_float();
}
switch (option)
{
case 'a':
printf("%g + %g = %g\n", num1, num2, add(num1, num2));
break;
case 's':
printf("%g - %g = %g\n", num1, num2, sub(num1, num2));
break;
case 'm':
printf("%g * %g = %g\n", num1, num2, mul(num1, num2));
break;
case 'd':
while (num2 == 0)
{
printf("input error! Repeat!\n");
printf("Enter second number: \n");
num2 = get_float();
}
printf("%g / %g = %g\n", num1, num2, div(num1, num2));
break;
case 'q':
printf("Bye!\n");
return 0;
default:
printf("Program error!\n");
return 0;
break;
}
}
return 0;
}
char get_first(void)
{
int ch;
// ch = getchar(); /* 读取下一个字符 */
while (ch = getchar())
{
if (!isblank(ch))
break;
}
while (getchar() != '\n')
continue; /* 跳过该行剩下的内容 */
return ch;
}
char get_choice(void)
{
printf("=====================================\n");
printf("a. add s. subtract\n");
printf("m. multiply d. divide\n");
printf("q. quit\n");
printf("=====================================\n");
printf("Enter the operation of your choice:\n");
char ch = get_first();
if (ch != 'a' && ch != 's' && ch != 'm' && ch != 'd' && ch != 'q')
{
return '0';
}
return ch;
}
float add(float num1, float num2)
{
return num1 + num2;
}
float sub(float num1, float num2)
{
return num1 - num2;
}
float mul(float num1, float num2)
{
return num1 * num2;
}
float div(float num1, float num2)
{
return num1 / num2;
}
float get_float(void)
{
float num;
while (scanf("%f", &num) != 1)
{
printf("input error! Repeat!\n");
while (getchar() != '\n')
{
continue;
}
}
while (getchar() != '\n')
{
continue;
}
return num;
}