后浪笔记一零二四

C程序设计语言-第7章输入与输出

第7章 输入与输出

输入/输出功能并不是C语言本身的组成部分,所以到目前为止,我们并没有过多地强调它们。但是,程序与环境之间的交互比我们在前面部分中描述的情况要复杂很多。本章将讲述标准库,介绍一些输入/输出函数、字符串处理函数、存储管理函数与数学函数,以及其他一些C语言程序的功能。本章讨论的重点将放在输入/输出上。

ANSI标准精确地定义了这些库函数,所以,在任何可以使用C语言的系统中都有这些函数的兼容形式。如果程序的系统交互部分仅仅使用了标准库提供的功能,则可以不经修改地从一个系统移植到另一个系统中。

这些库函数的属性分别在十多个头文件中声明,前面已经遇到过一部分,如<stdio.h><string.h><ctype.h>。我们不打算把整个标准库都罗列于此,因为我们更关心如何使用标准库编写C语言程序。附录B对标准库进行了详细的描述。

7.1 标准输入/输出

我们在第1章中讲过,标准库实现了简单的文本输入/输出模式。文本流由一系列行组成,每一行的结尾是一个换行符。如果系统没有遵循这种模式,则标准库将通过一些措施使得该系统适应这种模式。例如,标准库可以在输入端将回车符和换页符都转换为换行符,而在输出端进行反向转换。

最简单的输入机制是使用getchar函数从标准输入中(一般为键盘)一次读取一个字符:

int getchar(void)

getchar函数在每次调用时返回下一个输入字符。若遇到文件结尾,则返回EOF。符号常量EOF在头文件<stdio.h>中定义,其值一般为-1,但程序中应该使用EOF来测试文件是否结束,这样才能保证程序同EOF的特定值无关。

在许多环境中,可以使用符号<来实现输入重定向,它将把键盘输入转换为文件输入:如果程序prog中使用了函数getchar,则命令行

prog <infile

将使得程序prog从输入文件infile(而不是从键盘)中读取字符。实际上,程序prog本身并不在意输入方式的改变,并且,字符串“<infile”也并不包含在argv的命令行参数中。如果输入通过管道机制来自于另一个程序,那么这种输入切换也是不可见的。比如,在某些系统中,下列命令行:

otherprog | prog

将运行两个程序otherprog和prog,并将程序otherprog的标准输出通过管道重定向到程序prog的标准输入上。

函数

int putchar(int)

用于输出数据。putchar(c)将字符c送至标准输出上,在默认情况下,标准输出为屏幕显示。如果没有发生错误,则函数putchar将返回输出的字符;如果发生了错误,则返回EOF。同样,通常情况下,也可以使用“>输出文件名”的格式将输出重定向到某个文件中。例如,如果程序prog调用了函数putchar,那么命令行

prog > 输出文件名

将把程序prog的输出从标准输出设备重定向到文件中。如果系统支持管道,那么命令行

prog | anotherprog

将把程序prog的输出从标准输出通过管道重定向到程序anotherprog的标准输入中。

函数printf也向标准输出设备上输出数据。我们在程序中可以交叉调用函数putchar和printf,输出将按照函数调用的先后顺序依次产生。

使用输入/输出库函数的每个源程序文件必须在引用这些函数之前包含下列语句:

#include <stdio.h>

当文件名用一对尖括号<>括起来时,预处理器将在由具体实现定义的有关位置中查找指定的文件(例如,在UNIX系统中,文件一般放在目录/usr/include中)。

许多程序只从一个输入流中读取数据,并且只向一个输出流中输出数据。对于这样的程序,只需要使用函数getchar、putchar和printf实现输入/输出即可,并且对程序来说已经足够了。特别是,如果通过重定向将一个程序的输出连接到另一个程序的输入,仅仅使用这些函数就足够了。例如,考虑下列程序,它用于将输入转换为小写字母的形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>
#include <ctype.h>

main() {
    int c;

    while ((c = getchar()) != EOF)
        /* tolower函数: 将输入转换为小写形式 */
        putchar(tolower(c));
    return 0;
}

函数tolower在头文件<ctype.h>中定义,它把大写字母转换为小写形式,并把其他字符原样返回。我们在前面提到过,头文件<stdio.h>中的getchar和putchar“函数”以及<ctype.h>中的tolower“函数”一般都是宏,这样就避免了对每个字符都进行函数调用的开销。我们将在8.5节介绍它们的实现方法。无论<ctype.h>中的函数在给定的机器上是如何实现的,使用这些函数的程序都不必了解字符集的知识。

练习7-1 编写一个程序,根据它自身被调用时存放在argv[0]中的名字,实现将大写字母转换为小写字母或将小写字母转换为大写字母的功能。

7.2 格式化输出————printf函数

输出函数printf将内部数值转换为字符的形式。前面的有关章节中已经使用过该函数。下面只讲述该函数最典型的用法,附录B中给出了该函数完整的描述。

int printf(char *format, arg1, arg2, ...);

函数printf在输出格式format的控制下,将其参数进行转换与格式化,并在标准输出设备上打印出来。它的返回值为打印的字符数。

格式字符串包含两种类型的对象:普通字符和转换说明。在输出时,普通字符将原样不动地复制到输出流中,而转换说明并不直接输出到输出流中,而是用于控制printf中参数的转换和打印。每个转换说明都由一个百分号字符(即%)开始,并以一个转换字符结束。在字符%和转换字符中间可能依次包含下列组成部分:

  • 负号,用于指定被转换的参数按照左对齐的形式输出。
  • 数,用于指定最小字段宽度。转换后的参数将打印不小于最小字段宽度的字段。如果有必要,字段左边(如果使用左对齐的方式,则为右边)多余的字符位置用空格填充以保证最小字段宽。
  • 小数点,用于将字段宽度和精度分开。
  • 数,用于指定精度,即指定字符串中要打印的最大字符数、浮点数小数点后的位数、整型最少输出的数字数目。
  • 字母h或l,字母h表示将整数作为short类型打印,字母l表示将整数作为long类型打印。

表7-1列出了所有的转换字符。如果%后面的字符不是一个转换说明,则该行为是未定义的。

表7-1 printf函数基本的转换说明

字符 参数类型:输出形式
d,i int类型;十进制数
o int类型;无符号八进制数(没有前导0)
x,X int类型;无符号十六进制数(没有前导0x或0X),10~15分别用abcdef或ABCDEF表示
u int类型:无符号十进制数
c int类型;单个字符
s char *类型;顺序打印字符串中的字符,直到遇到'\0'或已打印了由精度指定的字符数为止
f double类型;十进制小数[-]m.dddddd,其中d的个数由精度指定(默认值为6)
e,E double类型;[-]m.dddddd e ±xx或[-]m.dddddd E ±xx,其中d的个数由精度指定(默认值为6)
g,G double类型:如果指数小于-4或大于等于精度,则用%e或%E格式输出,否则用%f格式输出。尾部的0和小数点不打印
p void *类型;指针(取决于具体实现)
% 不转换参数;打印一个百分号%

在转换说明中,宽度或精度可以用星号*表示,这时,宽度或精度的值通过转换下一个参数(必须为int类型)来计算。例如,为了从字符串s中打印最多max个字符,可以使用下列语法:

printf("%.*s", max, s);

前面的章节中已经介绍过大部分的格式转换,但没有介绍与字符串相关的精度。下表说明了在打印字符串“hello, world”(12个字符)时根据不同的转换说明产生的不同结果。我们在每个字段的左边和右边加上冒号,这样可以清晰地表示出字段的宽度。

:%s:          :hello, world:
:%10s:        :hello, world:
:%.10s:       :hello, wor:
:%-10s:       :hello, world:
:%.15s:       :hello, world:
:%-15s:       :hello, world   :
:%15.10s:     :     hello, wor:
:%-15.10s:    :hello, wor     :

注意: 函数printf使用第一个参数判断后面参数的个数及类型。如果参数的个数不够或者类型错误,则将得到错误的结果。请注意下面两个函数调用之间的区别:

1
2
printf(s);        /* 如果字符串s含有字符 %,输出将出错 */
printf("%s", s);  /* 正确 */

函数sprintf执行的转换和函数printf相同,但它将输出保存到一个字符串中:

int sprintf(char *string, char *format, arg1, arg2, ...);

sprintf函数和printf函数一样,按照format格式格式化参数序列arg1、arg2、…,但它将输出结果存放到string中,而不是输出到标准输出中。当然,string必须足够大以存放输出结果。

练习7-2 编写一个程序,以合理的方式打印任何输入。该程序至少能够根据用户的习惯以八进制或十六进制打印非图形字符,并截取长文本行。

7.3 变长参数表

本节以实现函数printf的一个最简单版本为例,介绍如何以可移植的方式编写可处理变长参数表的函数。因为我们的重点在于参数的处理,所以,函数minprintf只处理格式字符串和参数,格式转换则通过调用函数printf实现。

函数printf的正确声明形式为:

int printf(char *fmt, ...)

其中,省略号表示参数表中参数的数量和类型是可变的。省略号只能出现在参数表的尾部。因为minprintf函数不需要像printf函数一样返回实际输出的字符数,因此,我们将它声明为下列形式:

void minprintf(char *fmt, ...);

编写函数minprintf的关键在于如何处理一个甚至连名字都没有的参数表。标准头文件<stdarg.h>中包含一组宏定义,它们对如何遍历参数表进行了定义。该头文件的实现因不同机器而不同,但提供的接口是一致的。

va_list类型用于声明一个变量,该变量将依次引用各参数。在函数minprintf中,我们将该变量称为ap,意思是“参数指针”。宏va_start将ap初始化为指向第一个无名参数的指针。在使用ap之前,该宏必须被调用一次。参数表必须至少包含一个有名参数,va_start将最后一个有名参数作为起点。

每次调用va_arg,该函数都将返回一个参数,并将ap指向下一个参数。va_arg使用一个类型名来决定返回的对象类型、指针移动的步长。最后,必须在函数返回之前调用va_end,以完成一些必要的清理工作。

基于上面这些讨论,我们实现的简化printf函数如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdarg.h>

/* minprint函数:带有可变参数表的简化的printf函数 */
void minprintf(char *fmt, ...) {
    va_list ap;       /* 依次指向每个无名参数 */
    char *p, *sval;
    int ival;
    double dval;

    va_start(ap, fmt);  /* 将ap指向第一个无名参数 */
    for (p = fmt; *p; p++) {
        if (*p != '%') {
            putchar(*p);
            continue;
        }
        switch(*++p) {
        case 'd':
            ival = va_arg(ap, int);
            printf("%d", ival);
            break;
        case 'f':
    l       dval = va_arg(ap, double);
            printf("%f", dval);
            break;
        case 's':
            for (sval = va_arg(ap, char *); *sval; sval++)
                putchar(*sval);
            break;
        default:
            putchar(*p);
            break;
        }
    }
    va_end(ap);    /* 结束时的清理工作 */
}

练习7-3 改写minprintf函数,使它能完成printf函数的更多功能。

7.4 格式化输入————scanf函数

输入函数scanf对应于输出函数printf,它在与后者相反的方向上提供同样的转换功能。

具有变长参数表的函数scanf的声明形式如下:

int scanf(char *format, ...);

scanf函数从标准输入中读取字符序列,按照format中的格式说明对字符序列进行解释,并把结果保存到其余的参数中。格式参数format将在接下来的内容中进行讨论。其他所有参数都必须是指针,用于指定经格式转换后的相应输入保存的位置。和上节讲述printf一样,本节只介绍scanf函数最有用的一些特征,而并不完整地介绍。

当scanf函数扫描完其格式串,或者碰到某些输入无法与格式控制说明匹配的情况时,该函数将终止,同时,成功匹配并赋值的输入项的个数将作为函数值返回,所有,该函数的返回值可以用来确定已匹配的输入项的个数。如果到达文件的结尾,该函数将返回EOF。注意,返回EOF与0是不同的,0表示下一个输入字符与格式串中的第一个格式说明不匹配。下一次调用scanf函数将从上一次转换的最后一个字符的下一个字符开始继续搜索。

另外还有一个输入函数sscanf,它用于从一个字符串(而不是标准输入)中读取字符序列:

int sscanf(char *string, char *format, arg1, arg2, ...);

它按照格式参数format中规定的格式扫描字符串string,并把结果分别保存到arg1、arg2、…这些参数中。这些参数必须是指针。

格式串通常都包含转换说明,用于控制输入的转换。格式串可能包含下列部分:

  • 空格或制表符,在处理过程中将被忽略
  • 普通字符(不包含%),用于匹配输入流中下一个非空白符字符。
  • 转换说明,依次由一个%、一个可选的赋值禁止字符*、一个可选的数值(指定最大字段宽度)、一个可选的h、l或L字段(指定目标对象的宽度)以及一个转换字符组成。

转换说明控制下一个输入字段的转换。一般来说,转换结果存放在相应的参数指向的变量中。但是,如果转换说明中有赋值禁止字符*,则跳过该输入字符,不进行赋值。输入字段定义为一个不包含空白符的字符串,其边界定义为到下一个空白符或达到指定的字段宽度。这表明scanf函数将越过行边界读取输入,因为换行符也是空白符。(空白符包括空格符、横向制表符、换行符、回车符、纵向制表符以及换页符)。

转换字符指定对输入字符的解释。对应的参数必须是指针,这也是C语言通过值调用语义所要求的。表7-2中列出了这些转换字符。

表7-2 scanf函数的基本转换说明

字符 输入数据;参数类型
d 十进制整数;int*类型
i 整数;int*类型,可以是八进制(以0开头)或十六进制(以0x或0X开头)
o 八进制整数(可以以0开头,也可以不以0开头);int *类型
u 无符号十进制整数;unsigned int*类型
x 十六进制整数(可以0x或0X开头,也可以不以0x或0X开头);int *类型
c 字符;char *类型,将接下来的多个输入字符(默认为1个字符)存放到指定位置。该转换规范通常不跳过空白符。如果需要读入下一个非空白符,可以使用%1s
s 字符串(不加引号);char *类型,指向一个足以存放该字符串(还包括尾部的字符'\0')的字符数组。字符串的末尾将被添加一个结束符’\0'
e,f,g 浮点数,它可以包括正负号(可选)、小数点(可选)及指数部分(可选);float*类型
% 字符%;不进行任何赋值操作

转换说明d、i、o、u及x的前面可以加上字符h或l。前缀h表明参数表的相应参数是一个指向short类型的非int类型的指针,前缀l表明参数表的相应参数是一个指向long类型的指针。类似地,转换说明e、f和g的前面也可以加上前缀l,它表明参数表的相应参数是一个指向double类型而非float类型的指针。

来看第一个例子。我们通过函数scanf执行输入转换来改写第4章中的简单计算器程序,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>

main() {  /* 简单计算器程序 */
    double sum, v;

    sum = 0;
    while(scanf("%lf", &v) == 1)
        printf("\t%.2f\n", sum += v);
    return 0;
}

假设我们要读取包含下列日期格式的输入行:

25 Dec 1988

相应的scanf语句可以这样编写:

1
2
3
4
int day, year;
char monthname[20];

scanf("%d %s %d", &day, monthname, &year);

因为数组名本身就是指针,所以,monthname的前面没有取地址运算符&

字符字面值也可以出现在scanf的格式串中,它们必须与输入中相同的字符匹配。因此,我们可以使用下列scanf语句读入形如 mm/dd/yy 的日期数据:

1
2
3
int day, month, year;

scanf("%d/%d/%d", &month, &day, &year);

scanf函数忽略格式串中的空格和制表符。此外,在读取输入值时,它将跳过空白符(空格、制表符、换行符等等)。如果要读取格式不固定的输入,最好每次读入一行,然后再用sscanf将合适的格式分离出来读入。例如,假定我们需要读取一些包含日期数据的输入行,日期的格式可能是上述任一种形式。我们可以这样编写程序:

1
2
3
4
5
6
7
8
while (getLine(line, sizeof(line)) > 0) {
    if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3)
        printf("valid: %s\n", line);     /* 25 Dec 1988形式的日期数据 */
    else if (sscanf(line, "%d/%d/%d", &month, &day, &year) == 3)
        printf("valid: %s\n", line);     /* mm/dd/yy形式的日期数据 */
    else
        printf("invalid: %s\n", line);   /* 日期形式无效 */
}

scanf函数可以和其他输入函数混合使用。无论调用哪个输入函数,下一个输入函数的调用将从scanf没有读取的第一个字符处开始读取数据。

注意,scanf和sscanf函数的所有参数都必须是指针。最常见的错误是将输入语句写成下列形式:

1
scanf("%d", n);

正确的形式应该为:

scanf("%d", &n)

编译器在编译时一般检查不到这类错误。

练习7-4 类似于上一节中的函数minprintf,编写scanf函数的一个简化版本。

练习7-5 改写第4章中的后缀计算器程序,用scanf函数和(或)sscanf函数实现输入以及数的转换。

7.5 文件访问

到目前为止,我们讨论的例子都是从标准输入读取数据,并向标准输出输出数据。标准输入和标准输出是操作系统自动提供给程序访问的。

接下来,我们编写一个访问文件的程序,且它所访问的文件还没有连接到该程序。程序cat可以用来说明该问题,它把一批命名文件串联后输出到标准输出上。cat可用来在屏幕上打印文件,对于那些无法通过名字访问文件的程序来说,它还可以用作通用的输入收集器。例如,下列命令行:

cat x.c y.c

将在标准输出上打印文件x.c和y.c的内容。

问题在于,如何设计命名文件的读取过程呢?换句话说,如何将用户需要使用的文件的外部名同读取数据的语句关联起来。

方法其实很简单。在读取一个文件之前,必须通过库函数fopen打开该文件。fopen用类似于x.c或y.c这样的外部名与操作系统进行某些必要的连接和通信(我们不必关心这些细节),并返回一个随后可以用于文件读写操作的指针。

该指针称为文件指针,它指向一个包含文件信息的结构,这些信息包括:缓冲区的位置、缓冲区中当前字符的位置、文件的读或写状态、是否出错或是否已经到达文件结尾等等。用户不必关心这些细节,因为<stdio.h>中已经定义了一个包含这些信息的结构FILE。在程序中只需按照下列方式声明一个文件指针即可:

1
2
FILE *fp;
FILE *fopen(char *name, char *mode);

在本例中,fp是一个指向结构FILE的指针,并且,fopen函数返回一个指向结构FILE的指针。注意,FILE像int一样是一个类型名,而不是结构标记。它是通过typedef定义的(UNIX系统中fopen的实现细节将在8.5节中讨论)。

在程序中,可以这样调用fopen函数:

fp = fopen(name, mode);

fopne的第一个参数是一个字符串,它包含文件名。第二个参数是访问模式,也是一个字符串,用于指定文件的使用方式。允许的模式包括:读(“r”)、写(“w”)及追加(“a”)。某些系统还区分文本文件和二进制文件,对后者的访问需要在模式字符串中增加字符“b”。

如果打开一个不存在的文件用于写或追加,该文件将被创建(如果可能的话)。当以写方式打开一个已存在的文件时,该文件原来的内容将被覆盖。但是,如果以追加方式打开一个文件,则该文件原来的内容将被保留不变。读一个不存在的文件会导致错误,其他一些操作也可能导致错误,比如试图读取一个无读取权限的文件。如果发生错误,fopen将返回NULL。(可以更进一步地定位错误的类型,具体方法请参见附录B.1节中关于错误处理函数的讨论。)

文件被打开后,就需要考虑采用哪种方法对文件进行读写。有多种方法可供考虑,其中,getc和putc函数最为简单。getc从文件中返回下一个字符,它需要知道文件指针,以确定对哪个文件执行操作:

int getc(FILE *fp);

getc函数返回fp指向的输入流中的下一个字符。如果到达文件尾或出现错误,该函数将返回EOF。

putc是一个输出函数,如下所示:

int putc(int c, FILE *fp);

该函数将字符c写入到fp指向的文件中,并返回写入的字符。如果发生错误,则返回EOF。类似于getchar和putchar,getc和putc是宏而不是函数。

启动一个C语言程序时,操作系统环境负责打开3个文件,并将这3个文件的指针提供给该程序。这3个文件分别是标准输入、标准输出和标准错误,相应的文件指针分别为stdin、stdout和stderr,它们在<stdio.h>中声明。在大多数环境中,stdin指向键盘,而stdout和stderr指向显示器。我们从7.1节的讨论中可以知道,stdin和stdout可以被重定向到文件或管道。

getchar和putchar函数可以通过getc、putc、stdin及stdout定义如下:

1
2
#define getchar()  getc(stdin)
#define putchar()  putc((c), stdout)

对于文件的格式化输入或输出,可以使用函数fscanf和fprintf。它们与scanf和printf函数的区别仅仅在于它们的第一个参数是一个指向所要读写的文件的指针,第二个参数是格式串。如下所示:

1
2
int fscanf(FILE *fp, char *format, ...);
int fprintf(FILE *fp, char *format, ...);

掌握这些预备知识之后,我们现在就可以编写出将多个文件连接起来的cat程序了。该程序的设计思路和其他许多程序类似。如果有命令行参数,参数将被解释为文件名,并按顺序逐个处理。如果没有参数,则处理标准输入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

/* cat函数:连接多个文件,版本1 */
main(int argc, char *argv[]) {
    FILE *fp;
    void filecopy(FILE *, FILE *);

    if (argc == 1)   /* 如果没有命令行参数,则复制标准输入 */
        filecopy(stdin, stdout);
    else
        while(--argc > 0)
            if ((fp = fopen(*++argv, "r")) == NULL) {
                printf("cat: can't open %s\n", *argv);
                return 1;
            } else {
                filecopy(fp, stdout);
                fclose(fp);
            }
    return 0;
}

/* filecopy函数:将文件ifp复制到文件ofp */
void filecopy(FILE *ifp, FILE *ofp) {
    int c;

    while ((c = getc(ifp)) != EOF)
        putc(c, ofp);
}

文件指针stdin与stdout都是FILE*类型的对象。但它们是常量,而非变量,因此不能对它们赋值。

函数

int fclose(FILE *fp)

执行和fopen相反的操作,它断开由fopen函数建立的文件指针和外部名之间的连接,并释放文件指针以供其他文件使用。因为大多数操作系统都限制了一个程序可以同时打开的文件数,所以,当文件指针不再需要时就应该释放,这是一个好的编程习惯,就像我们在cat程序中所做的那样。对输出文件执行fclose还有另外一个原因:它将把缓冲区中由putc函数正在收集的输出写到文件中。当程序正常终止时,程序会自动为每个打开的文件调用fclose函数。(如果不需要使用stdin与stdout,可以把它们关闭掉。也可以通过库函数freopen重新指定它们。)

7.6 错误处理————stderr和exit

cat程序的错误处理功能并不完善。问题在于,如果因为某种原因而造成其中的一个文件无法访问,相应的诊断信息要在该连接的输出的末尾才能打印出来。当输出到屏幕时,这种处理方法尚可以接受,但如果输出到一个文件或通过管道输出到另一个程序时,就无法接受了。

为了更好地处理这种情况,另一个输出流以与stdin和stdout相同的方式分派给程序,即stderr。即使对标准输出进行了重定向,写到stderr中的输出通常也会显示在屏幕上。

下面我们改写cat程序,将其出错信息写到标准错误文件上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

/* cat函数: 连接多个文件,版本2  */
main(int argc, char *argv[]) {
    FILE *fp;
    void filecopy(FILE *, FILE *);
    char *prog = argv[0];    /* 记下程序名,供错误处理用 */

    if (argc == 1)   /* 如果命令行不带参数,则复制标准输入 */
        filecopy(stdin, stdout);
    else
        while(--argc > 0)
            if ((fp = fopen(*++argv, "r")) == NULL) {
                fprintf(stderr, "%s: can't open %s\n",
                    prog, *argv);
                exit(1);
            } else {
                filecopy(fp, stdout);
                fclose(fp);
            }
    if (ferror(stdout)) {
        fprintf(stderr, "%s: error writing stdout\n", prog);
        exit(2);
    }
    exit(0);
}

该程序通过两种方式发出出错信息。首先,将fprintf函数产生的诊断信息输出到stderr上,因此诊断信息将会显示在屏幕上,而不是仅仅输出到管道或输出文件中。诊断信息中包含argv[0]中的程序名,因此,当该程序和其他程序一起运行时,可以识别错误的来源。

其次,程序使用了标准库函数exit,当该函数被调用时,它将终止调用程序的执行。任何调用该程序的进程都可以获取exit的参数值,因此,可通过另一个将该程序作为子进程的程序来测试该程序的执行是否成功。按照惯例,返回值0表示一切正常,而非0返回值通常表示出现了异常情况。exit为每个已打开的输出文件调用fclose函数,以将缓冲区中的所有输出写到相应的文件中。

在主程序main中,语句return expr等价于exit(expr)。但是,使用函数exit有一个优点,它可以从其他函数中调用,并且可以用类似于第5章中描述的模式查找程序查找这些调用。

如果流fp中出现错误,则函数ferror返回一个非0值。

int ferror(FILE *fp);

尽管输出错误很少出现,但还是存在的(例如,当磁盘满时),因此,成熟的产品程序应该检查这种类型的错误。

函数feof(FILE*)ferror类似。如果指定的文件达到文件结尾,它将返回一个非0值。

int feof(FILE *fp)

在上面的小程序中,我们的目的是为了说明问题,因此并不太关心程序的退出状态,但对于任何重要的程序来说,都应该让程序返回有意义且有用的值。

7.7 行输入和行输出

标准库提供了一个输入函数fgets,它和前面几章中用到的函数getLine类似。

char *fgets(char *line, int maxline, FILE *fp)

fgets函数从fp指向的文件中读取下一个输入行(包括换行符),并将它存放在字符数组line中,它最多可读取maxline-1个字符。读取的行将以'\0'结尾保存到数组中。通常情况下,fgets返回line,但如果遇到了文件结尾或发生了错误,则返回NULL(我们编写的getLine函数返回行的长度,这个值更有用,当它为0时意味着已经达到了文件的结尾)。

输出函数fputs将一个字符串(不需要包含换行符)写入到一个文件中:

int fputs(char *line, FILE *fp)

如果发生错误,该函数将返回EOF,否则返回一个非负值。

库函数gets和puts的功能与fgets和fputs函数类似,但它们是对stdin和stdout进行操作。有一点我们需要注意,gets函数在读取字符串时将删除结尾的换行符('\n'),而puts函数在写入字符串时将在结尾添加一个换行符。

下面的代码是标准库中fgets和fputs函数的代码,从中可以看出,这两个函数并没有什么特别的地方。代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* fgets函数:从iop指向的文件中最多读取n-1个字符,再加上一个NULL */
char *fgets(char *s, int n, FILE *iop) {
    register int c;
    regisster char *cs;

    cs = s;
    while(--n > 0 && (c = getc(iop)) != EOF)
        if ((*cs++ = c) == '\n')
            break;
    *cs = '\0';
    return (c == EOF && cs == s) ? NULL : s;
}

/* fputs函数:将字符串s输出到iop指向的文件中 */
int fputs(char *s, FILE *iop) {
    int c;

    while(c = *s++)
        puts(c, iop);
    return ferror(iop) ? EOF: 非负值;
}

ANSI标准规定,ferror在发生错误时返回非0值,而fputs在发生错误时返回EOF,其他情况返回一个非负值。

使用fgets函数很容易实现getLine函数:

1
2
3
4
5
6
7
/* getLines函数:读入一个输入行,并返回其长度 */
int getLines(char *line, int max) {
    if (fgets(line, max, stdin) == NULL)
        return 0;
    else
        return strlen(line);
}

练习7-6 编写一个程序,比较两个文件并打印它们第一个不相同的行。

练习7-7 修改第5章的模式查找程序,使它从一个命令文件的集合中读取输入(有文件名参数时),如果没有文件名参数,则从标准输入中读取输入。当发现一个匹配行时,是否应该将相应的文件名打印出来?

练习7-8 编写一个程序,以打印一个文件集合,每个文件从新的一页开始打印,并且打印每个文件相应的标题和页数。

7.8 其他函数

标准库提供了很多功能各异的函数。本节将对其中特别有用的函数做一个简要的概述。更详细的信息以及其他许多没有介绍的函数请参见附录B。

7.8.1 字符串操作函数

前面已经提到过字符串函数strlen、strcpy、strcat和strcmp,它们都在头文件<string.h>中定义。在下面的各个函数中,s与t为char *类型,c与n为int类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
strcat(s,t)      t指向的字符串连接到s指向的字符串的末尾
strncat(s,t,n)   t指向的字符串中前n个字符连接到s指向的字符串的末尾
strcmp(s,t)      根据s指向的字符串小于s<t),等于(s==t)或大于(s>t
               t指向的字符串的不同情况,分别返回负整数、0或正整数
strncmp(s,t,n)   strcmp相同,但只在前n个字符中比较
strcpy(s,t)      t指向的字符串复制到s指向的位置
strncpy(s,t,n)   t指向的字符串中前n个字符复制到s指向的位置
strlen(s)        返回s指向的字符串的长度
strchr(s,c)      s指向的字符串中查找c,若找到,则返回指向它第一次出现
               的位置的指针,否则返回NULL
strrchr(s,c)     s指向的字符串中查找c,若找到,则返回指向它最后一次出
               现的位置的指针,否则返回NULL

7.8.2 字符类别测试和转换函数

头文件<ctype.h>中定义了一些用于字符测试和转换的函数。在下面各个函数中,c是一个可表示为unsigned char类型或EOF的int对象。该函数的返回值类型为int。

isalpha(c)  若c是字母,则返回一个非0值,否则返回0
isupper(c)  若c是大写字母,则返回一个非0值,否则返回0
islower(c)  若c是小写字母,则返回一个非0值,否则返回0
isdigit(c)  若c是数字,则返回一个非0值,否则返回0
isalnum(c)  若isalpha(c)或isdigit(c),则返回一个非0值,否则返回0
isspace(c)  若c是空格、横向制表符、换行符、回车符、换页符或纵向制表符,则
            返回一个非0值
toupper(c)  返回c的大写形式
tolower(c)  返回c的小写形式

7.8.3 ungetc函数

标准库提供了一个称为ungetc的函数,它与第4章中编写的函数ungetch相比功能更受限制。

int ungetc(int c, FILE *fp)

该函数将字符c写回到文件fp中。如果执行成功,则返回c,否则返回EOF。每个文件只能接收一个写回字符。ungetc函数可以和任何一个输入函数一起使用,比如scanf、getc或getchar。

7.8.4 命令执行函数

函数system(char*s)执行包含在字符串s中的命令,然后继续执行当前程序。s的内容在很大程度上与所用的操作系统有关。下面来看一个UNIX操作系统环境的小例子。语句

system("date");

将执行程序date,它在标准输出上打印当天的日期和时间。system函数返回一个整型的状态值,其值来自于执行的命令,并同具体系统有关。在NUIX系统中,返回的状态是exit的返回值。

7.8.5 存储管理函数

函数malloc和calloc用于动态地分配存储块。函数malloc的声明如下:

void *malloc(size_t n)

当分配成功时,它返回一个指针,该指针指向n字节长度的未初始化的存储空间,否则返回NULL。函数calloc的声明为

void *calloc(size_t n, size_t size)

当分配成功时,它返回一个指针,该指针指向的空闲空间足以容纳由n个指定长度的对象组成的数组,否则返回NULL。该存储空间被初始化为0。

根据请求的对象类型,malloc或calloc函数返回的指针满足正确的对齐要求。下面的例子进行了类型转换:

1
2
int *ip;
ip = (int *) calloc(n, sizeof(int));

free(p)函数释放p指向的存储空间,其中,p是此前通过调用malloc或calloc函数得到的指针。存储空间的释放顺序没有什么限制,但是,如果释放一个不是通过调用malloc或calloc函数得到的指针所指向的存储空间,将是一个很严重的错误。

使用已经释放的存储空间同样是错误的。下面所示的代码是一个很典型的错误代码段,它通过一个循环释放列表中的项目:

1
2
for (p = head; p != NULL; p = p->next)  /* 错误的代码 */
    free(p);

正确的处理方法是,在释放项目之前先将一切必要的信息保存起来,如下所示:

1
2
3
4
for (p = head; p != NULL; p = q) {
    q = p->next;
    free(p);
}

8.7 节给出了一个类似于malloc函数的存储分配程序的实现。该存储分配程序分配的存储块可以以任意顺序释放。

7.8.6 数学函数

头文件<math.h>中声明了20多个数学函数。下面介绍一些常用的数学函数,每个函数带有一个或两个double类型的参数,并返回一个double类型的值。

1
2
3
4
5
6
7
8
9
sin(x)         x的正弦函数,其中x用弧度表示
cos(x)         x的余弦函数,其中x用弧度表示
atan2(y,x)     y/x的反正切函数,其中,x和y用弧度表示
exp(x)         指数函数e^x
log(x)         x的自然对数(以e为底),其中,x>0
log10(x)       x的常用对数(与10为底),其中,x>0函数
pow(x,y)       计算x^y的值
sqrt(x)        x的平方根x>=0
fabs(x)        x的绝对值

7.8.7 随机数发生器函数

函数rand()生成介于0和RAND_MAX之间的伪随机整数序列。其中RAND_MAX是在头文件<stdlib.h>中定义的符号常量。下面是一种生成大于等于0但小于1的随机浮点数的方法:

#define frand() {(double) rand() / (RAND_MAX+1.0)}

(如果所用的函数库中已经提供了一个生成浮点数随机数的函数,那么它可能比上面这个函数具有更好的统计学特性。)

函数srand(unsigned)设置rand函数的种子数。我们在2.7节中给出了遵循标准的rand和srand函数的可移植的实现。

练习7-9 类似于isupper这样的函数可以通过某种方式实现以达到节省空间或时间的目的。考虑节省空间或时间的实现方式。


专题:

本文发表于 2022-09-13,最后修改于 2022-09-13。

本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。


上一篇 « C程序设计语言-第8章UNIX系统接口 下一篇 » C程序设计语言-第6章结构

赞赏支持

请我吃鸡腿 =^_^=

i ysf

云闪付

i wechat

微信

推荐阅读

Big Image