C_Primer_Plus08.io

字符输入/输出和输入验证

  • 要点

更详细地介绍输入、输出以及缓冲输入和无缓冲输入的区别
通过键盘模拟文件结尾条件
使用重定向把程序和文件相连接
创建更友好的用户界面

最初,输入/输出不是C定义的一部分,C 把开发这些函数的任务留给了编译器的实现者来完成。UNIX 系统中的 C 实现为这些函数提供了一个模型。ANSI C 库吸取成功的经验,把大量的 UNIX I/O 函数囊括其中。由于必须保证这些标准函数在不同计算机环境中能正常工作,所以它们很少使用某些特殊系统才有的特性。所以,许多 C 供应商会利用硬件或操作系统的特性,额外提供一些 I/O 函数。

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

getchar() 和 putchar() 方法每次只处理一个字符,对人来说显得太笨拙,然而确很适合计算机,而且这是大多数文本处理程序使用的核心方法。

getchar() 和 putchar() 都不是真正的函数,他们被定义为供预处理器使用的宏。

程序例:

#include <stdio.h>

int main(void){
  char ch;

  while ('#' != (ch = getchar())){
    putchar(ch);
  }

  return 0;
}

output:

Hello, there. I would [enter]
Hello, there. I would
like a #2 bag of potatoes.[enter]
like a

缓冲区

上述程序在运行后输入一串字符,遇到回车才执行循环,然后把一行内容打印到屏幕上,是因为有缓冲机制。用户输入的字符被收集并存储在一个被称为缓冲区(buffer)的临时存储区,按回车后,程序才可使用用户输入的字符。
对于老式系统,则没有缓冲机制,称为无缓冲输入。

为什么要有缓冲区?

  • 把若干字符作为一个块进行传输比逐个传输节约时间
  • 如有输入错误,可以修正

使用无缓冲输入的情况:比如在游戏中,按一个键就执行相应的指令体验更好。所以有缓冲输入和无缓冲输入都有用武之地。

缓冲分两类:完全缓冲 I/O 和 行缓冲 I/O。完全缓冲是指当缓冲区被完全填满时才刷新缓冲区(内容发送到目的地),通常出现在文件传输中。缓冲区的大小取决于系统,常见的是 512B 和 4096B。行缓冲是指出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入。

结束键盘输入

文件、流和键盘输入

C 在读写文件时,有两种可行的方法。从较底层面上,C 可以使用主机操作系统的基本文件工具直接处理文件,这些直接调用系统的函数被称为底层 I/O (low-level I/O) 。由于计算机系统各不相同,不可能为普通的底层 I/O 函数创建统一的标准库。从较高层面上,C 还可以通过标准 I/O 包(standard I/O package) 来处理文件,这涉及创建用于处理文件的标准模型和一套标准 I/O 函数。

不同的操作系统对文件的处理不同。首先,存储方式的不同,有些系统把文件内容存储在一处,而文件的相关信息存储在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。有些系统用最小字节来衡量文件的大小,有些系统用字节块大小来衡量。
如果使用标准 I/O 包,就不用考虑这些差异。因此可以用 if ('\n' == ch) 来检查换行符。即使系统实际使用回车符和换行符的组合来标记行末尾,I/O 函数会在两种表示方法之间相互转换。

从概念上看,C 程序处理的是流而不是直接处理文件。流(stream)是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。所以打开文件的过程就是把流和文件相关联的过程,而且读写都由流来完成。
本章中,可以把键盘和显示设备视为 C 程序自动打开的文件。stdin 流表示键盘输入,stdout 表示屏幕输出流。getchar(), putchar(), printf() 和 scanf() 函数都是标准 I/O 包的成员,用于处理这两个流。

文件结尾

检测文件结尾的一种方法是,在文件末尾放一个特殊字符标记文件结尾。CP/M, IBM-DOS 和 MS-DOS 的文本文件曾使用内嵌的 Ctrl+Z 字符标记文件结尾,这种方法至今仍起作用,如果文件中有 Ctrl+Z,系统认为文件结束。但现在有了一些其他的方法,比如记录文件大小,所以现在的文本文件中不一定有嵌入的 Ctrl+Z. UNIX 系统则使用记录文件大小的方法处理所有的文件。

不论操作系统使用哪种方法检测文件结尾,C 语言中,用 getchar() 读取文件检测到文件结尾时将返回一个特殊的值 EOF (end of file)。EOF 定义在 stdio.h 文件中:

#define EOF (-1)

为什么是 -1,因为 getchar() 函数的返回值通常都是 0-127,对应标准字符集,但是如果系统能识别扩展字符集,该函数的返回值可能在 0-255之间,不论哪种情况,-1 都不对应任何字符。
某些系统可能把 EOF 定义为 -1 以外的值,但无论如何都不同文件中的字符。所以,如果包含 stdio.h 文件,并使用 EOF 符号,就不用担心 EOF 值不同的问题。

程序例:

#include <stdio.h>

int main(void){
    int ch;

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

    return 0;
}

注意,上面程序中的 ch 定义为 int 类型,若仍然使用 char 类型,则它会把负数转成正数,与 EOF 判断时会不准确。getchar() 函数的返回值类型是 int,如果把它的返回值赋给 char 类型变量,一些编译器会警告可能丢失数据。
另外,运行该程序试图输入 EOF 字符时,不能只输入字符 'EOF' 或 '-1',正确的做法是,必须找出当前系统的要求,比如,大多数 Unix 和 Linux 系统中,在一行开始处按下 Ctrl+D 会传输文件结尾信号,而许多微型计算机系统把一行开始处的 Ctrl+Z 识别为文件结尾信号,有些系统把任意位置的 Ctrl+Z 解释成文件结尾信号。
改变这个程序的输入或输出的流,可以实现读写文件、复制文件的功能。

重定向和文件

默认情况下,使用 getchar() 函数时,C 程序使用标准 I/O 包查找标准输入作为输入源,即 stdin流。它可以使磁带、穿孔卡或电传打印机,或者是键盘、文件,甚至是语音输入。

程序通过两种方式使用文件。显式地使用特定的函数打开文件、关闭文件、读文件、写文件(这种方法以后再介绍)。另一种是设计能与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和从文件输出。即把 stdin 流重新赋给文件。这样在使用 getchar() 函数时,它并不关心从流的什么位置获取数据,虽然这种重定向的方法在某些方面有些限制,但用起来比较简单。

重定向的一个主要问题与操作系统有关,与 C 无关。尽管如此,许多 C 的环境中(Unix, linux, windows 命令模式)都有重定向特性,甚至有些 C 实现还在缺少重定向特性的系统中模拟它。

Unix,Linux 和 DOS 重定向

假设程序编译后的可执行文件为 echo_eof (windows为 echo_eof.exe)

将文本文件 words 内容输出到屏幕

echo_eof < words

echo_eof 并不知道输入的内容来自文件还是键盘

将文本文件 words 复制到 savewords

echo_eof < words > savewords
# 或:(命令与重定向运算符的顺序无关)
echo_eof > savewords < words
  • 输入、输出文件名不能相同
  • 重定向运算符连接一个可执行程序和一个文件,不能用于文件之间或程序之间
  • 重定向运算符不能读取多个文件的输入,也不能输出到过个文件
  • 通常,文件名和运算符之间的空格不是必须的

不使用重定向

打开文件:

/* eof of file */
#include <stdio.h>
#include <stdlib.h>

int main(void){
    char ch;
    char filename[50];
    FILE * fp;

    printf("Enter a file name:\n");
    scanf("%s", filename);
    fp = fopen(filename, "r");
    if (NULL == fp){
        printf("Failed to open file %s. Bye\n", filename);
        exit(1);
    }

    while (EOF != (ch = getc(fp))){
        putchar(ch);
    }
    fclose(fp);

    return 0;
}

绝大部分 C 系统都可以使用重定向,可以通过操作系统重定向所有程序,或只在 C 编译器允许的情况下重定向 C 程序。
有些系统要求重定向运算符左侧有一个空格,右侧没有空格,而其他系统允许在重定向运算符两侧有空格或没有空格。

创建更友好的用户界面

处理缓冲输入中的换行符

使用缓冲输入时,用户需要按下回车发送输入,但这样做换行符作为一个字符发送给了程序,所以程序应当能够妥善处理这个麻烦的换行符。以猜字谜程序为例:

#include <stdio.h>

int main(void){
    int guess = 1;

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

    return 0;
}

output:

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.
Un...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!

由于程序没有处理换行符,while循环中每次循环了两次,其中一次是 回车符。对程序进行修改:

#include <stdio.h>

int main(void){
    int guess = 1;
    char ch;

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

    return 0;
}

output:

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.
Un...is your number 1?
n
Well, then, is it 2?
n
Well, then, is it 3?
y
I knew I could do it!

上面的修改修正了回车符的问题,但如果一行中输入多个字符,则会打印多行,应把一行中第一个字符之后的所有字符跳过。而且,如果输入除了 n 和 y 以外的字符,会被当做n处理。修改后的程序:

#include <stdio.h>

int main(void){
    int guess = 1;
    char ch;

    printf("Pick an integer from 1 to 100. I will try to guess it.\n"
            "Respond with a y if my guess is right and with an n\n"
            "if it is wrong.\n");
    printf("Un...is your number %d?\n", guess);
    while ('y' != (ch = getchar())){
        if ('n' == ch){
            printf("Well, then, is it %d?\n", ++guess);
        }else{
            printf("Sorry, I understand only y or n.\n");
        }
        while ('\n' != getchar()){
            continue;
        }
    }
    printf("I knew I could do it!\n");

    return 0;
}

output:

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.
Un...is your number 1?
n
Well, then, is it 2?
no sir
Well, then, is it 3?
ny
Well, then, is it 4?
holly
Sorry, I understand only y or n.
y
I knew I could do it!

混合数值和字符输入

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

程序例:

#include <stdio.h>

void display(char ch, int row, int col);

int main(void){
    char ch;
    int row, col;

    printf("Enter a character and two integers:\n");
    while ((ch = getchar()) != '\n'){
        if (scanf("%d %d", &row, &col) != 2){
            printf("Invalid input. ");
            break;
        }
        display(ch, row, col);
        while (getchar() != '\n'){
            continue;
        }
        printf("Enter another character and another two integers:\n");
        printf("Enter a newline to quit.\n");
    }
    printf("Bye.\n");

    return 0;
}

void display(char ch, int row, int col){
    for (int i = 0, j = 0; i < row; ++i){
        for (j = 0; j < col; ++j){
            printf("%c", ch);
        }
        printf("\n");
    }
    printf("\n");
}

output:

Enter a character and two integers:
a 2 3
aaa
aaa

Enter another character and another two integers:
Enter a newline to quit.
b 3 4
bbbb
bbbb
bbbb

Enter another character and another two integers:
Enter a newline to quit.

Bye.

输入验证

用户并不一定按照程序设定的输入合法字符。比如需要输入正数的时候用户输入了负数,甚至输入了非法字符,程序仍然需要处理这些意外情况,可以用这种写法:

long n;
while (scanf("%ld", &a) == 1 && n >= 0){
    // process
}

这样当用户输入非正数的字符时,会跳过处理。然而也可以让程序友好一些,提示用户再次输入正确类型的值。这样就需要处理有问题的输入。此时,scanf() 没有成功读取的字符会留在输入队列中,可以利用 getchar() 函数逐字读取输入字符,从而跳过错误字符:

long n;
char ch;
while (scanf("%ld", &a) != 1 || n < 0){
    while ((ch = getchar()) != '\n'){
        putchar(ch);  // 处理错误输入
    }
    printf(" is not an integer.\n"
          "Please enter an integer: ");
}
// continue process with n

程序例:
依次输入两个数字,计算介于两个数之间的所有数的平方和。要求先输入较小数,再输入较大数,且两个数不能超过上下界限。当两个输入的数都是0时退出程序。
该程序展示了模块化编程,不同模块之间功能分离。

/* check input & calc sum squares between two numbers */
#include <stdio.h>
#include <stdbool.h>

long get_long(void);
bool checkLimits(long low, long high, long min, long max);
long sum_squares(long low, long high);

int main(void){
    char ch;
    const long MIN = -10000000L;
    const long MAX = +10000000L;
    long low;
    long high;
    long result;

    printf("This program calculates the sum of the squares of "
            "integers in a range.\nThe lower bound should not be "
            "less than %ld and\nthe upper bound should not be "
            "more than %ld.\nEnter the limits (enter 0 for both "
            "to quit):\n"
            "lower limit: ", MIN, MAX);
    low = get_long();
    printf("upper limit: ");
    high = get_long();

    while (low != 0 && high != 0){
        if (!checkLimits(low, high, MIN, MAX)){
            printf("Retry.\n");
        }else{
            result = sum_squares(low, high);
            printf("sum sqaures between %ld and %ld is %ld.\n", low, high, result);
        }
        printf("Enter the limits (enter 0 for both limits to quit):\n");
        printf("lower limit: ");
        low = get_long();
        printf("upper limit: ");
        high = get_long();
    }
    printf("Bye.\n");

    return 0;
}

long get_long(void){
    long input;
    while (scanf("%ld", &input) == 0){
        printf("Invalid input, retry:\n");
        while (getchar() != '\n'){
            continue;
        }
    }
    return input;
}

bool checkLimits(long low, long high, long min, long max){
    if (low < min || high < min){
        printf("low and high must no smaller than %ld.\n", min);
        return false;
    }
    if (low > max || high > max){
        printf("low and high must no larger than %ld.\n", max);
        return false;
    }
    if (low > high){
        printf("%ld isn't smaller than %ld.\n", low, high);
        return false;
    }
    return true;
}

long sum_squares(long low, long high){
    long result;
    for (; low <= high; ++low){
        result += low^2;
    }
    return result;
}

output:

This program calculates 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 to quit):
lower limit: 10
upper limit: 20
sum sqaures between 10 and 20 is 173.
Enter the limits (enter 0 for both limits to quit):
lower limit: 100
upper limit: 2000000
sum sqaures between 100 and 2000000 is 2000000995152.
Enter the limits (enter 0 for both limits to quit):
lower limit: -1000000000000
upper limit: 2
low and high must no smaller than -10000000.
Retry.
Enter the limits (enter 0 for both limits to quit):
lower limit: 0
upper limit: 0
Bye.

菜单浏览 - 模块化

将某个特定功能委托给另一个函数。

#include <stdio.h>

char get_choice(void);
void count(void);

int main(void){
    char choice;

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

    return 0;
}

char get_choice(void){
    char ch;

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

void count(void){
    int n;
    char ch;

    printf("Count how far? Enter an integer:\n");
    while (scanf("%d", &n) != 1){
        while ((ch = getchar()) != '\n'){
            putchar(ch);
        }
        printf(" is not an integer.\nPlease enter an integer "
                "value, such as 25, -126, or 9: ");
    }
    while (getchar() != '\n'){
        continue;
    }

    for (int i = 1; i < n; ++i){
        printf("%d\n", i);
    }
}

output:

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

ex08:
编写一个程序,显示一个提供加法、减法、乘法、除法的菜单。获得用户选择的选项后,程序提示用户输入两个数字,然后执行用户刚才选择的操作。该程序只接受菜单提供的选项。程序使用float类型的变量储存用户输入的数字,如果用户输入失败,则允许再次输入。进行除法运算时,如果用户输入0作为第2个数(除数),程序应提示用户重新输入一个新值。

#include <stdio.h>

int get_choice(void);
float get_float(void);
void calc(int choice);


int main(void){
    int choice;

    while ((choice = get_choice()) != 'q'){
        switch (choice){
            case 'a':
            case 'b':
            case 'm':
            case 'd':
                calc(choice);
                break;
            default:
                printf("Program error.\n");
                break;
        }
    }
    printf("Bye.\n");

    return 0;
}

int get_choice(void){
    int choice;

    printf("Enter your choice:\n");
    printf("a. add      b. subtract\n"
            "m. multiply d. divide\n"
            "q. quit\n");
    choice = getchar();
    while (choice != 'a' && choice != 'b'
            && choice != 'm' && choice != 'd'
            && choice != 'q'){
        printf("Please respond with a, b, m, d, or q.\n");
        while (getchar() != '\n'){
            continue;
        }
        choice = getchar();
    }
    while (getchar() != '\n'){
        continue;
    }
    return choice;
}

float get_float(void){
    float input;
    char ch;

    while (scanf("%f", &input) != 1){
        while ((ch = getchar()) != '\n'){
            putchar(ch);
        }
        printf(" is not a float.\n"
                "Please input a float, such as 1.23, -0.56 or 3: ");
    }
    while (getchar() != '\n'){
        continue;
    }

    return input;
}

void calc(int calc){
    float first;
    float second;
    char sign;
    float result;


    printf("Enter first number: ");
    first = get_float();
    printf("Enter Second number: ");
    second = get_float();
    if ('d' == calc){
        while (second == 0.){
            printf("Enter a number other than 0: ");
            second = get_float();
        }
    }

    switch (calc){
        case 'a':
            sign = '+';
            result = first + second;
            break;
        case 'b':
            sign = '-';
            result = first - second;
            break;
        case 'm':
            sign = '*';
            result = first * second;
            break;
        case 'd':
            sign = '.';
            result = first / second;
            break;
        default:
            printf("Program error.\n");
            break;
    }

    printf("%.2f %c %.2f = %.2f\n", first, sign, second, result);
}

output:

Enter your choice:
a. add      b. subtract
m. multiply d. divide
q. quit
a
Enter first number: 10
Enter Second number: 3
10.00 + 3.00 = 13.00
Enter your choice:
a. add      b. subtract
m. multiply d. divide
q. quit
d
Enter first number: 9
Enter Second number: 0
Enter a number other than 0: 5
9.00 . 5.00 = 1.80
Enter your choice:
a. add      b. subtract
m. multiply d. divide
q. quit
e
Please respond with a, b, m, d, or q.
q
Bye.
posted @ 2020-06-10 22:39  keep-minding  阅读(92)  评论(0)    收藏  举报