Warning:
为了避免非零基础人群感到身体不适、头晕恶心、易怒及粗口,请不要查看以下内容。
本章是C程序设计语言的最后一章(除去最后一章unix相关内容),我们将会学习有关输入输出的相关知识。
输入/输出功能并不是C语言本身的组成部分,所以到目前为止,我们并没有过多地强调它们。但是,程序与环境之间的交互比我们前面部分中描述的情况要复杂的多。本章将讲述标准库,介绍一些输入/输出函数、字符串处理函数、存储管理函数与数学函数,以及其它一些C语言程序的功能。本章讨论的重点是输入/输出。
ANSI标准精确定义了这些库函数,所以,在任何可以使用C语言的系统中都有这些函数的兼容形式。如果程序的系统交互部分仅仅使用了标准库提供的功能,则可以不经修地从一个系统移植到另一个系统中。
这些库函数的属性分别在十多个头文件中声明,前面已经遇到过一部分,如、 和 。我们不打算把整个标准库都罗列于此,因为我们更关心如何使用标准库编写C语言程序。
最简单的输入机制是使用getchar函数从标准输入中(一般为键盘)一次读取一个字符:
int getchar(void)
getchar函数在每次被调用时返回下一个输入字符,若遇到文件结尾,则返回EOF。
在许多环境中可以使用符号 < 来实现输入重定向,它将把键盘输入替换为文件输入:如果程序prog中使用了函数getchar,则面临:
prog <infile
将使得程序prog从输入文件infile(而不是从键盘)中读取字符。
使用输入/输出库函数的每个源文件必须在引用这些函数之前包括下列语句:
#include
许多程序只从一个输入流中读取数据,并且只向一个输出流中输出数据。对于这样的程序,只需要使用函数getchar、putchar和printf实现输入/输出即可,并且对于程序来说已经足够了。特别是,如果通过重定向将一个程序的输出连接到另一个程序的输入,仅仅使用这些函数就足够了。
输出函数printf将内部数值转换为字符的形式。在输出格式format的控制下,它将其参数进行转换与格式化,并在标准输出设备上打印出来。它的返回值为打印的字符数。
格式字符串包含两种类型的对象:普通字符和转换说明。在输出时,普通字符将原样不动的复制到输出流中,而转换说明并不之间输出到输出流中,而是用于控制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输出。 |
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, world: |
:%-15. 10s: | :hello, wor : |
…上面的表格貌似有点问题,因为CSDN的这个编辑器我打不出来多个空格。暂时忽视它…
注意:函数printf使用第一个参数判断后面参数的个数及类型。如果参数的个数不够或类型错误,则将得到错误的结果。
函数sprintf执行的转换和printf相同,但它将输出保存到一个字符串中。
本节以实现函数printf的一个最简单版本为例,介绍如何以可移植的方式编写可处理变长参数表的函数。因为我们的重点在于参数的处理,所以,函数minprintf只处理格式字符串和参数,格式转换则通过调用函数printf实现。
函数printf的正确声明形式为:
int printf(char *fmt, ...)
其中,省略号表示参数表中参数的数量和类型是可变的。省略号只能出现在参数表的尾部。因为minprintf函数不需要像printf函数一样返回实际输出的字符数,因此,我们将它声明为下列形式:
void minprintf(char *fmt, ...)
编写函数minprintf的关键在于如何处理一个甚至连名字都没有的参数表。标准头文件
va_list类型用于声明一个变量,该变量将依次引用各参数。在函数minprintf中,我们将该变量称为ap,意思是“参数指针”。宏va_start将ap初始化为指向一个无名参数的指针。在使用ap之前,该宏必须被调用一次。参数表必须在函数返回之前调用va_end,以完成一些必要工作。
基于上面这些讨论,我们实现的简化printf函数如下所示:
#include
/* minprintf函数:带有可变参数表的简化的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':
ival = 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); /* 结束时的清理工作 */
}
输入函数scanf对应于输出函数printf,它在与后者相反的方向上提供同样的转换功能。
具有变长参数表的函数scanf的声明形式如下:
int scanf(char *format, ...)
scanf函数从标准输入中读取字符序列,按照format中的格式说明对字符序列进行解释,并把结果保存到其余的参数中。格式参数format将在接下来的内容中进行讨论。其它所有参数都必须是指针,用于指定经格式转换后的相应输入保存的为止。和上节讲述的printf一样,本节只介绍scanf函数一些有用的特征。
当scanf函数扫码完其格式串,或者碰到某些输入无法与格式控制说明匹配的情况时,该函数将终止,同时,成功匹配并赋值的输入项的个数将作为函数值返回。所以,该函数的返回值可用来确定已匹配的输入项的个数。如果到达文件的结尾,该函数将返回EOF。若下一个输入字符与格式串中的第一个格式说明不匹配,将返回0。下一次调用函数scanf将从上一次转换的最后一个字符的下一个字符开始继续搜索。
另外还有一个输入函数sscanf,它用于从一个字符串(而不是标准输入)中读取字符序列:
int sscanf(char *string, char *format, arg1, arg2, ...)
它按照格式参数format中规定的格式扫描字符串string,并把结果分别保存到arg1, arg2, …中。这些参数必须是指针。
格式串中通常都包含转换说明,用于控制输入的转换。格式串可能包含下列部分:
转换说明控制下一个输入字段的转换。一般来说,转换结果存放在相应的参数指向的变量中,但是,如果转换说明中有赋值禁止符*,则跳过该输入字段,不进行赋值。输入字段定义为一个不包括空白符的字符串,其边界定义为到下一个空白符或到指定的字段宽度。这表明scanf函数将越过行边界进行读取,因为换行符也是空白符。(空白符包括空格符、横向制表符、换行符、回车符、纵向制表符以及换页符)。
转换字符指定对输入字段的解释。对应的参数必须是指针,这也是C语言通过值调用语义所要求的。
字符 | 输入数据;参数类型 |
---|---|
d | 十进制整数;int*类型 |
i | 整数;int*类型,可以是八进制(以0开头)或十六进制(以0x或0X开头) |
o | 八进制整数;(可以以0开头,也可以不以0开头);int *类型 |
u | 无符号十进制整数 |
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章中的简单计算器程序,如下所示:
#include
main() /* 简单计算器程序 */
{
double sum, v;
sum = 0;
while(scanf("%lf", &v) == 1)
printf("\t%.2f\n", sum += v);
return 0;
}
假设我们要读取包含下列日期格式的输入行:
25 Dec 1988
相应的scanf语句可以这样编写:
int day, year;
char monthname[20];
scanf("%d %s %d", &day, monthname, &year);
scanf函数忽略格式字符串中的空格和制表符。此外,在读取输入值的时候,它将跳过空白符。如果要读取格式不固定的输入,最好每次读入一行,然后再用sscanf将合适的格式分离出来读入。
下面,我们编写一个访问文件的程序,且它访问的文件还没有连接到该程序。
问题在于,如何设计命名文件的读取过程呢?换句话说,如何将用户需要使用的文件外部名同读取数据的语句关联起来。方法其实很简单,在读写一个文件之前,必须通过库函数fopen打开该文件。fopen用类似于x.c或y.c这样的外部名与操作系统进行某些必要的链接和通信,并返回一个随后可以用于文件读写操作的指针。
该指针称为文件指针,它指向一个包含文件信息的结构,这些信息包括:缓冲区的为止、缓冲区中当前字符的为止、文件读或写的状态、是否出错或是否已经到达文件结尾等等。在程序中只需按照下列方式声明一个文件指针即可:
FILE *fp;
FILE *fopen(char *name, char *mode);
在本例中,fp是一个指向结构FILE的指针,并且,fopen函数返回一个指向结构FILE的指针。注意,FILE像int一样是一个类型名,而不是结构标记。它是通过typedef定义的。
在程序中,可以这样调用fopen函数:
fp = fopen(name, node);
fopen的第一个参数是一个字符串,它包含文件名。第二个参数是访问模式,也是一个字符串,用于指定文件的使用方式。允许的模式包括:读(“r”)、写(“w”)以及追加(“a”)。某些系统还区分文本文件和二进制文件,对后者的访问需要在模式字符串中增加字符“b”。
如果打开一个不存在的文件用于写或追加,该文件将被创卷。当以写方式打开一个已存在的文件时,该文件原来的内容将被覆盖。但是,如果以追加方式打开一个文件,则该文件原来的内容将保留不变。读一个不存在的文件会导致错误,其它一些操作也可能导致错误。如果发生错误,fopen则返回NULL。
文件被打开后,就需要考虑采用哪种方法对文件进行读写。有多种方法可供考虑,其中,getc和putc函数最为简单。getc从文件中返回下一个字符,它需要知道文件指针,以确定对哪个文件执行操作:
int getc(FILE *fp);
getc函数返回fp指向的输入流中的下一个字符。如果到达文件尾或出现错误,则该函数将返回EOF。
putc是一个输出函数,如下所示:
int putc(int c, FILE *fp);
该函数将字符c写入到fp指向的文件中,并返回写入的字符。如果发生错误,返回EOF。
启动一个C语言程序时,操作系统环境负责打开3个文件。并将这3个文件的指针提供给该程序。这3个文件分别是标准输入、标准输出和标准错误。相应的文件指针分别为stdin、stdout、stderr,它们在
getchar和putchar函数可以通过getc、putc、stdin及stdout定义如下:
#define getchar() getc(stdin)
#define putchar(c) put((c), stdout)
对于文件的格式化输入或输出,可以使用函数fscanf和fprintf。它们与scanf和printf函数的区别仅仅在于它们的第一个参数是一个指向所要读写的文件的指针,第二个参数是格式串。
在cat程序中,如果因为某种原因而造成其中的一个文件无法访问,相应的诊断信息要在该连接的输出的末尾才能打印出来。当输出到屏幕时,这种处理方法尚可以接受,但如果输出到一个文件或通过管道输出到另一个程序时,就无法接受了。
为了更好的处理这种情况,另一个输出流以与stdin和stdout相同的方式分派给程序,即stderr。即使对标准输出进行了重定义,写到stderr中的输出通常也会显示在屏幕上。
程序在发生错误时可以调用函数exit,当该函数被调用时,它将终止调用程序的指向。任何调用该程序的进程都可以获取exit的参数值,因此,可通过另一个将该程序作为子进程的程序来测试该程序的执行是否成功。按照惯例,返回值0表示一切正常,而非0返回值则表示出现了异常情况。exit为每个已打开的输出文件调用调用fclose函数,以将缓冲区中所有输出写到相应文件中。
在主程序main中,语句return expr等价于exit(expr)。但是,使用函数exit有一个优点,它可以从其它函数中调用,并且可以用类似于查找程序查找这些调用。
标准库提供了一个输入函数fgets,它和前面几章用到的getline类似。
char *fgets(char *line, int maxline, FILE *fp);
fgets函数从fp指向的文件中读取下一个输入行。它最多可以读取maxline-1个字符。
输出函数fputs将一个字符串写入到一个文件中:
int fputs(char *line, FILE *fp);
库函数gets和puts的功能与fget和fputs函数类似,但它们是对stdin和stdout进行操作。有一点我们需要注销,gets函数在读取字符串时将删除结尾的换行符,而puts函数在写入字符串时将在结尾添加一个换行符。