【例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 ? 0 We 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是寫到磁盤,則要等緩衝區滿了之後才能刷新。
判別緩衝區是否滿了,也是一件需要小心處理的事情。
注意:千萬小心程序對系統的依賴性。