天天看點

《UNIXLinux程式設計教程》一2.4 讀和寫流

一旦打開了一個流,就能對它進行讀寫,讀寫可以按無格式方式也可以按有格式方式進行。這一節介紹無格式i/o函數,下一節介紹有格式i/o函數。

有以下三種類型的無格式i/o函數可供選擇:

1)字元i/o函數。這種函數每次讀或寫一個字元。

2)行i/o函數。這種函數每次讀寫一行,每一行以換行符結束。

3)塊i/o函數。這種函數支援成塊i/o,它們每次讀寫若幹個對象,每個對象的大小是指定的。塊i/o有時也稱為二進制i/o、對象i/o或結構i/o。

如下三個字元輸入函數每次讀入一個字元:

fgetc()從流stream中按unsigned char類型讀取下一字元,并将它強制為int類型傳回,若遇到檔案結束或者出現錯誤,則傳回eof。

getc()的功能與fgetc()相同,不同的是允許将getc()作為宏來實作,而fgetc()則必須為函數。getc()常常是被高度優化了的,是以是最常用的讀單個字元的函數。

getchar()等價于getc(stdin)。

這三個函數之是以将流中的字元視為unsigned char,是為了保證在其高位被設定時函數的傳回值不會為負值。要求傳回值為int類型是為了能夠傳回所有可表示的字元,不僅是ascii字元集中的字元,也包括寬字元集中的字元,還包括遇到檔案結束和錯誤時的訓示符eof。這意味着我們不能将fgetc()的傳回值存儲在字元類型的變量中。

與三個字元輸入函數對應有如下三個字元輸出函數:

fputc()将字元c轉換為unsigned char 類型,然後寫至流stream并傳回字元c。

putc()與fputc()相同,但它常常是用較快的宏來實作的。putc()是用于輸出單個字元最合适的函數。類似于輸入函數,putchar()等價于putc(stdin)。

例2-2 程式2-2給出的函數y_or_n_ques()在标準輸入輸出終端提出參數指定的問題,并讀使用者的回答。回答為'y'則傳回真值,回答為'n'則傳回假值。用getc()和putc()或者getchar()和putchar()替換其中的fgetc()和fputc(),它也照樣工作。其中函數fputs()輸出一行字元串,下一節将詳細介紹它。tolower()将輸入字元轉換為小寫字元以便随後對它進行檢測。

《UNIXLinux程式設計教程》一2.4 讀和寫流

注意這個函數讀單字元指令的處理,它在用fgetc()讀取一個字元之後還繼續調用fgetc()抛棄同一行的其他字元。因為預設情況下終端輸入隻有在鍵入換行符之後才有效,如果不抛棄這個換行符,它将遺留在輸入流中使得下一次讀指令字元時會讀不到實際鍵入的正确指令。第9章低級終端i/o中我們将看到對這種問題更為精緻的處理方法。

有許多應用是按行來處理資料的,例如編譯程式通常每次讀入一行源程式來進行詞法掃描。标準c庫中有兩個函數用于每次讀入一行:

fgets()從stream指定的流中連續讀字元直至讀到換行符或者讀夠count–1個字元(包括換行符)為止,讀入的這一行字元(包括最後的換行符)存儲在參數s指定的字元串中,并且在其末尾添加一個空字元(0)作為結束。參數count指明字元串s的大小。

如果要讀入的這一行(包括結尾的換行符)長度大于count–1,則隻有部分字元被讀入,而字元串s總是以空字元結尾,下一次調用fgets()将傳回此行剩餘的部分。

gets()函數從标準輸入流stdin中讀入完整的一行至參數s指定的字元串中。它删除換行符并在字元串s的末尾添加一個空字元作為結束。

注意gets()與fgets()的不同。fgets()不能保證一定讀入完整的一行,是以為了判别是否已經讀入一行,它需要保留換行符,而gets()則無此需要,故它删除換行符。另外,由于gets()不要求提供字元串s的空間大小,這導緻gets()成了危險的函數:它沒有為字元串s的溢出提供保護!當要讀入的行長度超過字元串s所能容納的大小時,超出的部分将越過s提供的空間而覆寫其他的資料或程式。是以最好不要使用gets()。

如果調用這兩個函數時檔案已處在檔案尾,則字元串s不發生改變且傳回值都是eof。這兩個函數遇到錯誤時也傳回eof,正常情況下傳回指向字元串s的指針。

例2-3 程式2-3說明了fgets()和gets()的不同。該程式首先提問使用fgets()還是gets()讀輸入行,并根據使用者的選擇使用不同的函數。由于我們故意指定緩沖區的大小隻有8位元組,是以,當輸入行的長度大于8時,使用fgets()每次隻讀8個字元,并且為了讀入一完整的行需要循環讀直至讀到換行符為止。而用gets()讀一行則不需要循環,但卻可能導緻讀入的資料溢出緩沖區。

《UNIXLinux程式設計教程》一2.4 讀和寫流

為了展示gets()導緻數組buf溢出的情況,我們利用結構類型将兩個數組合在一起。當輸入字元超過buf的大小(8個字元)時,fgets()至多讀8個字元,而gets()則會導緻資料溢出到成員others中。注意,這種結果與編譯器的存儲配置設定方法有關。我們這裡假定編譯器按結構成員書寫的順序配置設定存儲。如果編譯器按逆序配置設定,則應将others書寫在buf之前。作為練習,建議你用不同的選擇和長度不同的輸入運作這個程式來檢視運作結果。注意,這個程式要與程式2-2一起連接配接。

gets()可以讀入完整的一行,但存在溢出的危險;fgets()雖然保險,但當輸入資料中含有空字元(nul,即'0')時卻會遇到麻煩。因為fgets()不能保證每次都能完整地讀入一行,并且它自動地在讀入的字元串末尾添加空字元,這使得在分辨輸入行中原本就有的空字元時需要特别的動作:當在字元串s中讀到一個空字元時,必須判别其前面是否有換行符以及所在位置來确定它是原本就有的資料,還是作為字元串結束的空字元。

linux中的gnu c庫為此專門提供了另外一個每次讀一行的函數getline(),此外還擴充了一個更通用的類似函數getdelim(),該函數讀入一被界定的記錄,這種記錄定義為直至下一特定分隔符為止的所有内容。

getline()從流stream中讀入一行(包括換行符和一個終止空字元),并存儲于lineptr所指緩沖區中,緩沖區的大小由參數n給出。

在調用getline()之前一般先要調用malloc()(5.5.2節)配置設定大小為 n個位元組的緩沖區,并存放其位址于lineptr。如果這個緩沖區的大小足夠容納輸入行,getline()将此行置于緩沖區中。否則,getline()會自動擴大此緩沖區,然後将新緩沖區的位址回填至lineptr,新增加後的緩沖區大小存回至n。特别地,當lineptr為空指針,n為0時,getline()會自動配置設定初始緩沖區。

getline()調用成功的傳回值是讀入的字元數(包括換行符,但不包括終止空字元),這使我們能夠區分行中的空字元和作為終止符的空字元:終止空字元所在的位置一定等于getline()的傳回值。

函數getdelim()類似于getline(),不同的隻是作為行終止的分隔符不一定是換行符,而可以通過參數delimiter指定,getdelim()一直讀至遇到該字元或檔案結束為止。讀入的正文包括該分隔符和一個終止的空字元。

實際上,getline()是用getdelim()來實作的:

這兩個函數雖然是gnu 的擴充,但它們能從流中可靠地讀一行,特别是getdelim()對正文比對處理特别友善。是以,這裡專門介紹了它們。

每次輸出一行可由fputs()或puts()函數來完成。

fputs()輸出以空字元結尾的字元串s至流stream,但結尾的空字元不寫入,也不添加換行符,它隻輸出字元串中的字元。如果發生錯誤,該函數傳回eof,否則傳回一非負值。

puts()輸出以空字元結尾的字元串s至标準輸出流stdout并添加一個換行符,但字元串結尾的空字元不寫出。注意它與fputs()的不同:fputs()不添加換行符,而puts()則添加換行符。例如,下述三次fputs()的連續調用:

輸出的正文是“are you hungry?”後随一個換行符。這個換行符是字元串自身所帶的。反之,如果用puts()替代上述調用,則會輸出四行:每行一個單詞并且最後有一空行。

puts()是用于列印簡短消息最友善的函數。

程式在讀輸入的過程中,有時候會隻想檢視一下輸入流中的下一字元而并不想将它從輸入流中讀走,這稱為對輸入流的超前窺視,因為程式隻是對下次要讀入的字元提前看一眼。在流i/o的情況下,隻能通過首先從流中讀出字元,然後再将該字元退回至輸入流來實作超前窺視。

回退字元至流的函數是ungetc(),它是getc()的逆操作。

ungetc()将字元c退回至輸入流stream。于是下一次從stream的輸入将首先讀到字元c。若調用成功,該函數傳回回退的字元,否則傳回eof。

如果要回退的字元c是eof,ungetc()不做任何動作并傳回eof,這使得我們在使用getc()的傳回值調用ungetc()時無須對getc()的錯誤進行檢測。

大多數系統都隻支援回退一個字元,linux也如此。這意味着連續兩次調用ungetc()之間必須有一次讀入。如果在未調用getc()的情況下再次調用ungetc()将使前一個回退的字元丢失。

回退的字元不必是從流中最後一次讀出的字元,可以是任意字元。事實上并不需要在用ungetc()做回退之前從流中真正讀出任何字元!不過以這種方式程式設計是較奇怪的,通常ungetc()隻用來回退剛從同一個流讀出的字元。

回退字元并不是将字元送回檔案本身,而是送回流的内部緩沖中。是以,如果調用了一個檔案定位函數(如fseek()或rewind()),則将丢棄任何未重新被讀入的回退字元。

回退一個字元至一個正處在檔案尾的流将清除該流的檔案尾訓示器,因為它使該回退字元重新變為有效輸入字元。當讀入該字元之後再次讀才會遇到檔案尾。

例2-4 程式2-4說明了用getc()和ungetc()跳過空白字元的用法。當getc()到達一個非空白字元時,程式用ungetc()回退此字元,進而在下一次讀時能再次讀到它。

《UNIXLinux程式設計教程》一2.4 讀和寫流

塊i/o也稱為二進制i/o,它以固定大小的塊為機關而不是以字元或行為機關來讀寫資料。要讀寫的資料既可以是字元正文,也可以是二進制資料。函數fread()和fwrite()用于進行這種成塊的輸入輸出。

fread()從流stream中讀count個資料項,并存放至data所指的數組中,每個資料項的長度為size位元組,所讀的總位元組數為 count×size。

fwrite()從data所指的區域中寫出count個資料項至流stream,每個資料項的長度為size位元組,所寫出的總位元組數為 count×size。

fread()和fwrite()均傳回實際讀寫的資料項數(注意,不是位元組數)。若調用成功,傳回值等于count;若遇到檔案尾或錯誤,傳回值小于count或為eof;當出現錯誤時,設定errno指明錯誤原因;如果count或size為0,則不做任何動作并傳回0。

這兩個函數常在如下情形中使用:

1)讀寫一個二進制數組。例如,為了輸出一個浮點數組的第二至第五個元素,可以這樣調用fwrite():

此處指定參數size為數組元素的位元組大小,參數count為元素個數。

2)讀寫一個結構。例如,

此處指定參數size為結構的位元組大小,參數count為1。

這兩種情形的更一般例子是讀寫一個結構數組。為此,參數size應當是該結構的位元組大小,參數count應當是數組的元素個數。

從這兩個例子看出,塊i/o是按資料原始形态,即二進制格式進行讀寫的。按二進制格式讀寫資料的效率常常比使用其他形式的i/o要好,特别是對于浮點資料,二進制格式避免了格式轉換處理(2.8節)時精度的丢失。但是它也有自己的問題,就是不能用許多标準的檔案處理實用程式(如正文編輯程式)對二進制檔案進行檢查或修改。特别是,在某個系統中寫出的二進制檔案一般隻能在同一個系統中才能讀出,也就是說,二進制檔案不能在不同的語言實作或不同類型的計算機系統之間進行移植。其原因主要是:

1)不同的編譯器和不同的系統中,由于存儲邊界對齊要求不同,使得對結構成員在結構内的偏移處理有所不同。有一些編譯器有選項開關,允許結構按緊縮方式配置設定空間(以節省存儲)或精确地按邊界對齊方式配置設定空間(以優化運作時各個成員的通路)。這意味着即使在同一個系統,結構的存儲配置設定也是不同的,這取決于具體的編譯選項。

2)不同的計算機體系結構存儲多位元組資料的方式不同。例如,有的采用big-endian,有的采用little-endian(12.3.5節),而有的系統則可以在這兩者之間進行選擇。

例2-5 程式2-5使用fread()和fwrite()連接配接一個檔案至另一個檔案末尾。它的第一個參數指明要複制的檔案,第二個參數指明被連接配接的檔案,如果這個檔案不存在,則建立它。

程式2-5 fread()和fwrite()連接配接兩個檔案之例