5 库函数
每个有用的C程序都会用到库函数,因为没有办法把输入和输出内建到语言中去。在这一节中,我们将会看到一些广泛使用的库函数在某种情况下会出现的一些非预期行为。
5.1 getc()返回整数
考虑下面的程序:
#include <stdio.h>
main() { char c;
while((c = getchar()) != EOF) putchar(c); }
这段程序看起来好像要讲标准输入复制到标准输出。实际上,它并不完全会做这些。
原因是c被声明为字符而不是整数。这意味着它将不能接收可能出现的所有字符包括EOF。
因此这里有两种可能性。有时一些合法的输入字符会导致c携带和EOF相同的值,有时又会使c无法存放EOF值。在前一种情况下,程序会在文件的中间停止复制。在后一种情况下,程序会陷入一个无限循环。
实际上,还存在着第三种可能:程序会偶然地正确工作。C语言参考手册严格地定义了表达式
((c = getchar()) != EOF)
的结果。其6.1节中声明:
当一个较长的整数被转换为一个较短的整数或一个char时,它会被截去左侧;超出的位被简单地丢弃。
7.14节声明:
存在着很多赋值运算符,它们都是从右至左结合的。它们都需要一个左值作为左侧的操作数,而赋值表达式的类型就是其左侧的操作数的类型。其值就是已经付过值的左操作数的值。
这两个条款的组合效果就是必须通过丢弃getchar()的结果的高位,将其截短为字符,之后这个被截短的值再与EOF进行比较。作为这个比较的一部分,c必须被扩展为一个整数,或者采取将左侧的位用0填充,或者适当地采取符号扩展。
然而,一些编译器并没有正确地实现这个表达式。它们确实将getchar()的值的低几位赋给c。但在c和EOF的比较中,它们却使用了getchar()的值!这样做的编译器会使这个事例程序看起来能够“正确地”工作。
5.2 缓冲输出和内存分配
当一个程序产生输出时,能够立即看到它有多重要?这取决于程序。
例如,终端上显示输出并要求人们坐在终端前面回答一个问题,人们能够看到输出以知道该输入什么就显得至关重要了。另一方面,如果输出到一个文件中,并最终被发送到一个行式打印机,只有所有的输出最终能够到达那里是重要的。
立即安排输出的显示通常比将其暂时保存在一大块一起输出要昂贵得多。因此,C实现通常允许程序员控制产生多少输出后在实际地写出它们。
这个控制通常约定为一个称为setbuf()的库函数。如果buf是一个具有适当大小的字符数组,则
setbuf(stdout, buf);
将告诉I/O库写入到stdout中的输出要以buf作为一个输出缓冲,并且等到buf满了或程序员直接调用fflush()再实际写出。缓冲区的合适的大小在<stdio.h>中定义为BUFSIZ。
因此,下面的程序解释了通过使用setbuf()来讲标准输入复制到标准输出:
#include <stdio.h>
main() { int c;
char buf[BUFSIZ]; setbuf(stdout, buf);
while((c = getchar()) != EOF) putchar(c); }
不幸的是,这个程序是错误的,因为一个细微的原因。
要知道毛病出在哪,我们需要知道缓冲区最后一次刷新是在什么时候。答案;主程序完成之后,作为库在将控制交回到操作系统之前所执行的清理的一部分。在这一时刻,缓冲区已经被释放了!
有两种方法可以避免这一问题。
首先,是用静态缓冲区,或者将其显式地声明为静态:
static char buf[BUFSIZ];
或者将整个声明移到主函数之外。
另一种可能的方法是动态地分配缓冲区并且从不释放它:
char *malloc(); setbuf(stdout, malloc(BUFSIZ));
注意在后一种情况中,不必检查malloc()的返回值,因为如果它失败了,会返回一个空指针。而setbuf()可以接受一个空指针作为其第二个参数,这将使得stdout变成非缓冲的。这会运行得很慢,但它是可以运行的。
|