【例20.5】为什么下面程序编译正常却进入死循环?
#include <stdio.h>void main(){ int c; char buf[BUFSIZ]; setbuf(stdout, buf); while((c=getchar())!=EOF) putchar(c);}
【解答】一般使用的程序输出方式是即时处理方式,这种方式往往会造成较高的系统负担。另外一种方式是先暂存起来,等存到一定数量再一次写入。对于这种缓存的方式,C语言实现通常都允许程序员进行实际地写操作之前控制产生的输出数据量。这种控制能力一般是通过库函数setbuf实现的。setbuf的原型如下。
void setbuf(FILE *steam, char *buf);
功能:把缓冲区与流相联。如果buf是一个大小适当的字符数组,语句
setbuf(stdout,buf);
将通知输入/输出库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf缓冲区被填满,缓冲区中的内容才实际写入到stdout中。缓冲区的大小由系统头文件<stdio.h>中的BUFSIZ定义。
这个程序是想把标准输入的内容复制到标准输出中,程序错在对库函数setbuf的调用上。程序虽然通知输入/输出库将所有字符的标准输出首先缓存在buf中,但这个buf是普通数组,随时都有被释放的可能。作为缓冲区的buf数组最后一次被清空是在什么时候呢?答案是在main函数结束之后,程序交回控制权给操作系统之前,C运行时库将它清除。但是,在进行该项清理工作之前,buf字符数组已经被释放了。
要避免这种类型的错误有两种办法,一种是调整数组的生存期,另一种是使用动态内存。
1.使用静态数组
可以直接显式声明buf为静态数组,即
static char buf[BUFSIZ];
为了便于演示,将EOF重新定义。修改后的程序如下。
#include <stdio.h>#undef EOF#define EOF /'0/'void main(){ int c; static char buf[BUFSIZ]; setbuf(stdout, buf); while((c=getchar())!=EOF) putchar(c); printf(/"n/");}
运行示范如下。
We are here!Where are you?0We are here!Where are you?
程序在接收到“0”时,停止向缓冲区写数据。main函数结束之后,静态数组的数据仍然存在,这时将buf的数据一次输出到屏幕上。
2.使用全局数组
也可以将buf声明为全局数组,运行效果一样。
#include <stdio.h>#undef EOF#define EOF /'0/'char buf[BUFSIZ];void main(){ int c; setbuf(stdout, buf); while((c=getchar())!=EOF) putchar(c); printf(/"n/");}
3.使用动态内存
可以使用动态分配缓冲区。由于缓冲区是动态分配的,所以main函数结束时并不会释放该缓冲区,这样C运行时库进行清理工作时就不会发生缓冲区已释放的情况。
#include <stdio.h>#include <stdlib.h>#undef EOF#define EOF /'0/'void main(){ int c; char *p; p=(char*)(malloc(BUFSIZ*sizeof(char))); setbuf(stdout, p); while((c=getchar())!=EOF) putchar(c); printf(/"n/");}
运行结果相同,不再赘述。这里其实并不需要检查malloc函数调用是否成功。如果malloc函数调用失败,将返回一个null指针。setbuf函数的第二个参数取值可以为null,此时标准输出不需要进行缓冲。这种情况下,程序仍然能够工作,只不过速度较慢而已。
4.使用缓冲区的例子
要注意数据写入缓冲区的格式和使用缓冲区的方式。仔细观察下面程序中不同语句各自的效果,以便更好地使用缓冲区。
【例20.6】使用缓冲区里的数据实例。
#include <stdio.h>char buf[BUFSIZ];int main(void){ setbuf(stdout,buf); puts(/"Where are you?/"); //(1) puts(buf); //(2) return 0;}
程序运行结果如下。
Where are you?Where are you?Press any key to continue
这里有意把“Press any key to continue”信息给出,为的是说明对换行的处理。
puts函数会将字符串送入stdout中,并在其后自动添加一个/'n/'。语句(1)是把字符串写入buf。执行该语句使得stdout和buf中的数据为“Where are you?n”。
语句(2)是把buf的数据再写入buf,即puts会将buf中的字符串送入stdout中,并在其后再添加一个“/'n/'”。此时,stdout和buf中的数据为“Where are you?nWhere are you?nn”。主程序结束时,在屏幕上显示的结果除了相同的一句话之外,还有一个空行。
5.putch和putchar的异同
【例20.7】如上例所说,puts函数会将字符串送入stdout中,并在其后自动添加一个/'n/'。按理好像这个程序应该分两行输出,请解释为何得到这种奇怪的输出结果。
#include <stdio.h>#include <conio.h>char buf[BUFSIZ];int main(void){ int i=0; setbuf(stdout,buf); puts(/"Where are you?/"); while (buf[i]!=/'n/') putch(buf[i++]); return 0;}
运行结果如下。
Where are you? Where are you?Press any key to continue
【解答】如果buf里有数据,putch函数是自成系统,将第1个字符从头部插入,以后的字符则从第1次的位置依次插入。也就是将其他函数写入的字符每次整体右移一个位置,然后插入一个字符。因为while语句结束循环的条件是“/'n/'”,所以没有写入回车换行,而原来puts函数写入一个回车换行符,所以得到这种输出。
关键是循环写入的内容是在原来语句之前。这两句的内容相同,误以为后写入的一定是在原来语句的后面,肯定要分两行输出,实际情况并不是想象的那样,请看下面的例子。
【例20.8】分析这个程序的输出内容。
#include <stdio.h>#include <conio.h>char buf[BUFSIZ];int main(void){ int i=0; setbuf(stdout,buf); putchar(/'M/'); putchar(/'L/'); while (buf[i]!=/'/0/') putch(buf[i++]); putch(/'A/'); putch(/'B/'); putch(/'C/'); puts(/"Where are you?/"); putchar(/'D/'); putch(/'E/'); putch(/'F/'); puts(/"Where are you?/"); return 0;}
【解答】putchar函数是按先后顺序写入ML,putch则将M和L字符分别转化为O和N并复制到最前面,缓冲区内容为ONML。
putch在ON之后写入ABC,而puts则在ML之后写入“Where are you”及回车换行符。所以putchar在下一行写入D。但putch则继续把EF插入到上一行的C之后,M之前。
puts的写入仍然是接在D之后,最终输出如下。
ONABCEFMLWhere are you?DWhere are you?
【例20.9】分析如下程序的输出内容。
#include <stdio.h>#include <conio.h>char buf[BUFSIZ];int main(void){ int i=0; char st=/"She/"; setbuf(stdout,buf); puts(/" Where are you?/"); putchar(/'M/'); while (st[i]!=/'/0/') putch(st[i++]); putch(/' /'); putch(/'b/'); putch(/'c/'); puts(/" Where are you?/"); putchar(/'d/'); putch(/'e/'); putch(/'f/'); puts(/" Where are you?/"); return 0;}
【解答】M是第2行的第1个字母,d是第3行的第1个字母,注意空格的作用,很容易写出如下输出结果。
She bcef Where are you?M Where are you?d Where are you?
6.fflush的作用
fflush函数用来清除文件缓冲区。函数原型为
int fflush(FILE *stream)
文件以写方式打开时,直接调用fflush将导致输出缓冲区的内容被实际地写入该文件。通常是为了确保不影响后面的数据读取,如在读完一个字符串后紧接着又要读取一个字符串时,需要清空输入缓冲区。
【例20.10】分析如下程序的输出内容。
#include <stdio.h>#include <conio.h>char buf[BUFSIZ];int main(void){ int i=0; char st=/"She/"; setbuf(stdout,buf); puts(/" Where are you?/"); putchar(/'M/'); while (st[i]!=/'/0/') putch(st[i++]); putch(/' /'); putch(/'b/'); putch(/'c/'); puts(/" Where are you?/"); putchar(/'d/'); putch(/'e/'); putch(/'f/'); puts(/" Where are you?/"); buf[0] = /'A/'; return 0;}
【解答】这个程序只是取消了puts语句中的空格,很容易误以为是如下输出。
Ahe bcefWhere are you?MWhere are you?dWhere are you?
实际上,buf[0]是指针,它指向的是puts最后写入的字符之后的位置,即W的位置,所以输出为
She bcefAhere are you?M Where are you?d Where are you?
如果在这条语句之前使用fflush语句,例如:
fflush(stdout);buf[0] = /'A/';
则不影响原来的输出。
【例20.11】在上例的程序中使用fflush函数,保证前两次写入的内容最先输出,并分析A出现的位置。
【解答】应该在while循环之前清除缓冲区。修改的程序为
#include <stdio.h>#include <conio.h>char buf[BUFSIZ];int main(void){ int i=0; char st=/"She/"; setbuf(stdout,buf); puts(/" Where are you?/"); putchar(/'M/'); fflush(stdout); while (st[i]!=/'/0/') putch(st[i++]); putch(/' /'); putch(/'b/'); putch(/'c/'); puts(/" Where are you?/"); putchar(/'d/'); putch(/'e/'); putch(/'f/'); puts(/" Where are you?/"); buf[0] = /'A/'; return 0;}
因为“S”应接在“M”之后,putch写入的字符“e”和“f”继续遵守插入规则,插在字符“c”之后。“f”之后是“W”,所以将“W”改成“A”,输出为
Where are you?MShe bcefAhere are you?d Where are you?
【例20.12】进一步演示使用fflush函数的例子。
在程序中使用序号标记可能增加的fflush函数位置及实验语句。
#include <stdio.h>#include <conio.h>char buf[BUFSIZ];int main(void){ int i=0; char st=/"She/"; setbuf(stdout,buf); puts(/"Where are you?/"); putchar(/'M/'); fflush(stdout); //(1) while (st[i]!=/'/0/') putch(st[i++]); //fflush(stdout); //(2) putch(/' /'); putch(/'b/'); putch(/'c/'); // fflush(stdout); //(3) puts(/"Where are you?/"); fflush(stdout); //(4) putchar(/'d/'); putch(/'e/'); putch(/'f/'); // fflush(stdout); //(5) puts(/"Where are you?/"); //fflush(stdout); //(6) //putch(/'i/'); //(7) //putchar(/'g/'); //(8) buf[0] = /'A/'; //(9) return 0;}
putch函数是很特殊的,不是随便使用fflush函数就可以分割输出信息的组成的。例如,(2)和(3)处的fflush函数均没有必要。一般是在插入其他输入格式之后使用fflush,以产生新的排列。例如在使用putchar或puts函数之后,可以使用fflush函数。如使用(4)的语句,这时就另开新行,把原本在第1个位置的“d”右移,插入“ef”。所以buf[0]的内容为“d”,并且被修改为“A”。输出变为
Where are you?MShe bcWhere are you?efAWhere are you?
需要注意的是,这种修改只能是在putch写入的字符之后,并且其后有其他函数写入的字符。例如,将(5)加入,puts函数符合要求,所以是将“W”(字符d已经不属于这里的内容)修改为“A”。输出变为
Where are you?MShe bcWhere are you?efdAhere are you?
如果没有其他函数的调用,则不起作用。例如,增加语句(6),则修改不起作用,输出结果同上。如果增加语句(7),因为是putch函数,修改也不起作用,只是另开一个新行并输出字符i。如果再增加语句(8),这就满足条件,会用A修改g,这一行输出为iA。
7.fflush的妙用
【例20.13】下面的程序有错误,但不是仅仅希望改正错误,而是希望能简单有效的发现类似错误。这里演示了使用fflush函数的优点。
#include <stdio.h>int FindIt(const int,const int);int main( ){ int mean; static char buffer[BUFSIZ]; setbuf(stdout,buffer); printf(/"Find...n/"); fflush(stdout); mean=FindIt(128,0); printf(/"ENDn/"); printf(/"mean=%dn/",mean); return 0;}int FindIt(const int sum,const int num){ return sum/num;}
当初始为0时,屏幕显示“Find...”,说明程序执行了“fflush(stdout);”语句,错误在这条语句之后,由此很容易发现问题出现在函数调用。
如果没有这条语句,printf的信息是送往缓冲区,而不是显示在屏幕上。只有缓冲区写满或有新行输出时,才将信息显示在屏幕上。由此可见,信息“Find...”被送往缓冲区,缓冲区没有满,所以屏幕上不会出现这个信息。执行函数调用出错,会以为还没有执行这个printf语句,从而误以为错误在printf语句之前。使用fflush语句显式地刷新缓冲区,使错误容易定位。
一般刷新缓冲区的方式依赖于所写文件的类型。可以参考如下规则:
(1)如果stdout或stderr是写到屏幕,可以在写完一行时进行刷新。这常常作为预防性措施,如本例在函数调用前清缓冲区,以保证信息提示的作用。也可以在读stdin之后刷新,以保证后续的信息。
(2)如果stdout或stderr是写到屏幕,在缓冲区满时,需要刷新缓冲区。
(3)如果stdout或stderr是写到磁盘,则要等缓冲区满了之后才能刷新。
判别缓冲区是否满了,也是一件需要小心处理的事情。
注意:千万小心程序对系统的依赖性。