第十一章 檔案(File)
11-1 檔案的基本運作- fopen(),fclose(),getc(),putc() 11-2 標準輸出/入檔案- stdin,stdout,stderr 11-3 區段 I/O 與字串 I/O- fread(),fwrite(),fgets(),fputs() 11-4 隨機存取函數-fseek(),ftell() 11-5 其他的檔案控制 11-6 低階檔案的運作 11-7 程式觀摩
C語言中對於檔案操作的方式,分成兩種。 一種是把檔案看成是由一個個字元串起來的資料集合,稱為資料流 (data stream) 的觀點,在程式內以檔案指標 (FILE *) 的資料型態來代表所欲操作的檔案,一般稱為高階的檔案操作。 另一種是由 UNIX 作業系統對檔案的控制方式而來,在程式內以檔案描述值 (file descriptor) 來代表,對 MSDOS 而言,就是用檔案代號 (file handle) 來指定,這種系統層次的檔案控制,一般稱為低階的檔案操作。 對初學者來說,高階的檔案操作較為重要,而且是最常見的檔案使用方式,本章主要討論高階檔案的各種運作方式及相關的函數。
11-1 檔案的基本運作-fopen(),fclose(),getc(),putc()
對C語言的程式來說,檔案是由一個個字元所串起來的資料集合,這些字元可能是代表文字或數字,也可能是機器碼或是代表一個圖形的幾個點。不管是原始程式檔、資料檔,或是可執行檔、影像圖形檔,對 C語言而言,都是相同的檔案概念。唯一不同的,是這些檔案的使用方式。 以程式的觀點來看,這個檔案是輸入資料用的(可讀)、或是輸出資料用的(可寫),甚或是既當輸入又當輸出(可讀可寫)。另一個重要的區別是,這個檔案是本文檔 (Text file) 或是二進位檔 (Binary file)。
本文檔是指檔案內容是由一些文字符號或數字所構成,這些文數字都是ASCII 代碼來表示,所以又稱為 ASCII 檔,例如書信、C 原始程式本身(xxx.c)、批次檔 (xxx.bat) 等,所有能用 MSDOS 的 type命令顯示出來的檔案,都是 ASCII檔。 二進位檔則是指檔案內容是一個個的二進位數字,例如以一個位元組的八個位元來代表八個黑白點(1是黑,0是白),這就是影像或圖形檔。數字如果直接以二進位格式存入檔案,會比用文數字來代表,節省一些空間(整數佔兩個位元組、浮點數佔四個位元組),這也是二進位檔。其他如 .COM 檔與 .EXE 檔案,都是二進位檔。
┌───┬─────────┬───────┐ │ │ Text File │ Binary File │ │ │ 本 文 檔 │ 二 進 位 檔 │ ├───┼─────────┼───────┤ │內 容│可印出的 ASCII 碼 │任何二進位資料│ │換 行│ CR 與 LF │ 只有 CR │ │ '\n' │ (OxOD 與 OxOA) │ │ │檔 案│ Ctrl-Z │ 只能根據 │ │結 束│ (即 Ox1A) │ 檔案長度 │ └───┴─────────┴───────┘ 圖 11-1-1 本文檔與二進位檔
開檔-fopen() ┌─────────────────┐ │ #include <stdio.h> │ │ FILE *fopen (檔案名稱,模式) │ └─────────────────┘ 其中檔案名稱是一個字串,就是 MSDOS 的檔案名稱例如 “autoexec.bat”,甚至可包括磁碟及路徑名稱,但是要注意斜線\需用脫序字元來表示,例如 “C:\\autoexec.bat”。模式是一個字串,指定這個檔案的使用模式。 常用的模式如下: "r" 開啟後,只可讀取資料 (read) "w" 開啟後,可寫入資料 (write) "a" 添加資料 (append)
如果要指定用二進位模式 (binary) 開檔,要附加一個 b,使用“rb”,“wb” 及 “ab” 模式來開檔。 欲指定用本文模式 (text) 開檔,則附加一個 t,使用 "rt","wt","at"。除了特殊的控制之外,不附加 t 或 b,會視為本文模式,所以有時會省略 t,只用 "r",'w","a"。 開檔函數 fopen() 會傳回一個檔案指標的值,如果傳回值為虛指標NULL,表示開檔不成功,通常是因為欲打開一個不存在的檔案或檔案系統資訊損壞所造成。 注意,打開一個檔案準備寫入("w" 或 "wb" 模式)時,如果該檔名已存在,則舊資料會被清除掉。
關檔-fclose() ┌─────────────┐ │ #include <stdio.h> │ │ int fclose(FILE *fptr) │ └─────────────┘ 檔案打開之後,不管是讀出資料或寫入資料,最後都一定要關閉,這樣才完成檔案使用的動作。如果檔案使用之後,不進行關檔的步驟,很可能會造成檔案資料的流失或損壞。 其中的 fptr 為一檔案指標,是由先前的開檔函數 fopen()傳回的檔案指標。
從檔案讀出一個字元-getc() ┌─────────────┐ │ #include <stdio.h> │ │ int getc (FILE *fptr) │ └─────────────┘ 其中的 fptr 是所欲讀取資料流的檔案指標。getc()會傳回所讀取字元的 ASCII碼,如果遇到檔案結束的時候,會傳回一個特殊值 EOF。
寫入一個字元到某個檔案-putc() ┌───────────────┐ │ #include <stdio.h> │ │ int putc(int ch,FILE *fptr) │ └───────────────┘ 其中的 ch 是所欲寫入的字元,而 fptr 是所指定的檔案指標。當然,先前的開檔 fopen() 函數應指定那個檔案是準備寫入資料的。 另外,fgetc() 與 fputc() 的效果分別和 getc() 與 putc() 相同,只是 fgetc() 與 fputc() 是用函數做成,而 getc() 與 putc() 是用巨集寫成罷了。 這些高階的檔案控制方式,不管是輸入資料或輸出資料,都是透過緩衝區 (Buffer) 來工作,所以又稱為緩衝區式的輸出入控制 (Buffer I/O)。
程式 讀取 H e l o \n B y 磁碟 檔案目前位置 圖 11-1-2 檔案的資料流觀點
【範例 11-1-1】 這個程式可以將一般的文書檔案內容,顯示到螢幕上,也就是相當於DOS 命令的 type。 7 FILE *fptr; 8 char fname[80]="ex11-1-1.c"; /* type this file */ 9 int ch; 10 11 fptr = fopen(fname,"r"); 12 if (fptr == NULL) { 13 printf("file %s open error",fname); 14 exit(1); 15 } 16 while ((ch=getc(fptr)) != EOF) 17 putchar(ch); 18 fclose(fptr);
【範例 11-1-2】 本程式將鍵盤敲入的文句,儲存到檔案之中。欲儲存的檔案名稱在程式執行時,才由使用者指定,輸入資料時,以按下 Ctrl-Z 鍵做為檔案結束字元。 12 printf("Enter filename to build : "); 13 gets( fname ); 14 fptr = fopen(fname,"w"); 15 if (fptr == NULL) { 16 printf("file %s open error",fname); 17 exit(1); 18 } 19 printf("Enter your message ended by CTRL-Z\n"); 20 while ((ch=getchar()) != EOF) 21 putc(ch,fptr); 22 fclose(fptr);
11-2 標準輸出/入檔案-stdin,stdout,stderr 任何一個C程式開始執行後,系統會自動替這個程式打開三個檔案,並指定固定的檔案指標給這三個檔案,程式中不必撰寫這些檔案的打開及關閉的動作。 這三個檔案稱為標準輸出入檔案 (standard input output file) ,其檔案指標分別是 stdin(標準輸入)、stdout(標準輸出)、以及stderr(標準錯誤輸出),它們都在 <stdio.h>標頭檔中宣告,型態都是FILE *(檔案指標)。
一般而言,stdin 是代表鍵盤輸入,stdout 是螢幕輸出,stderr 則是固定為螢幕輸出。所有指定從 stdin 檔案輸入的函數,都會去讀取鍵盤的資料,而指定寫出到 stdout 檔案的輸出函數,都將其結果顯示在螢幕上,這兩種標準輸出入檔案可以重新導向到其他檔案,稍後會加以介紹,而 stderr 通常用於印出重要的錯誤警示訊息,所以固定輸出到螢幕上,不能重新導向。 事實上,我們慣用的字元輸出入動作 getchar() 與 putchar(),是由 getc() 與 putc(),指定從 stdin 輸入及從 stdout 輸出來定義,也就是說它們是巨集而非函數
【輸出入重導向(I/O redirection)】 在使用標準輸出入函數的時候,我們可以指定輸出入重導向,將原先從鍵盤輸入的資料,改由指定的檔案讀取資料。而原本顯示在螢幕上的輸出訊息,改成寫入到指定的檔案中加以保存。對於每次試驗程式效果,卻需要輸入較多資料時,這是較簡便的辦法。 在 MSDOS 的系統提示下,例如: A:>try <data.doc >result.out 表示程式 try.exe 原先欲由鍵盤輸入的資料,改由 data.doc 檔去讀取,而將顯示在螢幕的標準輸出,改成寫入到 result.doc 檔案中。如果將result.out 改成裝置名稱 PRN,就會將結果送到印表機印出來了。命令行中,<data.doc 的 < 符號代表輸入重導向,而 >result.out 的 >符號代表輸出重導向。
11-3區段 I/O與字串 I/O fread(),fwrite(),fgets(),fputs() 相對於字元 I/O 函數一次讀寫一個字元,區段 I/O 函數每次可以從檔案讀取一個區段 (Block) 的資料,或是寫一個區段資料到輸出檔案內。 所謂的一個區段是指連續的許多字元(或位元組),通常是一個陣列的資料,一筆以結構來定義的員工紀錄資料,或是一塊緩衝區的記憶體資料。也可以在使用二進位格式儲存的檔案中,讀取一個整數或一個影像單元或浮點數的資料。
區段資料的讀取,使用 fread()函數,而寫入區段資料到檔案中則使用 fwrite() 函數,其語法如下: #include <stdio.h> size_t fread(void *buffer,size_t size,size_t n,FILE *fp) size_t fwrite(void *buffer,size_t size,size_t n,FILE *fp) 其中的 size_t 是資料型態,通常是 unsigned int,buffer 是指向資料儲存的緩衝區,size 是一個資料項目所佔的位元組個數,n 是欲讀取的項目個數,fp則是指定輸入或輸出的檔案指標。
例如, numread = fread(buffer,sizeof(char),80,fpin); 就是從檔案指標 fpin 所指的檔案中,一次讀取 80 個項目,每個項目佔一個 char 型態大小,所以總共讀取 80 個位元組,到 buffer(一個字元陣列)中,fread()會傳回實際上所讀取的資料項個數。 同理, numwrite = fwrite(buffer,sizeof(char),80,fpout); 可將 80 個位元組的連續資料寫入 fpout 所指向的檔中。
【範例 11-3-2】 本程式採用區段拷貝 (Block copy) 的作法,將某個檔案的內容,逐一讀入各個區段,拷貝到另一個檔案去。值得注意的是,fwrite寫入的資料項數,是由 fread所實際讀取的項數,而非固定的區段大小,因為檔案長度不見得剛好是區段的整數倍。 25 void filecopy( FILE *fpin, FILE *fpout ) 26 { 27 char buffer[128]; 28 int numread; 29 30 while ((numread=fread(buffer,sizeof(char),128,fpin)) > 0) 31 fwrite(buffer,sizeof(char),numread,fpout); 32 }
8 FILE *fptr1,*fptr2; 9 char fname1[80],fname2[80]; 12 printf("Enter source filename : "); 13 gets( fname1 ); 14 fptr1 = fopen(fname1,"r"); 15 if (fptr1==NULL) {printf("%s open error",fname1); exit(1);} 16 printf("Enter target filename : "); 17 gets( fname2 ); 18 fptr2 = fopen(fname2,"w"); 19 if (fptr2==NULL) {printf("%s open error",fname2); exit(2);} 20 filecopy( fptr1, fptr2 ); 21 fclose(fptr1); 22 fclose(fptr2);
如果想要以一列為單位來讀寫檔案,則使用字串 I/O 函數,fgets()及fputs() 。其語法如下: #include <stdio.h> char *fgets(char *line, int maxline, FILE *fp) int fputs(char *line,FILE *fp) fgets() 函數會從檔案 fp 讀取一串字元(以換行 '\n' 結尾)將字串存入字元陣到 line 中,最多只讀取 maxline 個字元。若讀取成功,傳回line 的位址,不成功或遇到檔案終了,則傳回 NULL。 fputs() 函數則將字串 line 寫入檔案 fp 中,字串必須附有結尾零值字元,fputs() 不會自動添加換行符號。
11-4 隨機存取函數- fseek(),ftell() 一般的資料流檔案控制,都是以循序存取 (sequential access) 的方式,從檔案開頭端依序讀取一筆筆資料,直到檔案終了為止。如果想要一下子跳到指定的檔案位置,也就是進行隨機存取 (random access) 的動作,就必須使用隨機存取函數 fseek()。
#include <stdio.h> int fseek(FILE *fp, long offset, int origin) fseek() 函數將 fp 檔案現在的讀寫位置,移動到以 origin 為起點距離 offset 的位置。origin 的值有三種可能,使用 <stdio.h> 所定義的三個常數值來代表起點的選擇。 值 符號常數 檔 案 位 置 ──────────────────────── 0 SEEK_SET 表示由 "檔案開頭" 算起 1 SEEK_CUR 表示由 "檔案目前讀寫位置" 算起 2 SEEK_END 表示由 "檔案結尾" 算起
我們可以使用 ftell() 函數,來得知目前該檔案的讀取位置,ftell() 會傳回由檔案開頭算起的位元組個數,語法如下: #include <stdio.h> long int ftell (FILE *fp)
11-5 其他的檔案控制 對於帶格式的檔案輸入及輸出,可以使用 fscanf() 函數及fprintf() 函數來處理。fscanf() 與 fprintf() 分別對應於標準輸入及輸出的 scanf() 與 printf(),只是需指定所欲輸入或輸出的對象而已。
#include <stdio.h> int fscanf (FILE *fp, char *format,…) int fprintf (FILE *fp, char *format,…) 它們的用法,類似於 scanf() 與 printf(),只需注意第一個引數是個檔案指標,指明所要輸出入的檔案,而格式字串則是第二個引數,其他引數視格式字串而定。
【命令列引數 argc,argv】 實際的程式應用情況,經常是執行程式時,直接指定使用的檔案。對MSDOS 來說,就是在系統提示符號的命令行,將檔案名稱視為引數,傳回到主程式之中,亦即把主程式 main() 看成普通的函數,只不過其引數來源是取自命令行而已。如此,程式 copy.exe 就可以在命令列下直接使用 A:>copy file1.in file2.out 指定將file1.in 檔案的內容拷貝到 file2.out 檔案去,不必在程式執行時,以提示方式來讀取鍵入的檔案名稱。
命令行引數,通常採取兩個特殊的變數,寫法如下: main ( int argc, char *argv [] ) 其中的 argc 是個整數變數,代表連程式名稱本身計算在內的引數個數(argument count),而 argv 是個字元指標的陣列,它的每一個元素都是一個指標來指向引數的字串。所以,上述的 copy 命令執行時,程式內的 argc 值為 3,而 argv 的內容為 ┌──┐ argv[0] │ ‧ ┼─> "copy" 完整的檔案路徑名稱 ├──┤ argv[1] │ ‧ ┼─> "file1.in" argv[2] │ ‧ ┼─> "file2.out" └──┘
【吐回字元─ungetc()】 有時候,我們讀取檔案的字元資料,會先讀取一個字元再判斷該字元是否為有效字元,遇到非有效字元時才停止。例如,程式中用 getc() 來連續讀取一個不事先知道長度的整數數字,這件事會持續至讀進一個非十進位數字(例如:英文字母)為止。這個英文字母應屬於下一項的資料,但是已經被預先 "讀進" 來了,這時可使用 ungetc() 將這個溢讀的字元"吐回"原來的檔案之中,但是只限於具有緩衝區的檔案控制。其語法如下: #include <stdio.h> int ungetc (int ch, FILE *fp) ch 是欲吐回的字元,fp 是指定的檔案指標。如果 "吐回" 失敗,ungetc() 會傳回 EOF 表示錯誤,否則傳回 ch 的值。
【刪除檔案─unlink()】 如果想在程式中直接刪除某個 MSDOS 的檔案,可使用 unlink() 函數,其語法格式如下: #include <stdio.h> 或 <io.h> 或 <dos.h> int unlink (char *filename) 其中 filename 是所欲刪除的檔案路徑名稱,若刪除成功,unlink() 會傳回 0,否則傳回 -1 的值表示錯誤發生。
11-6 低階檔案的運作 Turbo C 的程式館中,包括了下列三類不同運作方法的 I/O 函數: 資料流I/O 函數 11-6 低階檔案的運作 Turbo C 的程式館中,包括了下列三類不同運作方法的 I/O 函數: 資料流I/O 函數 低階檔案 I/O 函數 控制台及 I/O 埠函數。
所謂的資料流 I/O 函數,就是在處理 I/O 動作時,將檔案看成是一個具有緩衝區的連續位元組,這就是我們前面各節所說明的高階檔案的運作方式。緩衝區是讀入資料流,或寫出資料流位元組的資料暫存區,緩衝區的個數及大小是由系統來配置,必要時,可經由 config.sys 系統配備檔來設定。
低階檔案 I/O 函數,類似於資料流 I/O 函數,只是不具有緩衝區。程式執行讀寫動作時,將直接由磁碟或檔案所代表的 I/O裝置,讀取或寫出指定個數的位元組資料。因為輸出入的動作不帶格式化,所以較適合用於二進位格式檔的 I/O 運作。另外,由於磁碟的存取較費時,所以使用低階檔案 I/O 時,通常會一次讀寫許多位元組,以減少磁碟存取的次數,提高程式的效率。
控制台 (console) 及 I/O 埠 (port) 函數則是對 PC 所連接的鍵盤、螢幕、滑鼠或通訊用的序列埠 (serial port) 等周邊設備,直接進行I/O 的動作。所謂的控制台,輸入時是指鍵盤,輸出時是指螢幕而言,這些 I/O動作,都不需要打開檔案就可以直接使用。第三章的基本輸出入函數,都屬於此類。
【常用的Turbo C I/O 函數】 功 能 控 制 台 I/O 資 料 流 I/O 低 階 I/O 功 能 控 制 台 I/O 資 料 流 I/O 低 階 I/O ────── ──────── ─────── ───── 產生、開啟一個檔案 不必 fopen() creat(),open() 關閉一個檔案 不必 fclose() close() 帶格式的讀取 scanf() fscanf() 無 帶格式的輸出 printf() fprintf() 無 讀取一個字元 getchar(),getch()getche() fgetc(),getc() 無 輸出一個字元 putchar(),putc() fputc(),putc() 無 讀取一行存入字串 gets() fgets() 無 寫出一字串 puts() fputs() 無 設定讀/寫位置 無 fseek() lseek() 取得目前的讀/寫位置 無 ftell() tell() 二進位讀取 無 fread() read() 二進位寫出 無 fwrite() write() 清除緩衝區 無 fflush(),flushall() 無 檢查檔案終了 EOF 常數 feof() eof() 將字元吐回緩衝區 ungetch() ungetc() 無 刪除一個檔案 無 unlink() unlink()
低階檔案的運作,是透過檔案代號 (file handle) 的指定,來說明資料讀寫的檔案對象。使用 open() 函數開啟一個檔案後,會傳回一個整數值的檔案代號,接下來的讀取資料 read(),寫出資料 write(),關閉檔案案 close(),都需指明這個檔案代號,就好像高階檔案的檔案指標用法一般,只不過檔案代號是個整數值,而非指標罷了。常用的低階 I/O 函數,語法如下: #include <io.h> #include <fcntl.h> #include <sys\stat.h> int creat (char *filename, int amode) int open (char *filename, int access [,unsigned amode]) int close (int handle) int read (int handle, viod *buffer, unsigned length) int write (int handle, viod *buffer, unsigned length) long lseek (int handle, long offset, int from where)
第三個引數 amode只有第二個引數指定了 O_CREAT 時,才需要設定。 欲打開一個檔案,可使用 open()函數 int open (char *filename, int access [,unsigned amode]) 如果指定該檔案是供寫入資料用的,而該檔案尚未存在,就會自動產生一個新檔案。其中,access是存取旗號,指定檔案的使用方式,其值為下列之一用 | 符號("位元或"運算子)所連結而成。 O_APPEND 添加資料在檔尾 O_BINARY 二進位檔的格式 O_TEXT 文字檔的格式 O_RDONLY 指定只能讀取 O_WRONLY 指定只能寫入 O_RDWR 讀寫皆可 O_CREAT 舊檔名不存在時,產生一個新檔並打開供寫入用 第三個引數 amode只有第二個引數指定了 O_CREAT 時,才需要設定。
【範例 11-6-1】 這個程式採用低階檔案的處理方式,將一個檔案的內容以區段拷貝的做法,複製到另一個檔案去。讀者應將本例與範例 11-3-2 相互比較,就可了解低階檔案與高階檔案運用的異同。 1 /* Example 11-6-1 */ 2 /* low level file copy */ 3 4 #include <stdio.h> 5 #include <io.h> 6 #include <fcntl.h> 7 #include <sys\stat.h> 8 #define BUFSIZE 256 9 char buffer[BUFSIZE];
13 int fd1, fd2; 14 int numread; 20 if ((fd1=open(argv[1],O_RDONLY|O_BINARY)) == -1) { 21 printf("file %s open error\n",argv[1]); 22 exit(2); 23 } 24 if ((fd2=open(argv[2],O_CREAT|O_WRONLY|O_TRUNC|O_BINARY,S_IWRITE)) == -1) { 25 printf("file %s open error\n",argv[2]); 26 exit(2); 27 } 28 while ((numread=read(fd1,buffer,BUFSIZE)) > 0) 29 write(fd2,buffer,numread); 30 close(fd1); 31 close(fd2);
11-7 程式觀摩
【範例 11-7-1】 當我們想要知道某個檔案的內容時,常會用 type 之類的命令來進行,但是對於一般的二進位檔如. obj 或 【範例 11-7-1】 當我們想要知道某個檔案的內容時,常會用 type 之類的命令來進行,但是對於一般的二進位檔如 .obj 或 .exe 或其他的圖形檔,就無法適用。有許多的 DOS 工具可以讓我們觀察二進位檔的內容,例如 Debug、Pctools 或 Norton Utinlity等等。這個程式就是示範如何將二進位檔的內容以十六進位格式傾印 (dump) 出來。 程式的核心是一個 dumpploc 函數,其功能在於由 start指標開始的位址,傾印 len 個位元組的資料,每一行只印 16 個字元,包括十六進位碼及 ASCII字元分列於左右半面,對於無法印出的特殊字元,則以句點來顯示,程式第 51 列的 printf 格式使用 %*s,說明將 locbuf 以字串格式印出,共佔 j 格,請特別注意。檔案打開之後,用 fread 函數逐次讀入一個區段,再呼叫 dumpploc 函數來顯示區段的內容到螢幕上,直到檔案資料結束為止。如果修改本程式將傾印結果輸出另一檔案,可以成為一項方便好用的工具。
31 dumpploc( char *start, int len) 32 { 33 char locbuf[17]; 35 char *pnt; 37 pnt = start; 38 for (i=0,j=0 ; i<len ; j++,i++) { 39 printf("%02x ",(unsigned char)(locbuf[j] = *pnt++)); 40 if (locbuf[j] < 32 || locbuf[j] > 126) 41 locbuf[j]='.'; /* remark special ASCII codes */ 42 if (j == 15) { 43 locbuf[16] = 0; 44 printf(" %16s\n",locbuf); 45 j -= 16; 46 }
47 } 48 if (j > 0) { 49 for (i=0 ; i < (16-j) ; i++) printf(" "); 50 locbuf[j] = 0; 51 printf(" %*s\n",j,locbuf); 52 } 53 }