一、流和FILE對象
系統IO都是針對文件描述符,當打開一個文件時,即返回一個文件描述符,然後用該文件描述符來進行下面的操作,而對於標准IO庫,它們的操作則是圍繞流(stream)進行的。
當打開一個流時,標准IO函數fopen返回一個指向FILE對象的指針。該對象通常是一個結構,它包含了IO庫為管理該流所需要的所有信息:用於實際IO的文件描述符,指向流緩存的指針,緩存的長度,當前在緩存中的字符數,出錯標志等等。
我們稱指向FILE對象的指針(類型為FILE *)為文件指針。
二、緩存
標准IO提供緩存的目的是盡可能減少使用read和write調用的數量。標准IO提供了三種類型的緩存
(1) 全緩存。在這種情況下,當填滿標准IO緩存後才進行實際IO操作。對於駐在磁盤上的文件通常是由標准IO實施全緩存的。在一個流上執行第一次IO操作時,相關標准IO函數通常調用malloc獲得所需的緩存。
術語刷新(flush)說明標准IO緩存的寫操作。緩存可由標准IO例程自動地刷新(例如當填滿一個緩存時),或者可以調用函數flush刷新一個流。在UNIX環境中刷新有兩種意思。在標准IO庫方面,刷新意味著將緩存中的內容寫入到磁盤上(該緩存可以只是局部填寫 的)。在終端驅動程序方面,刷新表示丟棄已存在緩存中的數據。
(2) 行緩存。在這種情況下,當在輸入和輸出中遇到換行符時,標准IO庫執行IO操作。這允許我們一次輸入一個字符(用標准IO fputc函數),但只有在寫了一行之後才進行實際IO操作。當流涉及一個終端時(例如標准輸入和標准輸出),典型的使用行緩存。
對於行緩存有兩個限制,一是:因為標准IO庫用來收集每一行的緩存長度是固定的,所以只要填滿了緩存,那麼即使還沒有寫一個換行符,也進行IO操作。第二是:任何時候只要通過標准輸入輸出庫要求從(a)一個不帶緩存的流(b)一個行緩存的流(它預先要求從內 核得到數據)得到輸入數據,那麼就會造成刷新所有行緩存輸出流。在(b)中帶了一個在擴號中的說明的理由是,所需的數據可能已在緩存中,它並不要求內核在需要該數據時才進行操作。很明顯,從不帶緩存的一個流中進行輸入((a)項)要求當時從內核中得到數 據。
(3) 不帶緩存。標准IO庫不對字符進行緩存。標准出錯流通常不帶緩存,這可以使出錯信息盡快的顯示出來。
ASSI C要求以下緩存特征:
(1) 當且僅當標准輸入和標准輸出不涉及交互作用設備時,他們才是全緩存的。
(2) 標准出錯絕不是全緩存的。
對於任何一個流如果我們不喜歡系統默認則可以通過以下兩個函數來更改緩存類型:
#include <stdio.h> void setbuf(FILE *fp, char *buf); int setvbuf(FILE *fp, char *buf, int mode, size_t size);
這些函數必須在流打開後和對該流進行任何操作之前調用。
可以使用setbuf打開或關閉緩存機制。為了帶緩存進行IO,參數buf必須指向一個長度為BUFSIZ的緩存(該常數定義在stdio.h中)。通常在此之後該流就是全緩存的,但是該流與終端設備相關,那麼某些系統也可將其設置為行緩存。為了關閉緩存,將buf設置為NULL。
使用setvbuf可以精確的說明所需的緩存類型。由mode參數指定:
_IOFBF 全緩存 _IOLBF 行緩存, _IONBF 不帶緩存
如果指定了一個不帶緩存的流,則忽略buf和size參數。如果指定全緩存或行緩存,則buf和size可以選擇地指定一個緩存及長度。如果該流是帶緩存的,而buf是NULL,則標准IO庫將自動的為該流分配適當長度的緩存,適當長度是指struct stat結構體中的st_blksize所指定的值。如果系統不能為為該流決定此值(例如該流涉及一個設備或一個管道),則分配長度為BUFSIZ的緩存。
下表列出了這兩個函數的動作,以及它們的各個選擇項
如果在一個函數中分配了一個自動變量類的標准IO緩存,則從該函數返回之前必須關閉該流。SVR4將緩存的一部分用於ta自己的管理操作,所以可以存放在緩存中的實際數據字節數小於size。一般來說,應由系統選擇緩存的長度,並自動分配緩存。在這樣處理時,標准IO庫在關閉此流時將自動關閉釋放緩存。
可以在任何時候強制刷新一個流:
#include <stdio.h> int fflush(FILE *fp);
此函數使該流所有的數據傳遞到內核,如果fp是NULL,則此函數刷新所有輸出流。
三、打開流
#include <stdio.h> FILE *fopen(const char *pathname, const char *type); FILE *freopen(const char *pathname, const char *type, FILE fp); FILE *fdopen(int fileds, const char *type); 返回值: 成功文件指針,失敗NULL
三個函數的區別:
(1)fopen打開路徑為pathname的文件。
(2)freopen在一個特定流上(由fp指定)打開pathname文件。如果該流已打開則先關閉。此函數一般用於將一個文件打開為一個預定義的流:標准輸入、標准輸出和標准出錯。
(3)fdopen取一個現存的文件描述符(可能從open\dup\dup2\fcntl\pipe函數得到此文件描述符)並使一個標准的IO流與該文件描述符結合。此函數常用於由創建管道和網絡通信通道函數獲得的插入符。因為這些特殊類型的文件不能用標准IO fopen打開
type參數指定對該IO流的讀、寫方式,ANSI C規定type參數可以有15種值:
使用字符b作為type的一部分使得標准IO系統可以區分文本文件或二進制文件。因為UNXI並不對這兩種文件區分所以在UNIX環境下指定b作為type的一部分實際上並無作用。
對於fdopen,type參數的意義有些區別。因為文件描述符已打開,所以fdopen為寫而打開並不截短該文件。另外,標准IO添加方式也不能用於創建文件,因為如果一個文件描述符引用一個文件則該文件一定已經存在。
當用添加類型打開一個文件後則每次寫都將數據寫到文件當前尾端處。如果有多個進程用標准IO的方式打開同一個文件,則來自每個進程的數據都將正確的寫到文件中。
當以讀和寫打開一文件時(type中+號),具有如下限制:
如果中間沒有fflush、fseek、fsetpos或rewind,則在輸出的後面不能直接跟隨輸入。
如果中間沒有fseek、fsetpos、或rewind或者一個輸出操作沒有到達文件尾端,則在輸入操作之後不能直接跟隨輸出。
下表是打開一個流的六種不同的方式:
在指定w或a類型創建一個新文件時,無法說明文件的存取許可位,POSIX.1要求以這種方式創建的文件具有以下權限:
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
除非流引用終端設備,否則按系統默認,它被打開時是全緩存。若流引用終端設備則該流是行緩存。
用fclose關閉一個打開的流:
#include <stdio.h> int fclose(FILE *fp); 返回值: 成功0,出錯EOF
在該文件關閉之前,刷新緩存中的輸出數據。緩存中的輸入數據被丟棄。如果標准IO庫已經為該流自動分配了一個緩存則釋放該緩存。
當一個進程正常終止時(直接調用exit()函數或者從main函數中返回),則所有帶未寫緩存數據的標准IO流都被刷新,所有打開的標准IO流都被關閉。
四、讀和寫流
一旦打開了流,則可以在以下三種不同類型的非格式化IO中進行選擇,對其進行讀寫:
(1) 每次一個字符的IO。一次讀或寫一個字符,如果流是帶緩存的,則標准IO函數處理所有緩存。
(2) 每次一行的IO。使用fgets和fputs一次讀或寫一行。每行都以一個新行符結束。當調用fgets時應說明能處理的最大行長。
(3) 直接IO。 fwrite和fread函數支持這種類型的IO。每次IO操作讀或寫某種數量的對象,每個對象具有指定的長度。這兩個函數常用於從二進制文件中讀或寫一個結構。
直接IO(direct IO)這個術語來自ANSI C標准,有時也被稱為:二進制IO、一次一個對象IO、面向記錄的IO或面向結構的IO。
1.輸入函數
以下三個函數用於一次讀一個字符:
#include <stdio.h> int getc(FILE *fp); int fgetc(FILE *fp); int getchar(void); 返回值:成功則為下一個字符,如果已處於文件尾端或出錯則為EOF
getchar等同於getc(stdin)。前兩個函數的區別是getc可被實現為宏,而fgetc則不能。(這裡的可被實現為宏即大多數UNIX系統中getc的實現是這樣:在<stdio.h>中#define getc(FILE *fp) xxx(FILE *fp),即getc不是一個函數而是一個宏)這意味著:
(1) getc的參數不應當是具有副作用的表達式。
(2) 因為fgetc一定是個函數,所以可得到其地址。這就允許將fgetc的地址作為一個參數傳送給另一個函數。
(3) 調用fgetc所需時間可能長於調用getc,因為調用函數通常所需的時間長於調用宏。
這三個函數以unsigned char類型轉換為int的方式返回下一個字符。返回int的原因是函數可以返回一個負值(指示出錯或已到達文件尾端),在<studio.h>中常數EOF常要求是一個負值,其值經常是-1。所以不能將這三個函數的返回值放到一個字符變量中。
不管是出錯還是到達文件尾端,這三個函數都返回同樣的值。為了區分這兩種不同的情況,須調用ferror或feof。
#include <stdio.h> int ferror(FILE *fp); int feof(FILE *fp); 返回值:條件為真返回非0值,否則0 void clearerr(FILE *fp);
在大多數實現的FILE對象中,為每個流保持了兩個標志
從一個流讀之後,可以調用ungetc將字符再送回到流中。
#include <stdio.h> int ungetc(int c, FILE *fp);
送回到流中的字符又可以從流中讀出,但讀出字符的順序與送回的順序相反。回送的字符不一定是上一次讀到的字符。EOF不能回送。但是當已到達文件尾端時仍可以回送一個字符。下次讀將返回該字符,再次讀則返回EOF。之所以可以這樣做的原因是一次成功的ungetc調用會清除該流的文件結束指示。
當正在讀一個輸入流,並進行某種形式的分字或分記號操作時,會經常用到回送字符操作。有時需要先看一看下一個字符以決定如何處理當前字符。然後就需要方便的將剛查看的字符送回,以便下一次調用getc時返回該字符。
2. 輸出函數
#include <stdio.h> int putc(int c, FILE *fp); int fputc(int c, FILE *fp); int putchar(int c); 返回值:成功返回c,出錯EOF
putchar等同於putc(c, stdout)
五、 每次一行IO
下面兩個函數提供每次輸入一行的功能:
#include <stdio.h> char *fgets(char *buf, int n, FILE *fp); char *fgets(char *buf);
gets從標准輸入讀。對於fgets必須指定緩存buf的長度n。此函數會一直讀到下一個新行符為止,但是不超過n-1個字符,讀入的字符被送入到緩存。該緩存以null字符結尾。如果該行包括最後一個新行符的字符數超過n-1,則只返回一個不完整的行,而且緩存總是以null字符結尾。對fgets的下一次調用會繼續該行。
gets函數不被推薦使用因為不能指定緩存的長度,gets並不將新行符存入緩存中。
#include <stdio.h> int fputs(const char *str, FILE *fp); int puts(const char *str);
返回值:成功返回非負值,出錯EOF
函數fputs將一個以null符終止的字符串寫入到指定的流,終止符null不寫出。這並不一定是每次輸出一行,因為它並不要求在null符之前一定是新行符,但是通常在null符之前是一個新行符。
puts將一個以null符終止的字符串寫入到標准輸出,終止符不寫出。但是puts然後將一個新行符寫到標准輸出。