讀古今文學網 > CPrimerPlus(第6版)(中文版) > 第13章 文件輸入/輸出 >

第13章 文件輸入/輸出

本章介紹以下內容:

函數:fopen、getc、putc、exit、fclose

fprintf、fscanf、fgets、fputs

rewind、fseek、ftell、fflush

fgetpos、fsetpos、feof、ferror

ungetc、setvbuf、fread、fwrite

如何使用C標準I/O系列的函數處理文件

文件模式和二進制模式、文本和二進制格式、緩衝和無緩衝I/O

使用既可以順序訪問文件也可以隨機訪問文件的函數

文件是當今計算機系統不可或缺的部分。文件用於儲存程序、文檔、數據、書信、表格、圖形、照片、視頻和許多其他種類的信息。作為程序員,必須會編寫創建文件和從文件讀寫數據的程序。本章將介紹相關的內容。

13.1 與文件進行通信

有時,需要程序從文件中讀取信息或把信息寫入文件。這種程序與文件交互的形式就是文件重定向(第8章介紹過)。這種方法很簡單,但是有一定限制。例如,假設要編寫一個交互程序,詢問用戶書名並把完整的書名列表保存在文件中。如果使用重定向,應該類似於:

books > bklist

用戶的輸入被重定向到 bklist 中。這樣做不僅會把不符合要求的文本寫入 bklist,而且用戶也看不到要回答什麼問題。

C提供了更強大的文件通信方法,可以在程序中打開文件,然後使用特殊的I/O函數讀取文件中的信息或把信息寫入文件。在研究這些方法之前,先簡要介紹一下文件的性質。

13.1.1 文件是什麼

文件(file)通常是在磁盤或固態硬盤上的一段已命名的存儲區。對我們而言,stdio.h就是一個文件的名稱,該文件中包含一些有用的信息。然而,對操作系統而言,文件更複雜一些。例如,大型文件會被分開儲存,或者包含一些額外的數據,方便操作系統確定文件的種類。然而,這都是操作系統所關心的,程序員關心的是C程序如何處理文件(除非你正在編寫操作系統)。

C把文件看作是一系列連續的字節,每個字節都能被單獨讀取。這與UNIX環境中(C的發源地)的文件結構相對應。由於其他環境中可能無法完全對應這個模型,C提供兩種文件模式:文本模式和二進制模式。

13.1.2 文本模式和二進制模式

首先,要區分文本內容和二進制內容、文本文件格式和二進制文件格式,以及文件的文本模式和二進制模式。

所有文件的內容都以二進制形式(0或1)儲存。但是,如果文件最初使用二進制編碼的字符(例如, ASCII或Unicode)表示文本(就像C字符串那樣),該文件就是文本文件,其中包含文本內容。如果文件中的二進制值代表機器語言代碼或數值數據(使用相同的內部表示,假設,用於long或double類型的值)或圖片或音樂編碼,該文件就是二進制文件,其中包含二進制內容。

UNIX用同一種文件格式處理文本文件和二進制文件的內容。不奇怪,鑒於C是作為開發UNIX的工具而創建的,C和UNIX在文本中都使用\n(換行符)表示換行。UNIX目錄中有一個統計文件大小的計數,程序可使用該計數確定是否讀到文件結尾。然而,其他系統在此之前已經有其他方法處理文件,專門用於保存文本。也就是說,其他系統已經有一種與UNIX模型不同的格式處理文本文件。例如,以前的OS X Macintosh文件用\r (回車符)表示新的一行。早期的MS-DOS文件用\r\n組合表示新的一行,用嵌入的Ctrl+Z字符表示文件結尾,即使實際文件用添加空字符的方法使其總大小是256的倍數(在Windows中,Notepad仍然生成MS-DOS格式的文本文件,但是新的編輯器可能使用類UNIX格式居多)。其他系統可能保持文本文件中的每一行長度相同,如有必要,用空字符填充每一行,使其長度保持一致。或者,系統可能在每行的開始標出每行的長度。

為了規範文本文件的處理,C 提供兩種訪問文件的途徑:二進制模式和文本模式。在二進制模式中,程序可以訪問文件的每個字節。而在文本模式中,程序所見的內容和文件的實際內容不同。程序以文本模式讀取文件時,把本地環境表示的行末尾或文件結尾映射為C模式。例如,C程序在舊式Macintosh中以文本模式讀取文件時,把文件中的\r轉換成\n;以文本模式寫入文件時,把\n轉換成\r。或者,C文本模式程序在MS-DOS平台讀取文件時,把\r\n轉換成\n;寫入文件時,把\n轉換成\r\n。在其他環境中編寫的文本模式程序也會做類似的轉換。

除了以文本模式讀寫文本文件,還能以二進制模式讀寫文本文件。如果讀寫一個舊式MS-DOS文本文件,程序會看到文件中的\r 和\n 字符,不會發生映射(圖 13.1 演示了一些文本)。如果要編寫舊式 Mac格式、MS-DOS格式或UNIX/Linux格式的文件模式程序,應該使用二進制模式,這樣程序才能確定實際的文件內容並執行相應的動作。

圖13.1 二進制模式和文本模式

雖然C提供了二進制模式和文本模式,但是這兩種模式的實現可以相同。前面提到過,因為UNIX使用一種文件格式,這兩種模式對於UNIX實現而言完全相同。Linux也是如此。

13.1.3 I/O的級別

除了選擇文件的模式,大多數情況下,還可以選擇I/O的兩個級別(即處理文件訪問的兩個級別)。底層I/O(low-level I/O)使用操作系統提供的基本I/O服務。標準高級I/O(standard high-level I/O)使用C庫的標準包和stdio.h頭文件定義。因為無法保證所有的操作系統都使用相同的底層I/O模型,C標準只支持標準I/O包。有些實現會提供底層庫,但是C標準建立了可移植的I/O模型,我們主要討論這些I/O。

13.1.4 標準文件

C程序會自動打開3個文件,它們被稱為標準輸入(standard input)、標準輸出(standard output)和標準錯誤輸出(standard error output)。在默認情況下,標準輸入是系統的普通輸入設備,通常為鍵盤;標準輸出和標準錯誤輸出是系統的普通輸出設備,通常為顯示屏。

通常,標準輸入為程序提供輸入,它是 getchar和 scanf使用的文件。程序通常輸出到標準輸出,它是putchar、puts和printf使用的文件。第8章提到的重定向把其他文件視為標準輸入或標準輸出。標準錯誤輸出提供了一個邏輯上不同的地方來發送錯誤消息。例如,如果使用重定向把輸出發送給文件而不是屏幕,那麼發送至標準錯誤輸出的內容仍然會被發送到屏幕上。這樣很好,因為如果把錯誤消息發送至文件,就只能打開文件才能看到。

13.2 標準I/O

與底層I/O相比,標準I/O包除了可移植以外還有兩個好處。第一,標準I/O有許多專門的函數簡化了處理不同I/O的問題。例如,printf把不同形式的數據轉換成與終端相適應的字符串輸出。第二,輸入和輸出都是緩衝的。也就是說,一次轉移一大塊信息而不是一字節信息(通常至少512字節)。例如,當程序讀取文件時,一塊數據被拷貝到緩衝區(一塊中介存儲區域)。這種緩衝極大地提高了數據傳輸速率。程序可以檢查緩衝區中的字節。緩衝在後台處理,所以讓人有逐字符訪問的錯覺(如果使用底層I/O,要自己完成大部分工作)。程序清單13.1演示了如何用標準I/O讀取文件和統計文件中的字符數。我們將在後面幾節討論程序清單 13.1 中的一些特性。該程序使用命令行參數,如果你是Windows用戶,在編譯後必須在命令提示窗口運行該程序;如果你是Macintosh用戶,最簡單的方法是使用Terminal在命令行形式中編譯並運行該程序。或者,如第11章所述,如果在IDE中運行該程序,可以使用Xcode的Product菜單提供命令行參數。或者也可以用puts和fgets函數替換命令行參數來獲得文件名。

程序清單13.1 count.c程序

/* count.c -- 使用標準 I/O */

#include <stdio.h>

#include <stdlib.h>// 提供 exit的原型

int main(int argc, char *argv )

{

int ch;// 讀取文件時,儲存每個字符的地方

FILE *fp; // 「文件指針」

unsigned long count = 0;

if (argc != 2)

{

printf("Usage: %s filename\n", argv[0]);

exit(EXIT_FAILURE);

}

if ((fp = fopen(argv[1], "r")) == NULL)

{

printf("Can't open %s\n", argv[1]);

exit(EXIT_FAILURE);

}

while ((ch = getc(fp)) != EOF)

{

putc(ch, stdout); // 與 putchar(ch); 相同

count++;

}

fclose(fp);

printf("File %s has %lu characters\n", argv[1], count);

return 0;

}

13.2.1 檢查命令行參數

首先,程序清單13.1中的程序檢查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也能結束整個程序。

13.2.2 fopen函數

繼續分析程序清單13.1,該程序使用fopen函數打開文件。該函數聲明在stdio.h中。它的第1個參數是待打開文件的名稱,更確切地說是一個包含改文件名的字符串地址。第 2 個參數是一個字符串,指定待打開文件的模式。表13.1列出了C庫提供的一些模式。

表13.1 fopen的模式字符串

像UNIX和Linux這樣只有一種文件類型的系統,帶b字母的模式和不帶b字母的模式相同。

新的C11新增了帶x字母的寫模式,與以前的寫模式相比具有更多特性。第一,如果以傳統的一種寫模式打開一個現有文件,fopen會把該文件的長度截為 0,這樣就丟失了該文件的內容。但是使用帶 x字母的寫模式,即使fopen操作失敗,原文件的內容也不會被刪除。第二,如果環境允許,x模式的獨佔特性使得其他程序或線程無法訪問正在被打開的文件。

警告

如果使用任何一種"w"模式(不帶x字母)打開一個現有文件,該文件的內容會被刪除,以便程序在一個空白文件中開始操作。然而,如果使用帶x字母的任何一種模式,將無法打開一個現有文件。

程序成功打開文件後,fopen將返回文件指針(file pointer),其他I/O函數可以使用這個指針指定該文件。文件指針(該例中是fp)的類型是指向FILE的指針,FILE是一個定義在stdio.h中的派生類型。文件指針fp並不指向實際的文件,它指向一個包含文件信息的數據對象,其中包含操作文件的I/O函數所用的緩衝區信息。因為標準庫中的I/O函數使用緩衝區,所以它們不僅要知道緩衝區的位置,還要知道緩衝區被填充的程度以及操作哪一個文件。標準I/O函數根據這些信息在必要時決定再次填充或清空緩衝區。fp指向的數據對像包含了這些信息(該數據對象是一個 C結構,將在第 14章中介紹)。

13.2.3 getc和putc函數

getc和putc函數與getchar和putchar函數類似。所不同的是,要告訴getc和putc函數使用哪一個文件。下面這條語句的意思是「從標準輸入中獲取一個字符」:

ch = getchar;

然而,下面這條語句的意思是「從fp指定的文件中獲取一個字符」:

ch = getc(fp);

與此類似,下面語句的意思是「把字符ch放入FILE指針fpout指定的文件中」:

putc(ch, fpout);

在putc函數的參數列表中,第1個參數是待寫入的字符,第2個參數是文件指針。

程序清單13.1把stdout作為putc的第2個參數。stdout作為與標準輸出相關聯的文件指針,定義在stdio.h中,所以putc(ch, stdout)與putchar(ch)的作用相同。實際上,putchar函數一般通過putc來定義。與此類似,getchar也通過使用標準輸入的getc來定義。

為何該示例不用 putchar而要用 putc?原因之一是為了介紹 putc函數;原因之二是,把stdout替換成別的參數,很容易將這段程序改寫成文件輸出。

13.2.4 文件結尾

從文件中讀取數據的程序在讀到文件結尾時要停止。如何告訴程序已經讀到文件結尾?如果 getc函數在讀取一個字符時發現是文件結尾,它將返回一個特殊值EOF。所以C程序只有在讀到超過文件末尾時才會發現文件的結尾(一些其他語言用一個特殊的函數在讀取之前測試文件結尾,C語言不同)。

為了避免讀到空文件,應該使用入口條件循環(不是do while循環)進行文件輸入。鑒於getc (和其他C輸入函數)的設計,程序應該在進入循環體之前先嘗試讀取。如下面設計所示:

// 設計範例 #1

int ch;// 用int類型的變量儲存EOF

FILE * fp;

fp = fopen("wacky.txt", "r");

ch = getc(fp); // 獲取初始輸入

while (ch != EOF)

{

putchar(ch); // 處理輸入

ch = getc(fp);// 獲取下一個輸入

}

以上代碼可簡化為:

// 設計範例 #2

int ch;

FILE * fp;

fp = fopen("wacky.txt", "r");

while (( ch = getc(fp)) != EOF)

{

putchar(ch); //處理輸入

}

由於ch = getc(fp)是while測試條件的一部分,所以程序在進入循環體之前就讀取了文件。不要設計成下面這樣:

// 糟糕的設計(存在兩個問題)

int ch;

FILE * fp;

fp = fopen("wacky.txt", "r");

while (ch != EOF) // 首次使用ch時,它的值尚未確定

{

ch = getc(fp);// 獲取輸入

putchar(ch);  // 處理輸入

}

第1個問題是,ch首次與EOF比較時,其值尚未確定。第2個問題是,如果getc返回EOF,該循環會把EOF作為一個有效字符處理。這些問題都可以解決。例如,把ch初始化為一個啞值(dummy value),再把一個if語句加入到循環中。但是,何必多此一舉,直接使用上面的設計範例即可。

其他輸入函數也會用到這種處理方案,它們在讀到文件結尾時也會返回一個錯誤信號(EOF 或 NULL指針)。

13.2.5 fclose函數

fclose(fp)函數關閉fp指定的文件,必要時刷新緩衝區。對於較正式的程序,應該檢查是否成功關閉文件。如果成功關閉,fclose函數返回0,否則返回EOF:

if (fclose(fp) != 0)

printf("Error in closing file %s\n", argv[1]);

如果磁盤已滿、移動硬盤被移除或出現I/O錯誤,都會導致調用fclose函數失敗。

13.2.6 指向標準文件的指針

stdio.h頭文件把3個文件指針與3個標準文件相關聯,C程序會自動打開這3個標準文件。如表13.2所示:

表13.2 標準文件和相關聯的文件指針

這些文件指針都是指向FILE的指針,所以它們可用作標準I/O函數的參數,如fclose(fp)中的fp。接下來,我們用一個程序示例創建一個新文件,並寫入內容。

13.3 一個簡單的文件壓縮程序

下面的程序示例把一個文件中選定的數據拷貝到另一個文件中。該程序同時打開了兩個文件,以"r"模式打開一個,以"w"模式打開另一個。該程序(程序清單13.2)以保留每3個字符中的第1個字符的方式壓縮第1個文件的內容。最後,把壓縮後的文本存入第2個文件。第2個文件的名稱是第1個文件名加上.red後綴(此處的red代表reduced)。使用命令行參數,同時打開多個文件,以及在原文件名後面加上後綴,都是相當有用的技巧。這種壓縮方式有限,但是也有它的用途(很容易把該程序改成用標準 I/O 而不是命令行參數提供文件名)。

程序清單13.2 reducto.c程序

// reducto.c –把文件壓縮成原來的1/3!

#include <stdio.h>

#include <stdlib.h>// 提供 exit的原型

#include <string.h>// 提供 strcpy、strcat的原型

#define LEN 40

int main(int argc, char *argv )

{

FILE *in, *out;  // 聲明兩個指向 FILE 的指針

int ch;

char name[LEN];  // 儲存輸出文件名

int count = 0;

// 檢查命令行參數

if (argc < 2)

{

fprintf(stderr, "Usage: %s filename\n", argv[0]);

exit(EXIT_FAILURE);

}

// 設置輸入

if ((in = fopen(argv[1], "r")) == NULL)

{

fprintf(stderr, "I couldn't open the file \"%s\"\n",

argv[1]);

exit(EXIT_FAILURE);

}

// 設置輸出

strncpy(name, argv[1], LEN - 5);// 拷貝文件名

name[LEN - 5] = '\0';

strcat(name, ".red");  // 在文件名後添加.red

if ((out = fopen(name, "w")) == NULL)

{// 以寫模式打開文件

fprintf(stderr, "Can't create output file.\n");

exit(3);

}

// 拷貝數據

while ((ch = getc(in)) != EOF)

if (count++ % 3 == 0)

putc(ch, out);// 打印3個字符中的第1個字符

// 收尾工作

if (fclose(in) != 0 || fclose(out) != 0)

fprintf(stderr, "Error in closing files\n");

return 0;

}

假設可執行文件名是reducto,待讀取的文件名為eddy,該文件中包含下面一行內容:

So even Eddy came oven ready.

命令如下:

reducto eddy

待寫入的文件名為eddy.red。該程序把輸出顯示在eddy.red中,而不是屏幕上。打開eddy.red,內容如下:

Send money

該程序示例演示了幾個編程技巧。我們來仔細研究一下。

fprintf和 printf類似,但是 fprintf的第 1 個參數必須是一個文件指針。程序中使用stderr指針把錯誤消息發送至標準錯誤,C標準通常都這麼做。

為了構造新的輸出文件名,該程序使用strncpy把名稱eddy拷貝到數組name中。參數LEN-5為.red後綴和末尾的空字符預留了空間。如果argv[2]字符串比LEN-5長,就拷貝不了空字符。出現這種情況時,程序會添加空字符。調用strncpy後,name中的第1個空字符在調用strcat函數時,被.red的.覆蓋,生成了eddy.red。程序中還檢查了是否成功打開名為eddy.red的文件。這個步驟在一些環境中相當重要,因為像strange.c.red這樣的文件名可能是無效的。例如,在傳統的DOS環境中,不能在後綴名後面添加後綴名(MS-DOS使用的方法是用.red替換現有後綴名,所以strange.c將變成strange.red。例如,可以用strchr函數定位(如果有的話),然後只拷貝點前面的部分即可)。

該程序同時打開了兩個文件,所以我們要聲明兩個 FIFL 指針。注意,程序都是單獨打開和關閉每個文件。同時打開的文件數量是有限的,這個限制取決於系統和實現,範圍一般是10~20。相同的文件指針可以處理不同的文件,前提是這些文件不需要同時打開。

13.4 文件I/O:fprintf、fscanf、fgets和fputs

前面章節介紹的I/O函數都類似於文件I/O函數。它們的主要區別是,文件I/O函數要用FILE指針指定待處理的文件。與 getc、putc類似,這些函數都要求用指向 FILE 的指針(如,stdout)指定一個文件,或者使用fopen的返回值。

13.4.1 fprintf和fscanf函數

文件I/O函數fprintf和fscanf函數的工作方式與printf和scanf類似,區別在於前者需要用第1個參數指定待處理的文件。我們在前面用過fprintf。程序清單13.3演示了這兩個文件I/O函數和rewind函數的用法。

程序清單13.3 addaword.c程序

/* addaword.c -- 使用 fprintf、fscanf 和 rewind */

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define MAX 41

int main(void)

{

FILE *fp;

char words[MAX];

if ((fp = fopen("wordy", "a+")) == NULL)

{

fprintf(stdout, "Can't open \"wordy\" file.\n");

exit(EXIT_FAILURE);

}

puts("Enter words to add to the file; press the #");

puts("key at the beginning of a line to terminate.");

while ((fscanf(stdin, "%40s", words) == 1) && (words[0] != '#'))

fprintf(fp, "%s\n", words);

puts("File contents:");

rewind(fp);  /* 返回到文件開始處 */

while (fscanf(fp, "%s", words) == 1)

puts(words);

puts("Done!");

if (fclose(fp) != 0)

fprintf(stderr, "Error closing file\n");

return 0;

}

該程序可以在文件中添加單詞。使用"a+"模式,程序可以對文件進行讀寫操作。首次使用該程序,它將創建wordy文件,以便把單詞存入其中。隨後再使用該程序,可以在wordy文件後面添加單詞。雖然"a+"模式只允許在文件末尾添加內容,但是該模式下可以讀整個文件。rewind函數讓程序回到文件開始處,方便while循環打印整個文件的內容。注意,rewind接受一個文件指針作為參數。

下面是該程序在UNIX環境中的一個運行示例(可執行程序已重命名為addword):

$ addaword

Enter words to add to the file; press the Enter

key at the beginning of a line to terminate.

The fabulous programmer

#

File contents:

The

fabulous

programmer

Done!

$ addaword

Enter words to add to the file; press the Enter

key at the beginning of a line to terminate.

enchanted the

large

#

File contents:

The

fabulous

programmer

enchanted

the

large

Done!

如你所見,fprintf和 fscanf的工作方式與 printf和 scanf類似。但是,與 putc不同的是,fprintf和fscanf函數都把FILE指針作為第1個參數,而不是最後一個參數。

13.4.2 fgets和fputs函數

第11章時介紹過fgets函數。它的第1個參數和gets函數一樣,也是表示儲存輸入位置的地址(char * 類型);第2個參數是一個整數,表示待輸入字符串的大小 [1];最後一個參數是文件指針,指定待讀取的文件。下面是一個調用該函數的例子:

fgets(buf, STLEN, fp);

這裡,buf是char類型數組的名稱,STLEN是字符串的大小,fp是指向FILE的指針。

fgets函數讀取輸入直到第 1 個換行符的後面,或讀到文件結尾,或者讀取STLEN-1 個字符(以上面的 fgets為例)。然後,fgets在末尾添加一個空字符使之成為一個字符串。字符串的大小是其字符數加上一個空字符。如果fgets在讀到字符上限之前已讀完一整行,它會把表示行結尾的換行符放在空字符前面。fgets函數在遇到EOF時將返回NULL值,可以利用這一機制檢查是否到達文件結尾;如果未遇到EOF則之前返回傳給它的地址。

fputs函數接受兩個參數:第1個是字符串的地址;第2個是文件指針。該函數根據傳入地址找到的字符串寫入指定的文件中。和 puts函數不同,fputs在打印字符串時不會在其末尾添加換行符。下面是一個調用該函數的例子:

fputs(buf, fp);

這裡,buf是字符串的地址,fp用於指定目標文件。

由於fgets保留了換行符,fputs就不會再添加換行符,它們配合得非常好。如第11章的程序清單11.8所示,即使輸入行比STLEN長,這兩個函數依然處理得很好。

13.5 隨機訪問:fseek和ftell

有了 fseek函數,便可把文件看作是數組,在 fopen打開的文件中直接移動到任意字節處。我們創建一個程序(程序清單13.4)演示fseek和ftell的用法。注意,fseek有3個參數,返回int類型的值;ftell函數返回一個long類型的值,表示文件中的當前位置。

程序清單13.4 reverse.c程序

/* reverse.c -- 倒序顯示文件的內容 */

#include <stdio.h>

#include <stdlib.h>

#define CNTL_Z '\032' /* DOS文本文件中的文件結尾標記 */

#define SLEN 81

int main(void)

{

char file[SLEN];

char ch;

FILE *fp;

long count, last;

puts("Enter the name of the file to be processed:");

scanf("%80s", file);

if ((fp = fopen(file, "rb")) == NULL)

{  /* 只讀模式  */

printf("reverse can't open %s\n", file);

exit(EXIT_FAILURE);

}

fseek(fp, 0L, SEEK_END); /* 定位到文件末尾 */

last = ftell(fp);

for (count = 1L; count <= last; count++)

{

fseek(fp, -count, SEEK_END);  /* 回退 */

ch = getc(fp);

if (ch != CNTL_Z && ch != '\r') /* MS-DOS 文件 */

putchar(ch);

}

putchar('\n');

fclose(fp);

return 0;

}

下面是對一個文件的輸出:

Enter the name of the file to be processed:

Cluv

.C ni eno naht ylevol erom margorp a

ees reven llahs I taht kniht I

該程序使用二進制模式,以便處理MS-DOS文本和UNIX文件。但是,在使用其他格式文本文件的環境中可能無法正常工作。

注意

如果通過命令行環境運行該程序,待處理文件要和可執行文件在同一個目錄(或文件夾)中。如果在IDE中運行該程序,具體查找方案序因實現而異。例如,默認情況下,Microsoft Visual Studio 2012在源代碼所在的目錄中查找,而Xcode 4.6則在可執行文件所在的目錄中查找。

接下來,我們要討論3個問題:fseek和ftell函數的工作原理、如何使用二進制流、如何讓程序可移植。

13.5.1 fseek和ftell的工作原理

fseek的第1個參數是FILE指針,指向待查找的文件,fopen應該已打開該文件。

fseek的第2個參數是偏移量(offset)。該參數表示從起始點開始要移動的距離(參見表13.3列出的起始點模式)。該參數必須是一個long類型的值,可以為正(前移)、負(後移)或0(保持不動)。

fseek的第3個參數是模式,該參數確定起始點。根據ANSI標準,在stdio.h頭文件中規定了幾個表示模式的明示常量(manifest constant),如表13.3所示。

表13.3 文件的起始點模式

舊的實現可能缺少這些定義,可以使用數值0L、1L、2L分別表示這3種模式。L後綴表明其值是long類型。或者,實現可能把這些明示常量定義在別的頭文件中。如果不確定,請查閱實現的使用手冊或在線幫助。

下面是調用fseek函數的一些示例,fp是一個文件指針:

fseek(fp, 0L, SEEK_SET); // 定位至文件開始處

fseek(fp, 10L, SEEK_SET); // 定位至文件中的第10個字節

fseek(fp, 2L, SEEK_CUR); // 從文件當前位置前移2個字節

fseek(fp, 0L, SEEK_END); // 定位至文件結尾

fseek(fp, -10L, SEEK_END); // 從文件結尾處回退10個字節

對於這些調用還有一些限制,我們稍後再討論。

如果一切正常,fseek的返回值為0;如果出現錯誤(如試圖移動的距離超出文件的範圍),其返回值為-1。

ftell函數的返回類型是long,它返回的是當前的位置。ANSI C把它定義在stdio.h中。在最初實現的UNIX中,ftell通過返回距文件開始處的字節數來確定文件的位置。文件的第1個字節到文件開始處的距離是0,以此類推。ANSI C規定,該定義適用於以二進制模式打開的文件,以文件模式打開文件的情況不同。這也是程序清單13.4以二進制模式打開文件的原因。

下面,我們來分析程序清單13.4中的基本要素。首先,下面的語句:

fseek(fp, 0L, SEEK_END);

把當前位置設置為距文件末尾 0 字節偏移量。也就是說,該語句把當前位置設置在文件結尾。下一條語句:

last = ftell(fp);

把從文件開始處到文件結尾的字節數賦給last。

然後是一個for循環:

for (count = 1L; count <= last; count++)

{

fseek(fp, -count, SEEK_END); /* go backward */

ch = getc(fp);

}

第1輪迭代,把程序定位到文件結尾的第1個字符(即,文件的最後一個字符)。然後,程序打印該字符。下一輪迭代把程序定位到前一個字符,並打印該字符。重複這一過程直至到達文件的第1個字符,並打印。

13.5.2 二進制模式和文本模式

我們設計的程序清單13.4在UNIX和MS-DOS環境下都可以運行。UNIX只有一種文件格式,所以不需要進行特殊的轉換。然而MS-DOS要格外注意。許多MS-DOS編輯器都用Ctrl+Z標記文本文件的結尾。以文本模式打開這樣的文件時,C 能識別這個作為文件結尾標記的字符。但是,以二進制模式打開相同的文件時,Ctrl+Z字符被看作是文件中的一個字符,而實際的文件結尾符在該字符的後面。文件結尾符可能緊跟在Ctrl+Z字符後面,或者文件中可能用空字符填充,使該文件的大小是256的倍數。在DOS環境下不會打印空字符,程序清單13.4中就包含了防止打印Ctrl+Z字符的代碼。

二進制模式和文本模式的另一個不同之處是:MS-DOS用\r\n組合表示文本文件換行。以文本模式打開相同的文件時,C程序把\r\n「看成」\n。但是,以二進制模式打開該文件時,程序能看見這兩個字符。因此,程序清單13.4中還包含了不打印\r的代碼。通常,UNIX文本文件既沒有Ctrl+Z,也沒有\r,所以這部分代碼不會影響大部分UNIX文本文件。

ftell函數在文本模式和二進制模式中的工作方式不同。許多系統的文本文件格式與UNIX的模型有很大不同,導致從文件開始處統計的字節數成為一個毫無意義的值。ANSI C規定,對於文本模式,ftell返回的值可以作為fseek的第2個參數。對於MS-DOS,ftell返回的值把\r\n當作一個字節計數。

13.5.3 可移植性

理論上,fseek和ftell應該符合UNIX模型。但是,不同系統存在著差異,有時確實無法做到與UNIX模型一致。因此,ANSI對這些函數降低了要求。下面是一些限制。

在二進制模式中,實現不必支持SEEK_END模式。因此無法保證程序清單13.4的可移植性。移植性更高的方法是逐字節讀取整個文件直到文件末尾。C 預處理器的條件編譯指令(第 16 章介紹)提供了一種系統方法來處理這種情況。

在文本模式中,只有以下調用能保證其相應的行為。

不過,許多常見的環境都支持更多的行為。

13.5.4 fgetpos和fsetpos函數

fseek和 ftell潛在的問題是,它們都把文件大小限制在 long 類型能表示的範圍內。也許 20億字節看起來相當大,但是隨著存儲設備的容量迅猛增長,文件也越來越大。鑒於此,ANSI C新增了兩個處理較大文件的新定位函數:fgetpos和 fsetpos。這兩個函數不使用 long 類型的值表示位置,它們使用一種新類型:fpos_t(代表file position type,文件定位類型)。fpos_t類型不是基本類型,它根據其他類型來定義。fpos_t 類型的變量或數據對象可以在文件中指定一個位置,它不能是數組類型,除此之外,沒有其他限制。實現可以提供一個滿足特殊平台要求的類型,例如,fpos_t可以實現為結構。

ANSI C定義了如何使用fpos_t類型。fgetpos函數的原型如下:

int fgetpos(FILE * restrict stream, fpos_t * restrict pos);

調用該函數時,它把fpos_t類型的值放在pos指向的位置上,該值描述了文件中的一個位置。如果成功,fgetpos函數返回0;如果失敗,返回非0。

fsetpos函數的原型如下:

int fsetpos(FILE *stream, const fpos_t *pos);

調用該函數時,使用pos指向位置上的fpos_t類型值來設置文件指針指向該值指定的位置。如果成功,fsetpos函數返回0;如果失敗,則返回非0。fpos_t類型的值應通過之前調用fgetpos獲得。

13.6 標準I/O的機理

我們在前面學習了標準I/O包的特性,本節研究一個典型的概念模型,分析標準I/O的工作原理。

通常,使用標準I/O的第1步是調用fopen打開文件(前面介紹過,C程序會自動打開3種標準文件)。fopen函數不僅打開一個文件,還創建了一個緩衝區(在讀寫模式下會創建兩個緩衝區)以及一個包含文件和緩衝區數據的結構。另外,fopen返回一個指向該結構的指針,以便其他函數知道如何找到該結構。假設把該指針賦給一個指針變量fp,我們說fopen函數「打開一個流」。如果以文本模式打開該文件,就獲得一個文本流;如果以二進制模式打開該文件,就獲得一個二進制流。

這個結構通常包含一個指定流中當前位置的文件位置指示器。除此之外,它還包含錯誤和文件結尾的指示器、一個指向緩衝區開始處的指針、一個文件標識符和一個計數(統計實際拷貝進緩衝區的字節數)。

我們主要考慮文件輸入。通常,使用標準I/O的第2步是調用一個定義在stdio.h中的輸入函數,如fscanf、getc或 fgets。一調用這些函數,文件中的數據塊就被拷貝到緩衝區中。緩衝區的大小因實現而異,一般是512字節或是它的倍數,如4096或16384(隨著計算機硬盤容量越來越大,緩衝區的大小也越來越大)。最初調用函數,除了填充緩衝區外,還要設置fp所指向的結構中的值。尤其要設置流中的當前位置和拷貝進緩衝區的字節數。通常,當前位置從字節0開始。

在初始化結構和緩衝區後,輸入函數按要求從緩衝區中讀取數據。在它讀取數據時,文件位置指示器被設置為指向剛讀取字符的下一個字符。由於stdio.h系列的所有輸入函數都使用相同的緩衝區,所以調用任何一個函數都將從上一次函數停止調用的位置開始。

當輸入函數發現已讀完緩衝區中的所有字符時,會請求把下一個緩衝大小的數據塊從文件拷貝到該緩衝區中。以這種方式,輸入函數可以讀取文件中的所有內容,直到文件結尾。函數在讀取緩衝區中的最後一個字符後,把結尾指示器設置為真。於是,下一次被調用的輸入函數將返回EOF。

輸出函數以類似的方式把數據寫入緩衝區。當緩衝區被填滿時,數據將被拷貝至文件中。

13.7 其他標準I/O函數

ANSI標準庫的標準I/O系列有幾十個函數。雖然在這裡無法一一列舉,但是我們會簡要地介紹一些,讓讀者對它們有一個大概的瞭解。這裡列出函數的原型,表明函數的參數和返回類型。我們要討論的這些函數,除了setvbuf,其他函數均可在ANSI之前的實現中使用。參考資料V的「新增C99和C11的標準ANSI C庫」中列出了全部的ANSI C標準I/O包。

13.7.1 int ungetc(int c, FILE *fp)函數

int ungetc函數把c指定的字符放回輸入流中。如果把一個字符放回輸入流,下次調用標準輸入函數時將讀取該字符(見圖13.2)。例如,假設要讀取下一個冒號之前的所有字符,但是不包括冒號本身,可以使用 getchar或 getc函數讀取字符到冒號,然後使用 ungetc函數把冒號放回輸入流中。ANSI C標準保證每次只會放回一個字符。如果實現允許把一行中的多個字符放回輸入流,那麼下一次輸入函數讀入的字符順序與放回時的順序相反。

圖13.2 ungets函數

13.7.2 int fflush函數

fflush函數的原型如下:

int fflush(FILE *fp);

調用fflush函數引起輸出緩衝區中所有的未寫入數據被發送到fp指定的輸出文件。這個過程稱為刷新緩衝區。如果 fp是空指針,所有輸出緩衝區都被刷新。在輸入流中使用fflush函數的效果是未定義的。只要最近一次操作不是輸入操作,就可以用該函數來更新流(任何讀寫模式)。

13.7.3 int setvbuf函數

setvbuf函數的原型是:

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

setvbuf函數創建了一個供標準I/O函數替換使用的緩衝區。在打開文件後且未對流進行其他操作之前,調用該函數。指針fp識別待處理的流,buf指向待使用的存儲區。如果buf的值不是NULL,則必須創建一個緩衝區。例如,聲明一個內含1024個字符的數組,並傳遞該數組的地址。然而,如果把NULL作為buf的值,該函數會為自己分配一個緩衝區。變量size告訴setvbuf數組的大小(size_t是一種派生的整數類型,第5章介紹過)。mode的選擇如下:_IOFBF表示完全緩衝(在緩衝區滿時刷新);_IOLBF表示行緩衝(在緩衝區滿時或寫入一個換行符時);_IONBF表示無緩衝。如果操作成功,函數返回0,否則返回一個非零值。

假設一個程序要儲存一種數據對象,每個數據對象的大小是3000字節。可以使用setvbuf函數創建一個緩衝區,其大小是該數據對像大小的倍數。

13.7.4 二進制I/O:fread和fwrite

介紹fread和fwrite函數之前,先要瞭解一些背景知識。之前用到的標準I/O函數都是面向文本的,用於處理字符和字符串。如何要在文件中保存數值數據?用 fprintf函數和%f轉換說明只是把數值保存為字符串。例如,下面的代碼:

double num = 1./3.;

fprintf(fp,"%f", num);

把num儲存為8個字符:0.333333。使用%.2f轉換說明將其儲存為4個字符:0.33,用%.12f轉換說明則將其儲存為 14 個字符:0.333333333333。改變轉換說明將改變儲存該值所需的空間數量,也會導致儲存不同的值。把 num 儲存為 0.33 後,讀取文件時就無法將其恢復為更高的精度。一般而言, fprintf把數值轉換為字符數據,這種轉換可能會改變值。

為保證數值在儲存前後一致,最精確的做法是使用與計算機相同的位組合來儲存。因此,double 類型的值應該儲存在一個 double 大小的單元中。如果以程序所用的表示法把數據儲存在文件中,則稱以二進制形式儲存數據。不存在從數值形式到字符串的轉換過程。對於標準 I/O,fread和 fwrite 函數用於以二進制形式處理數據(見圖13.3)。

實際上,所有的數據都是以二進制形式儲存的,甚至連字符都以字符碼的二進製表示來儲存。如果文件中的所有數據都被解釋成字符碼,則稱該文件包含文本數據。如果部分或所有的數據都被解釋成二進制形式的數值數據,則稱該文件包含二進制數據(另外,用數據表示機器語言指令的文件都是二進制文件)。

圖13.3 二進制輸出和文本輸出

二進制和文本的用法很容易混淆。ANSI C和許多操作系統都識別兩種文件格式:二進制和文本。能以二進制數據或文本數據形式存儲或讀取信息。可以用二進制模式打開文本格式的文件,可以把文本儲存在二進制形式的文件中。可以調用 getc拷貝包含二進制數據的文件。然而,一般而言,用二進制模式在二進制格式文件中儲存二進制數據。類似地,最常用的還是以文本格式打開文本文件中的文本數據(通常文字處理器生成的文件都是二進制文件,因為這些文件中包含了大量非文本信息,如字體和格式等)。

13.7.5 size_t fwrite函數

fwrite函數的原型如下:

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

fwrite函數把二進制數據寫入文件。size_t是根據標準C類型定義的類型,它是sizeof運算符返回的類型,通常是unsigned int,但是實現可以選擇使用其他類型。指針ptr是待寫入數據塊的地址。size表示待寫入數據塊的大小(以字節為單位),nmemb表示待寫入數據塊的數量。和其他函數一樣, fp指定待寫入的文件。例如,要保存一個大小為256字節的數據對像(如數組),可以這樣做:

char buffer[256];

fwrite(buffer, 256, 1, fp);

以上調用把一塊256字節的數據從buffer寫入文件。另舉一例,要保存一個內含10個double類型值的數組,可以這樣做:

double earnings[10];

fwrite(earnings, sizeof(double), 10, fp);

以上調用把earnings數組中的數據寫入文件,數據被分成10塊,每塊都是double的大小。

注意fwrite原型中的const void * restrict ptr聲明。fwrite的一個問題是,它的第1個參數不是固定的類型。例如,第1個例子中使用buffer,其類型是指向char的指針;而第2個例子中使用earnings,其類型是指向double的指針。在ANSI C函數原型中,這些實際參數都被轉換成指向void的指針類型,這種指針可作為一種通用類型指針(在ANSI C之前,這些參數使用char *類型,需要把實參強制轉換成char *類型)。

fwrite函數返回成功寫入項的數量。正常情況下,該返回值就是 nmemb,但如果出現寫入錯誤,返回值會比nmemb小。

13.7.6 size_t fread函數

size_t fread函數的原型如下:

size_t fread(void * restrict ptr, size_t size, size_t nmemb,FILE * restrict fp);

fread函數接受的參數和fwrite函數相同。在fread函數中,ptr是待讀取文件數據在內存中的地址,fp指定待讀取的文件。該函數用於讀取被fwrite寫入文件的數據。例如,要恢復上例中保存的內含10個double類型值的數組,可以這樣做:

double earnings[10];

fread(earnings, sizeof (double), 10, fp);

該調用把10個double大小的值拷貝進earnings數組中。

fread函數返回成功讀取項的數量。正常情況下,該返回值就是nmemb,但如果出現讀取錯誤或讀到文件結尾,該返回值就會比nmemb小。

13.7.7 int feof(FILE *fp)和int ferror(FILE *fp)函數

如果標準輸入函數返回 EOF,則通常表明函數已到達文件結尾。然而,出現讀取錯誤時,函數也會返回EOF。feof和ferror函數用於區分這兩種情況。當上一次輸入調用檢測到文件結尾時,feof函數返回一個非零值,否則返回0。當讀或寫出現錯誤,ferror函數返回一個非零值,否則返回0。

13.7.8 一個程序示例

接下來,我們用一個程序示例說明這些函數的用法。該程序把一系列文件中的內容附加在另一個文件的末尾。該程序存在一個問題:如何給文件傳遞信息。可以通過交互或使用命令行參數來完成,我們先採用交互式的方法。下面列出了程序的設計方案。

詢問目標文件的名稱並打開它。

使用一個循環詢問源文件。

以讀模式依次打開每個源文件,並將其添加到目標文件的末尾。

為演示setvbuf函數的用法,該程序將使用它指定一個不同的緩衝區大小。下一步是細化程序打開目標文件的步驟:

1.以附加模式打開目標文件;

2.如果打開失敗,則退出程序;

3.為該文件創建一個4096字節的緩衝區;

4.如果創建失敗,則退出程序。

與此類似,通過以下具體步驟細化拷貝部分:

1.如果該文件與目標文件相同,則跳至下一個文件;

2.如果以讀模式無法打開文件,則跳至下一個文件;

3.把文件內容添加至目標文件末尾。

最後,程序回到目標文件的開始處,顯示當前整個文件的內容。

作為練習,我們使用fread和fwrite函數進行拷貝。程序清單13.5給出了這個程序。

程序清單13.5 append.c程序

/* append.c -- 把文件附加到另一個文件末尾 */

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define BUFSIZE 4096

#define SLEN 81

void append(FILE *source, FILE *dest);

char * s_gets(char * st, int n);

int main(void)

{

FILE *fa, *fs;// fa 指向目標文件,fs 指向源文件

int files = 0; // 附加的文件數量

char file_app[SLEN];  // 目標文件名

char file_src[SLEN];  // 源文件名

int ch;

puts("Enter name of destination file:");

s_gets(file_app, SLEN);

if ((fa = fopen(file_app, "a+")) == NULL)

{

fprintf(stderr, "Can't open %s\n", file_app);

exit(EXIT_FAILURE);

}

if (setvbuf(fa, NULL, _IOFBF, BUFSIZE) != 0)

{

fputs("Can't create output buffer\n", stderr);

exit(EXIT_FAILURE);

}

puts("Enter name of first source file (empty line to quit):");

while (s_gets(file_src, SLEN) && file_src[0] != '\0')

{

if (strcmp(file_src, file_app) == 0)

fputs("Can't append file to itself\n", stderr);

else if ((fs = fopen(file_src, "r")) == NULL)

fprintf(stderr, "Can't open %s\n", file_src);

else

{

if (setvbuf(fs, NULL, _IOFBF, BUFSIZE) != 0)

{

fputs("Can't create input buffer\n", stderr);

continue;

}

append(fs, fa);

if (ferror(fs) != 0)

fprintf(stderr, "Error in reading file %s.\n",

file_src);

if (ferror(fa) != 0)

fprintf(stderr, "Error in writing file %s.\n",

file_app);

fclose(fs);

files++;

printf("File %s appended.\n", file_src);

puts("Next file (empty line to quit):");

}

}

printf("Done appending.%d files appended.\n", files);

rewind(fa);

printf("%s contents:\n", file_app);

while ((ch = getc(fa)) != EOF)

putchar(ch);

puts("Done displaying.");

fclose(fa);

return 0;

}

void append(FILE *source, FILE *dest)

{

size_t bytes;

static char temp[BUFSIZE]; // 只分配一次

while ((bytes = fread(temp, sizeof(char), BUFSIZE, source)) > 0)

fwrite(temp, sizeof(char), bytes, dest);

}

char * s_gets(char * st, int n)

{

char * ret_val;

char * find;

ret_val = fgets(st, n, stdin);

if (ret_val)

{

find = strchr(st, '\n');  // 查找換行符

if (find)  // 如果地址不是NULL,

*find = '\0'; // 在此處放置一個空字符

else

while (getchar != '\n')

continue;

}

return ret_val;

}

如果setvbuf無法創建緩衝區,則返回一個非零值,然後終止程序。可以用類似的代碼為正在拷貝的文件創建一塊4096字節的緩衝區。把NULL作為setvbuf的第2個參數,便可讓函數分配緩衝區的存儲空間。

該程序獲取文件名所用的函數是 s_gets,而不是 scanf,因為 scanf會跳過空白,因此無法檢測到空行。該程序還用s_gets代替fgets,因為後者在字符串中保留換行符。

以下代碼防止程序把文件附加在自身末尾:

if (strcmp(file_src, file_app) == 0)

fputs("Can't append file to itself\n",stderr);

參數file_app表示目標文件名,file_src表示正在處理的文件名。

append函數完成拷貝任務。該函數使用fread和fwrite一次拷貝4096字節,而不是一次拷貝1字節:

void append(FILE *source, FILE *dest)

{

size_t bytes;

static char temp[BUFSIZE]; // 只分配一次

while ((bytes = fread(temp, sizeof(char), BUFSIZE, source)) > 0)

fwrite(temp, sizeof(char), bytes, dest);

}

因為是以附加模式打開由 dest 指定的文件,所以所有的源文件都被依次添加至目標文件的末尾。注意,temp數組具有靜態存儲期(意思是在編譯時分配該數組,不是在每次調用append函數時分配)和塊作用域(意思是該數組屬於它所在的函數私有)。

該程序示例使用文本模式的文件。使用"ab+"和"rb"模式可以處理二進制文件。

13.7.9 用二進制I/O進行隨機訪問

隨機訪問是用二進制I/O寫入二進制文件最常用的方式,我們來看一個簡短的例子。程序清單13.6中的程序創建了一個儲存double類型數字的文件,然後讓用戶訪問這些內容。

程序清單13.6 randbin.c程序

/* randbin.c -- 用二進制I/O進行隨機訪問 */

#include <stdio.h>

#include <stdlib.h>

#define ARSIZE 1000

int main

{

double numbers[ARSIZE];

double value;

const char * file = "numbers.dat";

int i;

long pos;

FILE *iofile;

// 創建一組 double類型的值

for (i = 0; i < ARSIZE; i++)

numbers[i] = 100.0 * i + 1.0 / (i + 1);

// 嘗試打開文件

if ((iofile = fopen(file, "wb")) == NULL)

{

fprintf(stderr, "Could not open %s for output.\n", file);

exit(EXIT_FAILURE);

}

// 以二進制格式把數組寫入文件

fwrite(numbers, sizeof(double), ARSIZE, iofile);

fclose(iofile);

if ((iofile = fopen(file, "rb")) == NULL)

{

fprintf(stderr,

"Could not open %s for random access.\n", file);

exit(EXIT_FAILURE);

}

// 從文件中讀取選定的內容

printf("Enter an index in the range 0-%d.\n", ARSIZE - 1);

while (scanf("%d", &i) == 1 && i >= 0 && i < ARSIZE)

{

pos = (long) i * sizeof(double);// 計算偏移量

fseek(iofile, pos, SEEK_SET);  // 定位到此處

fread(&value, sizeof(double), 1, iofile);

printf("The value there is %f.\n", value);

printf("Next index (out of range to quit):\n");

}

// 完成

fclose(iofile);

puts("Bye!");

return 0;

}

首先,該程序創建了一個數組,並在該數組中存放了一些值。然後,程序以二進制模式創建了一個名為numbers.dat的文件,並使用fwrite把數組中的內容拷貝到文件中。內存中數組的所有double類型值的位組合(每個位組合都是64位)都被拷貝至文件中。不能用文本編輯器讀取最後的二進制文件,因為無法把文件中的值轉換成字符串。然而,儲存在文件中的每個值都與儲存在內存中的值完全相同,沒有損失任何精確度。此外,每個值在文件中也同樣佔用64位存儲空間,所以可以很容易地計算出每個值的位置。

程序的第 2 部分用於打開待讀取的文件,提示用戶輸入一個值的索引。程序通過把索引值和 double類型值佔用的字節相乘,即可得出文件中的一個位置。然後,程序調用fseek定位到該位置,用fread讀取該位置上的數據值。注意,這裡並未使用轉換說明。fread從已定位的位置開始,拷貝8字節到內存中地址為&value的位置。然後,使用printf顯示value。下面是該程序的一個運行示例:

Enter an index in the range 0-999.

500

The value there is 50000.001996.

Next index (out of range to quit):

900

The value there is 90000.001110.

Next index (out of range to quit):

0

The value there is 1.000000.

Next index (out of range to quit):

-1

Bye!

13.8 關鍵概念

C程序把輸入看作是字節流,輸入流來源於文件、輸入設備(如鍵盤),或者甚至是另一個程序的輸出。類似地,C程序把輸出也看作是字節流,輸出流的目的地可以是文件、視頻顯示等。

C 如何解釋輸入流或輸出流取決於所使用的輸入/輸出函數。程序可以不做任何改動地讀取和存儲字節,或者把字節依次解釋成字符,隨後可以把這些字符解釋成普通文本以用文本表示數字。類似地,對於輸出,所使用的函數決定了二進制值是被原樣轉移,還是被轉換成文本或以文本表示數字。如果要在不損失精度的前提下保存或恢復數值數據,請使用二進制模式以及fread和fwrite函數。如果打算保存文本信息並創建能在普通文本編輯器查看的文本,請使用文本模式和函數(如getc和fprintf)。

要訪問文件,必須創建文件指針(類型是FILE *)並把指針與特定文件名相關聯。隨後的代碼就可以使用這個指針(而不是文件名)來處理該文件。

要重點理解C如何處理文件結尾。通常,用於讀取文件的程序使用一個循環讀取輸入,直至到達文件結尾。C 輸入函數在讀過文件結尾後才會檢測到文件結尾,這意味著應該在嘗試讀取之後立即判斷是否是文件結尾。可以使用13.2.4節中「設計範例」中的雙文件輸入模式。

13.9 本章小結

對於大多數C程序而言,寫入文件和讀取文件必不可少。為此,絕大對數C實現都提供底層I/O和標準高級I/O。因為ANSI C庫考慮到可移植性,包含了標準I/O包,但是未提供底層I/O。

標準 I/O 包自動創建輸入和輸出緩衝區以加快數據傳輸。fopen函數為標準 I/O 打開一個文件,並創建一個用於存儲文件和緩衝區信息的結構。fopen函數返回指向該結構的指針,其他函數可以使用該指針指定待處理的文件。feof和ferror函數報告I/O操作失敗的原因。

C把輸入視為字節流。如果使用fread函數,C把輸入看作是二進制值並將其儲存在指定存儲位置。如果使用fscanf、getc、fgets或其他相關函數,C則將每個字節看作是字符碼。然後fscanf和scanf函數嘗試把字符碼翻譯成轉換說明指定的其他類型。例如,輸入一個值23,%f轉換說明會把23翻譯成一個浮點值,%d轉換說明會把23翻譯成一個整數值,%s轉換說明則會把23儲存為字符串。getc和 fgetc系列函數把輸入作為字符碼儲存,將其作為單獨的字符保存在字符變量中或作為字符串儲存在字符數組中。類似地,fwrite將二進制數據直接放入輸出流,而其他輸出函數把非字符數據轉換成用字符表示後才將其放入輸出流。

ANSI C提供兩種文件打開模式:二進制和文本。以二進制模式打開文件時,可以逐字節讀取文件;以文本模式打開文件時,會把文件內容從文本的系統表示法映射為C表示法。對於UNIX和Linux系統,這兩種模式完全相同。

通常,輸入函數getc、fgets、fscanf和fread都從文件開始處按順序讀取文件。然而, fseek和ftell函數讓程序可以隨機訪問文件中的任意位置。fgetpos和fsetpos把類似的功能擴展至更大的文件。與文本模式相比,二進制模式更容易進行隨機訪問。

13.10 複習題

複習題的參考答案在附錄A中。

1.下面的程序有什麼問題?

int main(void)

{

int * fp;

int k;

fp = fopen("gelatin");

for (k = 0; k < 30; k++)

fputs(fp, "Nanette eats gelatin.");

fclose("gelatin");

return 0;

}

2.下面的程序完成什麼任務?(假設在命令行環境中運行)

#include <stdio.h>

#include <stdlib.h>

#include <ctype.h>

int main(int argc, char *argv )

{

int ch;

FILE *fp;

if (argc < 2)

exit(EXIT_FAILURE);

if ((fp = fopen(argv[1], "r")) == NULL)

exit(EXIT_FAILURE);

while ((ch = getc(fp)) != EOF)

if (isdigit(ch))

putchar(ch);

fclose(fp);

return 0;

}

3.假設程序中有下列語句:

#include <stdio.h>

FILE * fp1,* fp2;

char ch;

fp1 = fopen("terky", "r");

fp2 = fopen("jerky", "w");

另外,假設成功打開了兩個文件。補全下面函數調用中缺少的參數:

a.ch = getc;

b.fprintf( ,"%c\n", );

c.putc( , );

d.fclose; /* 關閉terky文件 */

4.編寫一個程序,不接受任何命令行參數或接受一個命令行參數。如果有一個參數,將其解釋為文件名;如果沒有參數,使用標準輸入(stdin)作為輸入。假設輸入完全是浮點數。該程序要計算和報告輸入數字的算術平均值。

5.編寫一個程序,接受兩個命令行參數。第1個參數是字符,第2個參數是文件名。要求該程序只打印文件中包含給定字符的那些行。

注意

C程序根據'\n'識別文件中的行。假設所有行都不超過256個字符,你可能會想到用fgets。

6.二進制文件和文本文件有何區別?二進制流和文本流有何區別?

7.

a.分別用fprintf和fwrite儲存8238201有何區別?

b.分別用putc和fwrite儲存字符S有何區別?

8.下面語句的區別是什麼?

printf("Hello, %s\n", name);

fprintf(stdout, "Hello, %s\n", name);

fprintf(stderr, "Hello, %s\n", name);

9."a+"、"r+"和"w+"模式打開的文件都是可讀寫的。哪種模式更適合用來更改文件中已有的內容?

13.11 編程練習

1.修改程序清單13.1中的程序,要求提示用戶輸入文件名,並讀取用戶輸入的信息,不使用命令行參數。

2.編寫一個文件拷貝程序,該程序通過命令行獲取原始文件名和拷貝文件名。盡量使用標準I/O和二進制模式。

3.編寫一個文件拷貝程序,提示用戶輸入文本文件名,並以該文件名作為原始文件名和輸出文件名。該程序要使用 ctype.h 中的 toupper函數,在寫入到輸出文件時把所有文本轉換成大寫。使用標準I/O和文本模式。

4.編寫一個程序,按順序在屏幕上顯示命令行中列出的所有文件。使用argc控制循環。

5.修改程序清單13.5中的程序,用命令行界面代替交互式界面。

6.使用命令行參數的程序依賴於用戶的內存如何正確地使用它們。重寫程序清單 13.2 中的程序,不使用命令行參數,而是提示用戶輸入所需信息。

7.編寫一個程序打開兩個文件。可以使用命令行參數或提示用戶輸入文件名。

a.該程序以這樣的順序打印:打印第1個文件的第1行,第2個文件的第1行,第1個文件的第2行,第2個文件的第2行,以此類推,打印到行數較多文件的最後一行。

b.修改該程序,把行號相同的行打印成一行。

8.編寫一個程序,以一個字符和任意文件名作為命令行參數。如果字符後面沒有參數,該程序讀取標準輸入;否則,程序依次打開每個文件並報告每個文件中該字符出現的次數。文件名和字符本身也要一同報告。程序應包含錯誤檢查,以確定參數數量是否正確和是否能打開文件。如果無法打開文件,程序應報告這一情況,然後繼續處理下一個文件。

9.修改程序清單 13.3 中的程序,從 1 開始,根據加入列表的順序為每個單詞編號。當程序下次運行時,確保新的單詞編號接著上次的編號開始。

10.編寫一個程序打開一個文本文件,通過交互方式獲得文件名。通過一個循環,提示用戶輸入一個文件位置。然後該程序打印從該位置開始到下一個換行符之前的內容。用戶輸入負數或非數值字符可以結束輸入循環。

11.編寫一個程序,接受兩個命令行參數。第1個參數是一個字符串,第2個參數是一個文件名。然後該程序查找該文件,打印文件中包含該字符串的所有行。因為該任務是面向行而不是面向字符的,所以要使用fgets而不是getc。使用標準C庫函數strstr(11.5.7節簡要介紹過)在每一行中查找指定字符串。假設文件中的所有行都不超過255個字符。

12.創建一個文本文件,內含20行,每行30個整數。這些整數都在0~9之間,用空格分開。該文件是用數字表示一張圖片,0~9表示逐漸增加的灰度。編寫一個程序,把文件中的內容讀入一個20×30的int數組中。一種把這些數字轉換為圖片的粗略方法是:該程序使用數組中的值初始化一個20×31的字符數組,用值0 對應空格字符,1 對應點字符,以此類推。數字越大表示字符所佔的空間越大。例如,用#表示9。每行的最後一個字符(第31個)是空字符,這樣該數組包含了20個字符串。最後,程序顯示最終的圖片(即,打印所有的字符串),並將結果儲存在文本文件中。例如,下面是開始的數據:

0 0 9 0 0 0 0 0 0 0 0 0 5 8 9 9 8 5 2 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 9 0 0 0 0 0 0 0 5 8 9 9 8 5 5 2 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 5 8 1 9 8 5 4 5 2 0 0 0 0 0 0 0 0 0

0 0 0 0 9 0 0 0 0 0 0 0 5 8 9 9 8 5 0 4 5 2 0 0 0 0 0 0 0 0

0 0 9 0 0 0 0 0 0 0 0 0 5 8 9 9 8 5 0 0 4 5 2 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 5 8 9 1 8 5 0 0 0 4 5 2 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 5 8 9 9 8 5 0 0 0 0 4 5 2 0 0 0 0 0

5 5 5 5 5 5 5 5 5 5 5 5 5 8 9 9 8 5 5 5 5 5 5 5 5 5 5 5 5 5

8 8 8 8 8 8 8 8 8 8 8 8 5 8 9 9 8 5 8 8 8 8 8 8 8 8 8 8 8 8

9 9 9 9 0 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 3 9 9 9 9 9 9 9

8 8 8 8 8 8 8 8 8 8 8 8 5 8 9 9 8 5 8 8 8 8 8 8 8 8 8 8 8 8

5 5 5 5 5 5 5 5 5 5 5 5 5 8 9 9 8 5 5 5 5 5 5 5 5 5 5 5 5 5

0 0 0 0 0 0 0 0 0 0 0 0 5 8 9 9 8 5 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 5 8 9 9 8 5 0 0 0 0 6 6 0 0 0 0 0 0

0 0 0 0 2 2 0 0 0 0 0 0 5 8 9 9 8 5 0 0 5 6 0 0 6 5 0 0 0 0

0 0 0 0 3 3 0 0 0 0 0 0 5 8 9 9 8 5 0 5 6 1 1 1 1 6 5 0 0 0

0 0 0 0 4 4 0 0 0 0 0 0 5 8 9 9 8 5 0 0 5 6 0 0 6 5 0 0 0 0

0 0 0 0 5 5 0 0 0 0 0 0 5 8 9 9 8 5 0 0 0 0 6 6 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 5 8 9 9 8 5 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 5 8 9 9 8 5 0 0 0 0 0 0 0 0 0 0 0 0

根據以上描述選擇特定的輸出字符,最終輸出如下:

13.用變長數組(VLA)代替標準數組,完成編程練習12。

14.數字圖像,尤其是從宇宙飛船發回的數字圖像,可能會包含一些失真。為編程練習12添加消除失真的函數。該函數把每個值與它上下左右相鄰的值作比較,如果該值與其周圍相鄰值的差都大於1,則用所有相鄰值的平均值(四捨五入為整數)代替該值。注意,與邊界上的點相鄰的點少於4個,所以做特殊處理。

[1].注意,字符串大小和字符串長度不同。前者指該字符串佔用多少空間,後者指該字符串的字符個數。——譯者注