1. 程式人生 > >第五章 標準I/O

第五章 標準I/O

依靠 成員 終端設備 添加 緩沖區 pan getc orm ron

5.1 引言

本章說明標準 I/O 庫。因為不僅在 UNIX 上,而且在很多操作系統上都實現了此庫,所以它由 ISO C 標準說明。

標準 I/O 庫處理很多細節,例如緩沖區分配,以優化長度執行 I/O 等。這些處理使用戶不必擔心如何選擇使用正確的塊長度(如3.9節所述)。這使得它便於用戶使用,但是如果不叫深入地了解 I/O 庫函數的操作,也會帶來一些問題。

標準 I/O 庫是由 Dennis Ritchie 在1975年左右編寫的。令人驚訝的是,經過30年後,對標準 I/O 庫只做了極小的修改。

5.2 流和FILE對象

在第3章中,所有I/O函數都是針對文件描述符的。當打開一個文件時,即返回一個文件描述符,然後該文件描述符就用於後續的 I/O 操作。而對於標準 I/O 庫,它們的操作則是圍繞流(stream)進行的。當標準 I/O 庫打開或創建一個文件時,我們已使一個流與一個文件相關聯。

對於 ASCII 字符集,一個字符用一個字節表示。對於國際字符集,一個字符可用多個字節表示。標準 I/O 文件流可用於單字節或多字節的字符集。流的定向(stream‘s orientation)決定了所讀、寫的字符是單字節還是多字節的。當一個流最初被創建時,它並沒有定向。如若在未定向的流上使用一個多字節 I/O 函數(減 <wchar.h>),則將該流的定向設置為寬定向。若在未定向的流上使用一個單字節 I/O 函數,則將該流的定向設置為字節定向的。只有兩個函數可以改變流的定向。 freopen函數(參見5.5節)清除一個流的定向;fwide 函數設置流的定向。

int fwide(FILE *fp, int
mode);

根據mode參數的不同值, fwide函數執行不同的工作:

(1)如若mode參數值為負,fwide將試圖使指定的流是字節定向的。

(2)如果mode參數值為正,fwide將試圖指定的流是寬定向的。

(3)如若mode參數值為0,fwide 將不試圖設置流的定向,但返回標識該流定向的值。

註意,fwide並不改變已定向流的定向。還應註意的是,fwide無出錯返回。試想如若流是無效的,那麽將會發生什麽呢?我們唯一可依靠的是,在調用 fwide 前先清除 errno,從fwide返回時檢查 errno的值。在本書的其余部分,我們只涉及字節定向流。

當打開一個流時,標準 I/O 函數 fopen 返回一個指向 FILE 對象的指針。該對象通常是一個結構,它包含了標準 I/O 庫為管理該流所需要的所有信息,包括:用於實際 I/O 的文件描述符、指向用於該流緩沖區的指針、緩沖區的長度、當前緩沖區中字節數以及出錯標準等等。

應用程序沒有必要檢查 FILE 對象。為了引用一個流,需將 FILE 指針作為參數傳遞給每個標準 I/O 函數。在整本書中,我們稱指向 FILE對象的指針(類型為 FILE *)為文件指針。

5.3 標準輸入、標準輸出、標準出錯

對一個進程預定義了三個流,並且這三個流可以自動地被進程使用,它們是:標準輸入、標準輸出和標準出錯。這些流引用地文件與3.2節中提到地文件描述符 STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO所引用地文件相同。

這三個標準 I/O 流通過預定義文件指針 stdin、stdout和stderr加以引用。這三個文件指針同樣定義在頭文件 <stdio.h>中。

5.4 緩沖

標準 I/O 庫提供緩沖地目的是盡可能減少使用 read 和write 調用次數。它也對每個 I/O 流自動地進行緩沖管理,從而避免了應用程序需要考慮這一點帶來地麻煩。不幸的是,標準 I/O 庫最令人迷惑地也是它地緩沖。

標準I/O 提供了三種類型地緩沖:

(1)全緩沖。這種情況下,在填滿標準 I/O 緩沖區後才進行實際 I/O 操作。對於駐留在磁盤上的文件通常是由標準 I/O 庫實施全緩沖的。 在一個流上執行第一次 I/O 操作時,相關標準 I/O 函數通常調用 malloc(見7.8節)獲得需使用的緩沖區。

術語沖洗(flush)說明標準I/O 緩沖區的寫操作。緩沖區可由標準 I/O 例程自動沖洗(例如當填滿一個緩沖區時),或者可以調用函數 fflush 沖洗一個流。值得引起註意的是在 UNIX 環境中,flush有兩種意思。在標準 I/O 庫方面, flush 意味著將緩沖區中的內容寫到磁盤上(該緩沖區可能只是局部填寫的)。在終端驅動程序方面,flush表示丟棄已存儲在緩沖區中的數據。

(2)行緩沖。在這種情況下,當輸入和輸出中遇到換行符時,標準 I/O 庫執行 I/O 操作。這允許我們一次輸出一個字符(用標準I/O fputc函數),但只有在寫了一行之後才進行實際 I/O 操作。當流涉及一個終端時(例如標準輸入和標準出錯),通常使用行緩沖。

對於行緩沖有兩個限制。第一,因為標準 I/O 庫用來收集每一行的緩沖區的長度是固定的,所以只要填滿了緩沖區,那麽即使還沒有寫一個換行符,也進行 I/O 操作。第二,任何時候只要通過標準 I/O 庫要求從 (a) 一個不帶緩沖的流,或者 (b)一個行緩沖的流(它要求從內核得到數據)得到輸入數據,那麽就會造成沖洗所有行緩沖輸出流。在(b)中帶了一個括號中的說明,其理由是,所需的數據可能已在該緩沖區中,它並不要求在需要數據時才從內核讀數據。很明顯,從不帶緩沖的一個流中進行輸入 要求當時從內核得到數據。

(3)不帶緩沖。標準 I/O 庫不對字符進行緩沖存儲。例如,如果用標準 I/O 函數 fputs 寫15個字符到不帶緩沖的流中,則該函數很可能用 write 系統調用函數將這些字符立即寫至相關聯的打開文件上。

對於任何給定的流,可調用下列函數更改緩沖類型:

void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode,
                 size_t size);

任何時候,我們都可強制沖洗一個流。

int fflush(FILE *fp);

函數使該流所有未寫的數據都被傳送至內核。作為一個特例,如若 fp 使 NULL,則此函數將導致所有輸出流被沖洗。

5.5 打開流

下列三個函數打開一個標準 I/O 流。

FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type,
                     FILE *restrict fp);
FILE *fdopen(int filedes, const char *type);

三個函數的區別是:

(1)fopen打開一個指定的文件。

(2)freopen在一個指定的流上打開一個指定的文件,如若該流已經打開,則先關閉該流。若該流已經定向,則 freopen 清除該定向。此函數一般用於將一個指定的文件打開為一個預定義的流:標準輸入、標準出錯、標準出錯。

(3)fdopen獲取一個現有文件描述符,並使一個標準 I/O 流與該描述符相結合。此函數常用於由創建管道和網絡通信函數返回的描述符。因為這些特殊類型的文件不能用標準 I/O fopen函數打開,所以我們必須先調用設備專門函數以獲得一個文件描述符,然後用 fdopen 使一個標準 I/O 流與該描述符相關聯。

type參數指定對該I/O流的讀、寫方式,ISO C規定type參數可以有15種不同的值,它們示於下表:

技術分享圖片

使用字符 b 作為 type 的一部分,這使得標準 I/O 可以區分文本文件和二進制文件。因為 UNIX內核並不對這兩種文件進行區分,所以在 UNIX 系統環境下指定字符 b 作為 type 的一部分實際上並無作為。

對於 fdopen,type參數的意義稍有區別。因為描述符已經被打開,所以fdopen為寫而打開並不截短該文件(例如,若該描述符原來是由 open 函數創建的,而且該文件那時已經存在,則其 O_TRUNC標誌將決定是否截短該文件。fdopen函數不能截短它為寫而打開的任一文件。)另外,標誌 I/O 添寫方式也不能用於創建該文件(因為如若一個描述符引用一個文件,則該文件一定已經存在)。

當用添寫類型打開一文件後,則每次寫都將數據寫到文件的尾端處。如若有多個進程用標誌 I/O 添寫方式打開了同一文件,那麽來自每個進程的數據都將正確地寫到文件中。

註意,在指定 w 或 a 類型創建一個新文件時,我們無法說明該文件地訪問權限位(open函數和creat函數則能做到)。

除非流引用終端設備,否則按系統默認情況,流被打開時是全緩沖的。若流引用終端設備,則該流是行緩沖的。一旦打開了流,那麽在對流執行任何操作之前,如果希望,則可使用上一節所述的 setbuf和setvbuf改變緩沖類型。

調用 fclose關閉一個打開的流

int fclose(FILE *fp);

在該文件被關閉之前,沖洗緩沖區中的輸出數據。丟棄緩沖區中的任何輸入數據。如果標誌 I/O庫已經為流自動分配了一個緩沖區,則釋放此緩沖區。

當一個進程正常終止時(直接調用 exit函數,或從 main 函數返回),則所有帶未寫緩沖數據的標誌 I/O 流都會被沖洗,所有打開的標誌 I/O 流都會被關閉。

5.6 讀和寫流

一旦打開了流,則可在三種不同類型的非格式化I/O中進行選擇,對其進行讀、寫操作:

(1)每次一個字符的I/O。一次讀或寫一個字符,如果流是帶緩沖的,則標準I/O函數會處理所有緩沖。

(2)每次一行的I/O。如果想要一次讀或寫一行,則使用fgets和fputs。每行都以一個換行符終止。當調用 fgets時,應說明能處理的最大行長。

(3)直接I/O。fread和fwrite函數支持這種類型的I/O。每次I/O操作讀或寫某種數量的對象,而每個對象具有指定的長度。這兩個函數常用於從二進制文件中每次讀或寫一個結構。

1.輸入函數

int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);

函數getchar等價於getc(stdin)。前面兩個函數的區別是getc可被實現為宏,而fgetc則不能實現為宏。這意味著:

(1)getc的參數不應當是具有副作用的表達式

(2)因為fgetc一定是一個函數,所以可以得到其地址。這就允許將fgetc的地址作為一個參數傳送給另一個函數。

(3)調用fgetc所需時間很可能長於調用getc,因為調用函數通常所需的時間長於調用宏。

這三個函數在返回下一個字符時,會將其 unsigned char 類型轉換成 int 類型。說明為不帶符合的理由是,如果最高位為1也不會使返回值為負。要求整形返回值的理由是,這樣就可以返回所有可能的字符值再加上一個已出錯或已達到文件尾端的指示值。在 <stdio.h> 中的常量 EOF被要求是一個負值,其值經常是-1.這就意味著不能將這三個函數的返回值存放在一個字符變量中,以後還要將這些函數的返回值與常量 EOF 相比較。

註意,不管是出錯還是到達文件尾端,這三個函數都返回同樣的值。為了區分這兩種不同的情況,必須調用 ferror 或 feof

int ferror(FILE *fp);
int feof(FILE *fp);

void clearerr(FILE *fp);

在大多數實現中,為每個流在FILE對象中維持了兩個標誌:

(1)出錯標誌。

(2)文件結束標誌。

調用 clearerr 則清除這兩個標誌。

從流中讀取數據以後,可以調用 ungetc 將字符再壓送回流中。

int ungetc(int c, FILE *fp);

壓送回流中的字符以後又可從流中讀出,但讀出字符的順序與壓送回流的順序相反。應當了解,雖然 ISO C允許實現支持任何次數的回送,但是它要求實現提供一次只送回一個字符。我們不能期望一次能送回多個字符。

回送的字符不必一定是上一次讀到的字符。不能回送EOF。但是當已經到達文件尾端時,仍可以回送一字符。下次讀將返回該字符,再次讀則返回EOF。之所以能這樣做的原因是一次成功的ungetc調用會清除流的文件結束標誌。

當正在讀一個輸入流,並進行某種形式的分字或分記號操作時,會經常用到回送字符操作。有時需要先看一看下一個字符,以決定如何處理當前字符。然後就需要方便地將剛查看地字符送回,以便下一次調用getc時返回該字符。如果標誌I/O庫不提供回送能力,就需將該字符存放到一個我們自己的變量中,並設置一個標誌以便判別在下一次需要一個字符時是調用getc還是從我們自己的變量中取用。

用ungetc壓送回字符時,並沒有將它們寫到文件中或設備上,只是將它們寫回標誌I/O庫的流緩沖區中。

2. 輸出函數

int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);

與輸入函數一樣,putchar(c)等效於putc(c, stdout),putc可實現為宏,而fputc則不能實現為宏。

5.7 每次一行I/O

下面兩個函數提供每次輸入一行的功能

char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);

這兩個函數都指定了緩沖區的地址,讀入的行將送入其中。gets從標準輸入讀,而fgets則從指定的流讀。

對於fgets,必須指定緩沖區的長度n,此函數一直讀到下一個換行符為止,但是不超過n-1個字符,讀入的字符被送入緩沖區。該緩沖區以null字符結尾。如若改行(包括最後一個換行符)的字符數超過n-1,則fgets只返回一個不完整的行,但是緩沖區總時以null字符結尾。對fgets的下一次調用會繼續讀該行。

gets是一個不推薦使用的函數。其問題是調用者在使用gets時不能指定緩沖區的長度。這樣就可能造成緩沖區溢出(如若該行長於緩沖區長度),寫到緩沖區之後的存儲空間中,從而產生不可預料的後果。這種缺陷曾被利用,造成1988年的因特網蠕蟲事件。gets和fgets的另一個區別是,gets並不將換行符存入緩沖區中。

即使ISO C要求實現提供gets,但請使用fgets,而不要使用gets。

fputs和puts提供每次輸出一行的功能。

int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);

函數fputs將一個以null符終止的字符串寫到指定的流,尾端的終止符null不寫出。註意,這並不一定是每次輸出一行,因為它並不要求在null符之前一定是換行符。通常,在null符之前是一個換行符,但並不要求總是如此。

puts將一個以null符終止的字符串寫到標準輸出,終止符不寫出。但是,puts然後又將一個換行符寫到標準輸出。

puts並不像它所對應的gets那樣不安全。但是我們還是應避免使用它,以免需要記住它在最後是否添加一個換行符。如果總是使用 fgets 和 fputs,那麽就會熟知在每行終止處我們必須自己處理換行符。

5.8 標準I/O的效率

使用上一節所述的函數,我們能對標準I/O系統的效率有所了解。

技術分享圖片

不在本文章中解釋原因和探究細節。

5.9 二進制 I/O

5.6節和5.7節中的函數以一次一個字符或一次一行的方式進行操作。如果進行二進制 I/O 操作,那麽我們更願意一次讀或寫整個結構。如果使用getc或putc讀、寫一個結構,那麽必須循環通過整個結構,每次循環處理一個字節,一次讀或寫一個字節,這會非常麻煩且費時。如果使用fputs和fgets,那麽因為fputs在遇到null字節時就停止,而在結構中可能含有null字節,所以不能使用它實現讀結構的要求。類似地,如果輸入數據中包含null字節或換行符,則fgets也不能正確工作。因此,提供了下列兩個函數以執行二進制I/O操作。

size_t fread(void *restrict ptr, size_t size, size_t nobj,
                  FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj,
                   FILE *restrict fp);

這些函數有兩種常見地用法:

(1)讀或寫一個二進制數組。例如,為了將一個浮點數組的第2~5個元素寫至一個文件上,可以編寫如下程序:

float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
   err_sys("fwrite error");

其中,指定size為數組元素的長度,nobj為欲寫的元素數。

(2)讀或寫一個結構,例如,可以編寫如下程序:

struct {
   short count;
   long total;
   char name[NAMESIZE];
} item;

if (fwrite(&item, sizeof(item), 1, fp) != 1)
   err_sys("fwrite error");

將這兩個例子結合起來就可讀或寫一個結構數組。為了做到這一點,size應當是該結構的sizeof,nobj應是該數組中元素個數。

fread和fwrite返回讀或寫的對象數。對於讀,如果出錯或到達文件尾端,則此數字可以少於nobj。在這種情況下,應調用ferror或feof以判斷究竟屬於哪一種情況。對於寫,如果返回值少於所要求的nobj,則出錯。

使用二進制I/O的基本問題是,他只能用於讀在同一個系統已寫的數據。所以在異構系統(多臺主機通過網絡互相連接起來,構成一個系統)中,在一個系統上寫的數據,要在另一個系統上進行處理,則可能出錯,原因是:

(1)在一個結構中,同一成員的偏移量可能因編譯器或系統而異(由於不同的對準要求)。

(2)用來存儲多個字節整數和浮點值得二進制格式在不同機器體系結構間也可能不同。

5.10 定位流

有三種方法定位標準I/O流。

(1)ftell和fseek函數。它們都假定文件的位置可以存放在一個長整型中。

(2)ftello和fseeko函數。可以使文件偏移量不必一定使用長整型。它們使用off_t數據類型代替長整型。

(3)fgetpos和fsetpos函數。它們使用一個抽象數據類型fpos_t記錄文件位置。這種數據類型可以定義為記錄一個文件位置所需的長度。

long ftell(FILE *fp);

int seek(FILE *fp, long offset, int whence);

void rewind(FILE *fp);

對於一個二進制文件,其文件位置指示器是從文件起始位置開始度量,並以字節為計量單位。ftell用於二進制文件時,其返回值就是這種字節位置。為了用fseek定位一個二進制文件,必須指定一個字節offset,以及解釋這種偏移量的方式。whence的值與3.6節中lseek函數的相同:SEEK_SET表示從文件的起始位置開始,SEEK_CUR表示從當前文件位置開始,SEEK_END表示從文件尾端開始。

使用rewind函數也可將一個流設置到文件的起始位置。

除了offset的類型是off_t而非long以外,ftello函數與ftell相同,fseeko函數與fseek相同。

off_t ftello(FILE *fp);
int fseeko(FILE *fp, off_t offset, int whence);

fgetpos和fsetpos這兩個函數是C標準引進的。

int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);

fgetpos將文件的位置指示器的當前值存入由pos指向的對象中。在以後調用fsetpos時,可以使用此值將流重定位至該位置。

5.11 格式化I/O

1.格式化輸出

執行格式化處理的是4個printf函數

int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int sprintf(char *restrict buf, const char *restrict format, ...);
int sprintf(char *restrict buf, size_t n, 
               const char *restrict format, ...);

printf將格式化數據寫到標準輸出,fprintf寫至指定的流,sprintf將格式化的字符送入數組buf中。sprintf在該數組的尾端自動加一個null字節,但該字節不包括在返回值中。

sprintf函數可能會造成緩沖區溢出,snprintf函數解決了該問題,緩沖區的長度是一個顯示參數,超過緩沖區的字符數會被丟棄。snprintf函數會返回寫入緩沖區的字符數,與sprintf相同,該返回值不包括結尾的null字節。若snprintf函數返回小於緩沖區長度n的正值,那麽沒有截短輸出。若發生了一個編碼錯誤,snprintf則返回負值。

2.格式化輸入

執行格式化輸入處理的是三個scanf函數

int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format,
               ...);

5.12 實現細節

正如前述,在UNIX系統中,標準I/O庫最終都要調用第3章中說明的I/O例程。每個標準I/O流都有一個與其相關聯的文件描述符,可以對一個流調用fileno函數以獲得其描述符。

int fileno(FILE *fp);

如果要調用dup或fcntl等函數,則需要此函數。

5.13 臨時文件

ISO C標準I/O庫提供了兩個函數以幫助創建臨時文件。

char *tmpnam(char *ptr);
FILE *tmpfile(void);

第五章 標準I/O