
與底層I/O相比,标準I/O包除了可移植以外還有兩個好處。
第一,标準I/O有許多專門的函數簡化了處理不同I/O的問題。例如,printf()把不同形式的資料轉換成與終端相适應的字元串輸出。
第二,輸入和輸出都是緩沖的。也就是說,一次轉移一大塊資訊而不是一位元組資訊(通常至少512位元組)。例如,當程式讀取檔案時,一塊資料被拷貝到緩沖區(一塊中介存儲區域)。這種緩沖極大地提高了資料傳輸速率。程式可以檢查緩沖區中的位元組。緩沖在背景處理,是以讓人有逐字元通路的錯覺(如果使用底層I/O,要自己完成大部分工作)。程式count.c示範了如何用标準I/O讀取檔案和統計檔案中的字元數。
/* count.c -- using standard I/O */#include #include // exit() prototypeint main(int argc, char *argv[]){ int ch; // place to store each character as read FILE *fp; // "file pointer" unsigned long count = 0; if (argc != 2) { printf("Usage: %s filenamen", argv[0]); exit(EXIT_FAILURE); } if ((fp = fopen(argv[1], "r")) == NULL) { printf("Can't open %sn", argv[1]); exit(EXIT_FAILURE); } while ((ch = getc(fp)) != EOF) { putc(ch,stdout); // same as putchar(ch); count++; } fclose(fp); printf("File %s has %lu charactersn", argv[1], count); return 0;}
1 檢查指令行參數
首先,程式count.c中的程式檢查argc的值,檢視是否有指令行參數。如果沒有,程式将列印一條消息并退出程式。字元串argv[0]是該程式的名稱。顯式使用argv[0]而不是程式名,錯誤消息的描述會随可執行檔案名的改變而自動改變。這一特性在像UNIX這種允許單個檔案具有多個檔案名的環境中也很友善。但是,一些作業系統可能不識别argv[0],是以這種用法并非完全可移植。
exit()函數關閉所有打開的檔案并結束程式。exit()的參數被傳遞給一些作業系統,包括UNIX、Linux、Windows和MS-DOS,以供其他程式使用。通常的慣例是:正常結束的程式傳遞0,異常結束的程式傳遞非零值。不同的退出值可用于區分程式失敗的不同原因,這也是UNIX和DOS程式設計的通常做法。但是,并不是所有的作業系統都能識别相同範圍内的傳回值。是以,C标準規定了一個最小的限制範圍。尤其是,标準要求0或宏EXIT_SUCCESS用于表明成功結束程式,宏EXIT_FAILURE用于表明結束程式失敗。這些宏和exit()原型都位于stdlib.h頭檔案中。
根據ANSI C的規定,在最初調用的main()中使用return與調用exit()的效果相同。是以,在main(),下面的語句:
return 0;
和下面這條語句的作用相同:
exit(0);
但是要注意,我們說的是“最初的調用”。如果main()在一個遞歸程式中,exit()仍然會終止程式,但是return隻會把控制權交給上一級遞歸,直至最初的一級。然後return結束程式。return和exit()的另一個差別是,即使在其他函數中(除main()以外)調用exit()也能結束整個程式。
2 fopen()函數
繼續分析程式清單13.1,該程式使用fopen()函數打開檔案。該函數聲明在stdio.h中。它的第1個參數是待打開檔案的名稱,更确切地說是一個包含該檔案名的字元串位址。第2個參數是一個字元串,指定待打開檔案的模式。表13.1列出了C庫提供的一些模式。
Mode Strings for fopen()
像UNIX和Linux這樣隻有一種檔案類型的系統,帶b字母的模式和不帶b字母的模式相同。
新的C11新增了帶x字母的寫模式,與以前的寫模式相比具有更多特性。
第一,如果以傳統的一種寫模式打開一個現有檔案,fopen()會把該檔案的長度截為0,這樣就丢失了該檔案的内容。但是使用帶x字母的寫模式,即使fopen()操作失敗,原檔案的内容也不會被删除。
第二,如果環境允許,x模式的獨占特性使得其他程式或線程無法通路正在被打開的檔案。
程式成功打開檔案後,fopen()将傳回檔案指針(file pointer),其他I/O函數可以使用這個指針指定該檔案。檔案指針(該例中是fp)的類型是指向FILE的指針,FILE是一個定義在stdio.h中的派生類型。檔案指針fp并不指向實際的檔案,它指向一個包含檔案資訊的資料對象,其中包含操作檔案的I/O函數所用的緩沖區資訊。因為标準庫中的I/O函數使用緩沖區,是以它們不僅要知道緩沖區的位置,還要知道緩沖區被填充的程度以及操作哪一個檔案。标準I/O函數根據這些資訊在必要時決定再次填充或清空緩沖區。fp指向的資料對象包含了這些資訊。
3 getc()和putc()函數
getc()和putc()函數與getchar()和putchar()函數類似。所不同的是,要告訴getc()和putc()函數使用哪一個檔案。下面這條語句的意思是“從标準輸入中擷取一個字元”:
ch = getchar();
然而,下面這條語句的意思是“從fp指定的檔案中擷取一個字元”:
ch = getc(fp);
與此類似,下面語句的意思是“把字元ch放入FILE指針fpout指定的檔案中”:
putc(ch, fpout);
在putc()函數的參數清單中,第1個參數是待寫入的字元,第2個參數是檔案指針。
程式count.c把stdout作為putc()的第2個參數。stdout作為與标準輸出相關聯的檔案指針,定義在stdio.h中,是以putc(ch, stdout)與putchar(ch)的作用相同。實際上,putchar()函數一般通過putc()來定義。與此類似,getchar()也通過使用标準輸入的getc()來定義。
為何該示例不用putchar()而要用putc()?原因之一是為了介紹putc()函數;原因之二是,把stdout替換成别的參數,很容易将這段程式改寫成檔案輸出。
4 檔案結尾
從檔案中讀取資料的程式在讀到檔案結尾時要停止。如何告訴程式已經讀到檔案結尾?如果getc()函數在讀取一個字元時發現是檔案結尾,它将傳回一個特殊值EOF。是以C程式隻有在讀到超過檔案末尾時才會發現檔案的結尾(一些其他語言用一個特殊的函數在讀取之前測試檔案結尾,C語言不同)。
為了避免讀到空檔案,應該使用入口條件循環(不是do-while循環)進行檔案輸入。鑒于getc()(和其他C輸入函數)的設計,程式應該在進入循環體之前先嘗試讀取。如下面設計所示:
// good design #1int ch; // int to hold EOFFILE * fp;fp = fopen("wacky.txt", "r");ch = getc(fp); // get initial inputwhile (ch != EOF){ putchar(ch); // process input ch = getc(fp); // get next input}
可以簡化為:
// good design #2int ch;FILE * fp;fp = fopen("wacky.txt", "r");while (( ch = getc(fp)) != EOF){ putchar(ch); // process input}
由于ch = getc(fp)是while測試條件的一部分,是以程式在進入循環體之前就讀取了檔案。不要設計成下面這樣:
// bad design (two problems)int ch;FILE * fp;fp = fopen("wacky.txt", "r");while (ch != EOF) // ch undetermined value first use{ ch = getc(fp); // get input putchar(ch); // process input}
第1個問題是,ch首次與EOF比較時,其值尚未确定。第2個問題是,如果getc()傳回EOF,該循環會把EOF作為一個有效字元處理。這些問題都可以解決。例如,把ch初始化為一個啞值(dummy value),再把一個if語句加入到循環中。但是,何必多此一舉,直接使用上面的設計範例即可。
其他輸入函數也會用到這種處理方案,它們在讀到檔案結尾時也會傳回一個錯誤信号(EOF或NULL指針)。
5 fclose()函數
fclose(fp)函數關閉fp指定的檔案,必要時重新整理緩沖區。對于較正式的程式,應該檢查是否成功關閉檔案。如果成功關閉,fclose()函數傳回0,否則傳回EOF:
if (fclose(fp) != 0) printf("Error in closing file %sn", argv[1]);
如果磁盤已滿、移動硬碟被移除或出現I/O錯誤,都會導緻調用fclose()函數失敗。
6 指向标準檔案的指針
stdio.h頭檔案把3個檔案指針與3個标準檔案相關聯,C程式會自動打開這3個标準檔案。如表13.2所示:
這些檔案指針都是指向FILE的指針,是以它們可用作标準I/O函數的參數,如fclose(fp)中的fp。接下來,我們用一個程式示例建立一個新檔案,并寫入内容。