數據的輸入和輸出幾乎伴隨着每個 C 語言程序,所謂輸入就是從“源端”獲取數據,所謂輸出可以理解為向“終端”寫入數據。這里的源端可以是鍵盤、鼠標、硬盤、光盤、掃描儀等輸入設備,終端可以是顯示器、硬盤、打印機等輸出設備。在 C 語言中,把這些輸入和輸出設備也看作“文件”。
文件及其分類
計算機上的各種資源都是由操作系統管理和控制的,操作系統中的文件系統,是專門負責將外部存儲設備中的信息組織方式進行統一管理規划,以便為程序訪問數據提供統一的方式。
文件是操作系統管理數據的基本單位,文件一般是指存儲在外部存儲介質上的有名字的一系列相關數據的有序集合。它是程序對數據進行讀寫操作的基本對象。在 C 語言中,把輸入和輸出設備都看作文件。
文件一般包括三要素:文件路徑、文件名、后綴。
由於在 C 語言中 '\' 一般是轉義字符的起始標志,故在路徑中需要用兩個 '\' 表示路徑中目錄層次的間隔,也可以使用 '/' 作為路徑中的分隔符。
例如,"E:\\ch10.doc"或者"E:/ch10.doc",表示文件 ch10.doc 保存在 E 盤根目錄下。"f1.txt" 表示當前目錄下的文件 f1.txt。
文件路徑:可以顯式指出其絕對路徑,如上面的”E:\\”或者”E:/”等;如果沒有顯式指出其路徑,默認為當前路徑。
C 語言不僅支持對當前目錄和根目錄文件的操作,也支持對多級目錄文件的操作,例如:
D:\\C_WorkSpace\\Chapter_10\\file_1.txt
或者
D:/C_WorkSpace/Chapter_10/file_1.txt
中的 file_1.txt 均是 C 語言可操作的多級目錄文件。
文件名:標識文件名字的合法標識符,如 ch10、file_1 等都是合法的文件名。
后綴:一般用於標明文件的類型,使用方式為:文件名.后綴,即文件名與后綴之間用 '.' 隔開。常見的后綴類型有:doc、txt、dat、c、cpp、obj、exe、bmp、jpg 等。
C 語言中的輸入和輸出都是和文件相關的,即程序從文件中輸入(讀取)數據,程序向文件中輸出(寫入)數據。
文件按其邏輯結構可分為:記錄文件和流式文件。而記錄文件又可分為:順序文件、索引文件、索引順序文件及散列文件等。
流式文件是以字節為單位,對流式文件的訪問一般采用窮舉搜索的方式,效率不高,故一般需頻繁訪問的較大數據不適宜采用流式文件邏輯結構。但由於流式文件管理簡單,用戶可以較方便地對文件進行相關操作。
流的概念及分類
I/O 設備的多樣性及復雜性,給程序設計者訪問這些設備帶來了很大的難度和不便。為此,ANSIC 的 I/O 系統即標准 I/O 系統,把任意輸入的源端或任意輸出的終端,都抽象轉換成了概念上的“標准 I/O 設備”或稱“標准邏輯設備”。程序繞過具體設備,直接與該“標准邏輯設備”進行交互,這樣就為程序設計者提供了一個不依賴於任何具體 I/O 設備的統一操作接口,通常把抽象出來的“標准邏輯設備”或“標准文件”稱作“流”。
把任意 I/O 設備,轉換成邏輯意義上的“標准 I/O 設備”或“標准文件”的過程,並不需要程序設計者感知和處理,是由標准 I/O 系統自動轉換完成的。故從這個意義上,可以認為任意輸入的源端和任意輸出的終端均對應一個“流”。
流按方向分為:輸入流和輸出流。從文件獲取數據的流稱為輸入流,向文件輸出數據稱為輸出流。
例如,從鍵盤輸入數據然后把該數據輸出到屏幕上的過程,相當於從一個文件輸入流(與鍵盤相關)中輸入(讀取)數據,然后通過另外一個文件輸出流(與顯示器相關)把獲取的數據輸出(寫入)到文件(顯示器)上。
流按數據形式分為:文本流和二進制流。文本流是 ASCII 碼字符序列,而二進制流是字節序列。
文本文件與二進制文件
根據文件中數據的組織形式的不同,可以把文件分為:文本文件和二進制文件。
- 文本文件:把要存儲的數據當成一系列字符組成,把每個字符的 ASCII 碼值存入文件中。每個 ASCII 碼值占一個字節,每個字節表示一個字符。故文本文件也稱作字符文件或 ASCII 文件,是字符序列文件。
- 二進制文件:把數據對應的二進制形式存儲到文件中,是字節序列文件。
例如數據 123,如果按文本文件形式存儲,把數據看成三個字符:'1'、'2'、'3' 的集合,文件中依次存儲各個字符的 ASCII 碼值,格式如表 1 所示。
字符 | '1' | '2' | '3' |
---|---|---|---|
ASCII(十進制) | 49 | 50 | 51 |
ASCII(二進制) | 0011 0001 | 0011 0010 | 0011 0011 |
如果按照二進制文件形式存儲,則把數據 123 看成整型數,如果該系統中整型數占 4 個字節,則數據 123 二進制存儲形式的 4 個字節如下。
0000 0000 0000 0000 0000 0000 0111 1011
文件的打開與關閉
打開函數 fopen 的原型如下。
FILE * fopen(char *filename, char *mode);
函數參數:
1.filename:文件名,包括路徑,如果不顯式含有路徑,則表示當前路徑。例如,“D:\\f1.txt”表示 D 盤根目錄下的文件 f1.txt 文件。“f2.doc”表示當前目錄下的文件 f2.doc。
2.mode:文件打開模式,指出對該文件可進行的操作。常見的打開模式如 “r” 表示只讀,“w” 表示只寫,“rw” 表示讀寫,“a” 表示追加寫入。更多的打開模式如表 2 所示。
模式 | 含 義 | 說 明 |
---|---|---|
r | 只讀 | 文件必須存在,否則打開失敗 |
w | 只寫 | 若文件存在,則清除原文件內容后寫入;否則,新建文件后寫入 |
a | 追加只寫 | 若文件存在,則位置指針移到文件末尾,在文件尾部追加寫人,故該方式不 刪除原文件數據;若文件不存在,則打開失敗 |
r+ | 讀寫 | 文件必須存在。在只讀 r 的基礎上加 '+' 表示增加可寫的功能。下同 |
w+ | 讀寫 | 新建一個文件,先向該文件中寫人數據,然后可從該文件中讀取數據 |
a+ | 讀寫 | 在” a”模式的基礎上,增加可讀功能 |
rb | 二進制讀 | 功能同模式”r”,區別:b表示以二進制模式打開。下同 |
wb | 二進制寫 | 功能同模式“w”。二進制模式 |
ab | 二進制追加 | 功能同模式”a”。二進制模式 |
rb+ | 二進制讀寫 | 功能同模式"r+”。二進制模式 |
wb+ | 二進制讀寫 | 功能同模式”w+”。二進制模式 |
ab+ | 二進制讀寫 | 功能同模式”a+”。二進制模式 |
返回值:打開成功,返回該文件對應的 FILE 類型的指針;打開失敗,返回 NULL。故需定義 FILE 類型的指針變量,保存該函數的返回值。可根據該函數的返回值判斷文件打開是否成功。
關閉函數 fclose 的原型如下。
int fclose(FILE *fp);
函數參數:
fp:已打開的文件指針。
返回值:正常關閉,返回否則返回 EOF(-1)。
例如:
文件的順序讀寫
換字符輸入輸出
c 語言中提供了從文件中逐個輸入字符及向文件中逐個輸出字符的順序讀寫函數 fgetc 和 fputc 及調整文件讀寫位置到文件開始處的函數 rewind。這些函數均在標准輸入輸出頭文件 stdio.h 中。
字符輸入函數 fgetc 的函數原型為:
int fgetc (FILE *fp);
所在頭文件:<stdio.h>。
函數功能:從文件指針 fp 所指向的文件中輸入一個字符。輸入成功,返回該字符;已讀取到文件末尾,或遇到其他錯誤,即輸入失敗,則返回文本文件結束標志 EOF(EOF 在 stdio.h 中已定義,一般為 -1)。
注意:由於 fgetc 是以 unsigned char 的形式從文件中輸入(讀取)一個字節,並在該字節前面補充若干 0 字節,使之擴展為該系統中的一個 int 型數並返回,而非直接返回 char 型。當輸入失敗時返回文本文件結束標志 EOF 即 -1,也是整數。故返回類型應為 int 型,而非 char 型。
如果誤將返回類型定義為 char 型,文件中特殊字符的讀取可能會出現意想不到的邏輯錯誤。
由於在 C 語言中把除磁盤文件外的輸入輸出設備也當成文件處理,故從鍵盤輸入字符不僅可以使用宏 getchar() 實現,也可以使用 fgetc (stdin) 實現。其中,stdin 指向標准輸入設備—鍵盤所對應的文件。stdin 不需要人工調用函數 fopen 打開和 fclose 關閉。
字符輸出函數 fputc 的函數原型為:
int fputc (int c, FILE *fp);
所在頭文件:<stdio.h>
函數功能:向 fp 指針所指向的文件中輸出字符 c,輸出成功,返回該字符;輸出失敗,則返回 EOF(-1)。
向標准輸出設備屏幕輸出字符變量 ch 中保存的字符,不僅可以使用宏 putchar(ch) 實現,也可以使用 fputc (ch,stdout); 實現。其中,stdout 指向標准輸出設備—顯示器所對應的文件。stdout 也不需要人工調用函數 fopen 打開和 fclose 關閉。
對一個文件進行讀寫操作時,經常會把一個文件中讀寫位置重新調整到文件的開始處,可以使用函數 rewind 實現。
文件讀寫位置復位函數 rewind 的函數原型為:
void rewind (FILE *fp);
所在頭文件:<stdio.h>
函數功能:把 fp 所指向文件中的讀寫位置重新調整到文件開始處。
【例 1】從鍵盤輸入若干個字符,同時把這些字符輸出到 D 盤根目錄下的文件 data_file.txt 中及屏幕上。各個字符連續輸入,最后按下回車鍵結束輸入過程。
輸出結果為:
請輸入字符,按回車鍵結束:I love C
I love C
此時,查看 D 盤根目錄下生成的 data_file.txt 文件,並且其內容為 I love C。
接字符串輸入輸出
下面主要介紹文件中常見的字符串輸入、輸出函數 fgets 和 fputs。
字符串輸入函數 fgets 的函數原型為:
char * fgets (char *s, int size, FILE * fp);
所在頭文件:<stdio.h>
函數功能:從 fp 所指向的文件內,讀取若干字符(一行字符串),並在其后自動添加字符串結束標志 '\0' 后,存入 s 所指的緩沖內存空間中(s 可為字符數組名),直到遇到回車換行符或已讀取 size-1 個字符或已讀到文件結尾為止。該函數讀取的字符串最大長度為 size-1。
參數 fp:可以指向磁盤文件或標准輸入設備 stdin。
返回值:讀取成功,返回緩沖區地址 s;讀取失敗,返回 NULL。
說明:fgets 較之 gets 字符串輸入函數是比較安全規范的。因為 fgets 函數可由程序設計者自行指定輸入緩沖區 s 及緩沖區大小 size。即使輸入的字符串長度超過了預定的緩沖區大小,也不會因溢出而使程序崩潰,而是自動截取長度為 size-1 的串存入 s 指向的緩沖區中。
字符串輸出函數 fputs 的函數原型為:
int fputs (const char *str, FILE *fp);
所在頭文件:<stdio.h>
函數功能:把 str(str 可為字符數組名)所指向的字符串,輸出到 fp 所指的文件中。
返回值:輸出成功,返回非負數;輸出失敗,返回EOF(-1)。
【例 2】從鍵盤輸入若干字符串存入 D 盤根目錄下文件 file.txt 中,然后從該文件中讀取所有字符串並輸出到屏幕上。
運行結果為:
請輸入3個字符串:
字符串1:How are you going today?
字符串2:Never speak die.
字符串3:Good job!
How are you going today?
Never speak die.
Good job!
此時,D 盤目錄下已生成 file.txt 文件,其內容同輸出結果完全相同。
按格式化輸入輸出
文件操作中的格式化輸入輸出函數 fscanf 和 fprintf 一定意義上就是 scanf 和 printf 的文本版本。程序設計者可根據需要采用多種格式靈活處理各種類型的數據,如整型、字符型、浮點型、字符串、自定義類型等。
文件格式化輸入函數 fscanf 的函數原型為:
int fscanf (文件指針,格式控制串,輸入地址表列);
所在頭文件:<stdio.h>
函數功能:從一個文件流中執行格式化輸入,當遇到空格或者換行時結束。注意該函數遇到空格時也結束,這是其與 fgets 的區別,fgets 遇到空格不結束。
返回值:返回整型,輸入成功時,返回輸入的數據個數;輸入失敗,或已讀取到文件結尾處,返回 EOF(-1)。
故一般可根據該函數的返回值是否為 EOF 來判斷是否已讀到文件結尾處。
例如,若文件 f1.dat 中保存了若干整數,各整數之間用空格間隔,從文件中讀取兩個整數,依次保存到兩個整型變量中。程序代碼段如下。
文件格式化輸出函數 fprintf 的函數原型為:
int fprintf (文件指針,格式控制串,輸出表列);
所在頭文件:<stdio.h>
函數功能:把輸出表列中的數據按照指定的格式輸出到文件中。
返回值:輸出成功,返回輸出的字符數;輸出失敗,返回一負數。
例如,向當前目錄文件file.txt中輸入一個學生的姓名、學號和年齡,采用文本方式,參考代碼如下。
運行程序后,當前目錄下生成了 file.txt 文件,並且其內容為:
張三 20170304007 17
按二進制方式讀寫數據塊
接下來介紹按塊讀寫數據的函數 fread 和 fwrite,這兩個函數主要應用於對二進制文件的讀寫操作,不建議在文本文件中使用。接着介紹了 fread 讀取二進制文件時,判斷是否已經到達文件結尾的函數 feof。
數據塊讀取(輸入)函數 fread 的函數原型為:
unsigned fread (void *buf, unsigned size, unsigned count, FILE* fp);
所在頭文件:<stdio.h>
函數功能:從 fp 指向的文件中讀取 count 個數據塊,每個數據塊的大小為 size。把讀取到的數據塊存放到 buf 指針指向的內存空間中。
返回值:返回實際讀取的數據塊(非字節)個數,如果該值比 count 小,則說明已讀到文件尾或有錯誤產生。這時一般采用函數 feof 及 ferror 來輔助判斷。
函數參數:
- buf:指向存放數據塊的內存空間,該內存可以是數組空間,也可以是動態分配的內存。void類型指針,故可存放各種類型的數據,包括基本類型及自定義類型等。
- size:每個數據塊所占的字節數。
- count:預讀取的數據塊最大個數。
- fp:文件指針,指向所讀取的文件。
數據塊寫入(輸出)函數 fwrite 的函數原型為:
unsigned fwrite (const void *bufAunsigned size,unsigned count,FILE* fp);
所在頭文件:<stdio.h>
函數功能:將 buf 所指向內存中的 count 個數據塊寫入 fp 指向的文件中。每個數據塊的大小為 size。
返回值:返回實際寫入的數據塊(非字節)個數,如果該值比 count 小,則說明 buf 所指空間中的所有數據塊已寫完或有錯誤產生。這時一般采用 feof 及 ferror 來輔助判斷。
函數參數:
- buf:前加const的含義是buf所指的內存空間的數據塊只讀屬性,避免程序中有意或無意的修改。
- size:每個數據塊所占的字節數。
- count:預寫入的數據塊最大個數。
- fp:文件指針,指向所讀取的文件。
注意:使用 fread 和 fwrite 對文件讀寫操作時,一定要記住使用“二進制模式”打開文件,否則,可能會出現意想不到的錯誤。
在操作文件時,經常使用 feof 函數來判斷是否到達文件結尾。
feof 函數的函數原型為:
int feof (FILE * fp);
所在頭文件:<stdio.h>
函數功能:檢查 fp 所關聯文件流中的結束標志是否被置位,如果該文件的結束標志已被置位,返回非 0 值;否則,返回 0。
需要注意的是:
1) 在文本文件和二進制文件中,均可使用該函數判斷是否到達文件結尾。
2) 文件流中的結束標志,是最近一次調用輸入等相關函數(如 fgetc、fgets、fread 及 fseek 等)時設置的。只有最近一次操作輸入的是非有效數據時,文件結束標志才被置位;否則,均不置位。
【例 3】從鍵盤輸入若干名學生的姓名、學號、語數外三門課成績並計算平均成績,將這些學生信息以二進制方式保存到當前目錄文件 Stia_Info.dat 中。采用 fwrite 函數寫入數據。存儲空間要求采用數組形式。采用靜態數組形式,僅為了復習數組作為函數參數的情況,且便於理解,實際編程中不建議采用這種方案。
實現代碼為:
由於采用二進制形式存儲,故打開生成的二進制文件 Stu_Info.dat 可能是“亂碼”。通過判斷文件的生成以及文件中部分顯示正常的數據,可判斷代碼是否運行正確。
文件的隨機讀寫
以上介紹的都是文件的順序讀寫操作,即每次只能從文件頭開始,從前往后依次讀寫文件中的數據。在實際的程序設計中,經常需要從文件的某個指定位置處開始對文件進行選擇性的讀寫操作,這時,首先要把文件的讀寫位置指針移動到指定處,然后再進行讀寫,這種讀寫方式稱為對文件的隨機讀寫操作。
C 語言程序中常使用 rewind、fseek 函數移動文件讀寫位置指針。使用 ftell 獲取當前文件讀寫位置指針。
函數 fseek 的函數原型為:
int fseek(FI:LE *fp, long offset, int origin);
所在頭文件:<stdio.h>
函數功能:把文件讀寫指針調整到從 origin 基點開始偏移 offset 處,即把文件讀寫指針移動到 origin+offset 處。
函數參數:
1) origin:文件讀寫指針移動的基准點(參考點)。基准位置 origin 有三種常量取值:SEEK_SET、SEEK_CUR 和 SEEK_END,取值依次為 0,1,2。
SEEK_SET:文件開頭,即第一個有效數據的起始位置。
SEEK_CUR:當前位置。
SEEK_END:文件結尾,即最后一個有效數據之后的位置。注意:此處並不能讀取到最后一個有效數據,必須前移一個數據塊所占的字節數,使該文件流的讀寫指針到達最后一個有效數據塊的起始位置處。
2) offset:位置偏移量,為 long 型,當 offset 為正整數時,表示從基准 origin 向后移動 offset 個字節的偏移;若 offset 為負數,表示從基准 origin 向前移動 |offset| 個字節的偏移。
返回值:成功,返回 0;失敗,返回 -1。
例如,若 fp 為文件指針,則 seek (fp,10L,0); 把讀寫指針移動到從文件開頭向后 10 個字節處。 fSeek(fp,10L,1); 把讀寫指針移動到從當前位置向后 10 個字節處。 fseek(fp,-20L,2); 把讀寫指針移動到從文件結尾處向前 20 個字節處。
調用 fseek 函數時,第三個實參建議不要使用 0、1、2 等數字,最好使用可讀性較強的常量符號形式,使用如下格式取代上面三條語句。
fseek(fp,10L,SEEK_SET);
fseek(fp,10L,SEEK_CUR);
fseek(fp,-20L,SEEK_END);
函數 ftell 的函數原型:
long ftell (FILE *fp);
所在頭文件:<stdio.h>
函數功能:用於獲取當前文件讀寫指針相對於文件頭的偏移字節數。
例如,分析以下程序,輸出其運行結果。
運行結果為:
名字 年齡 職務
閃電 10 車管所職工
尼克 8 協警
兔朱迪 5 交通警察