第七章主要介绍了有关输入输出的库函数。在提到库函数时作者说到,"Programs that confine their system interactions to facilities provided by the standard library can be moved from one system to another without change." 由此可见,程序中和操作系统打交道的地方应该尽可能地使用标准库函数,这样可以增加可移植性。
第一节 标准输入和输出
这节介绍了标准输入输出的模型,就是把输入输出的内容想象成字符流。这样的字符流由一行一行的字符组成,每一行以'/n'结束。另外,作者还介绍了重定向(redirection)和管道(pipe)机制,想必用过DOS和Unix/Linux的人都很熟悉。
最后,作者提到了很有意思的一件事,就是像getchar, putchar和tolower这样的库函数都是宏,这样能避免函数调用的开销。具体是怎么实现的,要等到第八章再介绍。
第二节 格式化输出-printf
本节详细介绍了printf和sprintf。如果以后写程序需要严格的格式化输出,可以参考本节。作者在最后给我们提了个醒:
printf(s); /* FAILS if s contains % */
printf("%s", s); /* SAFE */
大家一般输出一个字符串就用前者,但如果这个字符串包含%就会失败。后者才是安全的写法。
第三节 变长参数列表
本节作者介绍了像printf这种参数数量不定的函数是怎样实现的,重点讲了函数声明和怎样取到未命名的参数。首先来看这种函数是怎么声明的:
int printf(char *fmt, ...)
省略号...意味着参数的数量和类型可变,它只能用在参数列表的末尾。
要取到未命名的参数,首先要把头文件<stdarg.h>包含进来。这个头文件定义的va_list, va_start, va_arg和va_end配合使用就可以完成这件工作。具体怎么用可以参考作者举的例子:
#include <stdarg.h>
/* minprintf: minimal printf with variable argument list */
void minprintf(char *fmt, ...)
{
va_list ap; /* points to each unnamed arg in turn */
char *p, *sval;
int ival;
double dval;
va_start(ap, fmt); /* make ap point to 1st unnamed arg */
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':
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); /* clean up when done */
}
第四节 格式化输入-scanf
本节介绍了printf和sprintf的counterpart:scanf和sscanf。sscanf从一个指定的字符串中接受输入。这两对函数很多性质都很相似,只不过一个是输出函数,另一个是输入函数。另外,scanf和sscanf中接受输入的参数必须是指针。
通常我们用scanf只读入一个值,其实也可以读入多个值。比如想从输入流中读到25 Dec 1988这样的信息,就可以写这样的语句:
int day, year;
char monthname[20];
scanf("%d %s %d", &day, monthname, &year);
一次读入年月日的信息。另外,如果读取的格式不固定,可以一次读入一行,然后用sscanf从这行信息中匹配想要的格式。
第五节 文件访问
本节作者介绍了用于文件访问的库函数和相关用法,包括fopen, fclose, getc和putc。其中getc和putc与getchar和putchar相似,只不过前者用于文件读写,后者用于标准输入输出。它们的关系可从以下两式看出:
#define getchar() getc(stdin)
#define putchar(c) putc((c), stdout)
这正是getchar和putchar的实现。其中stdin和stdout是系统维护的两个文件指针,另一个类似的文件指针是stderr。通常情况下stdin和键盘相连,而stdout和stderr和显示器相连。
如果想进行格式化输入输出,作者还介绍了两个与scanf和printf类似的函数:
int fscanf(FILE *fp, char *format, ...)
int fprintf(FILE *fp, char *format, ...)
不同处只是它们对文件输入输出,第一个参数指定了文件指针。
第六节 错误处理-stderr和exit
这一节的内容很有价值,特别是当你做一个严肃的项目时。大学时代学C语言时,这方面的内容根本不考,所以一直是我的薄弱环节。这次看书搞清了之前很多糊涂的东西。
之前我知道stdout和stderr都是向显示器输出的,但不明白它们除了在概念上,还有哪些实际的区别。作者在这节就告诉我们了,区别在这里:写到stdout的输出有可能被重定向(另一个文件或者管道),即使处理过程中出现了错误,错误信息也会被重定向;而写到stderr的输出即使标准输出被重定向了,也会显示在屏幕上。
关于exit,我也不知道它和return的区别。看了作者的讲解后明白了,在程序中只要一调用exit,程序立马停止执行,而return只是函数返回。所以在main函数中exit和return是等价的,但在别的函数中完全不同。exit的优势就在于不管是谁调用,只要调用就退出程序执行。
第七节 行输入输出
本节作者介绍了两个针对文件的行输入输出库函数:fgets和fputs。它们与库函数gets和puts类似,只不过后者在stdin和stdout上读写。这一节没有太多值得一书的地方,但通过这么多标准输入输出函数都有相应的文件操纵函数可以看出,其实标准输入输出函数只是文件输入输出函数的特殊情况——它们在两个特殊的文件指针stdin和stdout上操作。
第八节 各种其它函数
这一节罗列了很多有用的库函数,可以作为很好的参考资料。下面列举几个我觉得重要的点:
- int ungetc(int c, FILE *fp):当你不小心从文件中多读了一个字符时,没关系,用这个函数给退回去。其实大部分情况下多读都是故意的,程序的需要,不过要记住一个文件只能退回一个字符。
- system(char *s):调用这个函数会让系统执行字符串中的命令。至于命令可以是什么,和具体的操作系统密切相关。
- malloc和calloc:前些天还在论坛上看到有人问这两个函数的区别,我觉得唯一明显的区别在于calloc有一个把申请到的内存初始化成0的过程。至于用法就是仁者见仁了,我感觉calloc从它接受的参数上讲更适合给数组分配空间,而malloc则是通吃,适应面更广一点。
- rand()生成一个0到RAND_MAX之间的随机整数,而srand(unsigned)给它设置种子(seed)。如果想生成一个0到1之间的随机浮点数,可以这样定义:#define frand() ((double) rand() / (RAND_MAX+1.0))