文件打開之後,就可以對它進行讀寫了。這一節將介紹常用的讀寫函數。
1.正確選擇打開方式
【例22.9】下面的程序是將128個字符寫入文件,程序是否正確?
#include <stdio.h> #include <string.h> int main (void ) { unsigned char ch ; FILE *fp=NULL ; fp=fopen ("ttt.txt" ,"w" ); // 創建一個文件 for (ch=0 ; ch<128 ; ++ch ) fputc (ch ,fp ); fclose (fp ); return 0 ; }
【解答】程序輸出寫入的最後一個字符的16進制編碼是7f,好像驗證了確實寫入128個字符。其實,這裡是按文本文件方式創建了一個只寫文件,所以實際上會多寫一個回車符。要寫入128個,則不應寫入回車符,這就要創建二進制文件。
// 修改後使用讀入驗證的程序 #include <stdio.h> #include <string.h> int main (void ) { unsigned char ch ; int i=0 ; FILE *fp=NULL ; fp=fopen ("ttt.txt" ,"wb" ); // 創建一個二進制只寫文件 for (ch=0 ,i=1 ; ch<128 ; ++ch ,++i ) fputc (ch ,fp ); printf ("i = %d , ch = %x\n" ,i-1 , ch-1 ); fclose (fp ); i=0 ; fp=fopen ("ttt.txt" ,"rb" ); // 打開一個二進制只讀文件 while ( !feof (fp ) ) { ch = fgetc ( fp ); i++ ; } printf ("i = %d , ch = %x \n" , i , ch ); fclose (fp ); return 0 ; }
程序輸出結果如下:
i = 128 , ch = 7f i = 129 , ch = ff
上面程序加入驗證輸出信息,證明讀出128個字符,第129次讀入的是文件結束符ff,也就是EOF(-1),從而說明寫入的是128個二進制碼。
【例22.10】下面程序錯在哪裡?
#include <stdio.h> #include <stdlib.h> const char name[ ]="f :\ct3\new\ttt" ; int main (void ) { FILE *fp ; fp = fopen (name ,"w" ); if (fp == NULL ) { printf (" 創建文件%s 出錯!\n" , name ); exit (1 ); } fclose (fp ); return 0 ; }
【解答】字串"f:\\ct3\\new\\ttt"如果用在#include包含語句中,是正確表示文件路徑和文件名的方法。但這裡是用在程序語句中,反斜槓被用作轉義字符,\n表示換行。\new變成換行後輸出ew。\t是Tab鍵,它把\ttt分解為按Tab規定,輸出tt,即程序輸出結果為:
創建文件f :ct3 ew tt 出錯!
正確的DOS路徑和文件的表示應該為:
const char name="f :\\ct3\\new\\ttt" ;
改正後,程序在f:\ct3\new下面創建文件ttt。注意,一定要給出文件名,如果只給出文件夾,如"f:\\ct3\\new\\ttt",則將給出如下出錯信息:
創建文件f :\ct3\new\ 出錯!
2.文本文件的操作
【例22.11】下面程序存入文件的內容正確嗎?
#include<stdio.h> #include <stdlib.h> void main ( ) { FILE *fp ; char ch ,filename[10] ; printf ( "Enter a file name :" ); scanf ( " %s" , filename ); if ( ( fp = fopen ( filename ,"w" ) ) == NULL ) { printf ( " cannot open file %s\n" ,filename ); exit (1 ); } printf ( "Input :" ); // 輸入以字符# 作為結束符 while (1 ){ scanf ( " %c" , &ch ); if ( ch == '#' ) { printf ( "\nBye !\n" ); break ; } fputc (ch ,fp ); putchar (ch ); // 在屏幕上顯示出來 } fclose (fp ); }
【解答】不正確。scanf語句是以空格作為分界符的,即它不接受空格,所以存入文件的內容將是去除空格以後的內容,因此不合要求。用「ch=getchar();」語句代替它既可。
putchar(ch)用來在屏幕上顯示寫入文件的字符。這裡作為驗證的信息,實際程序中可以刪除。putchar函數就是從fputc函數派生出來的。putchar(c)是如下定義的宏。
#difine putchar (c ) fputc ( c ,stdout )
這裡的stdout是系統定義的文件指針變量,它與終端輸出相關聯。fputc(c,stdout)的作用是將c的值輸出到終端。用宏putchar(c)比書寫fputc(c,stdout)簡單一些。從用戶的角度看,可以把putchar(c)看做函數而不必嚴格地稱它為宏。
程序運行示範如下:
Enter a file name : t.txt Input : We are here ! We are here ! Go home !# Go home ! Bye !
fputc(ch,fp)函數把一個字符寫入磁盤文件中。其中,ch是要輸出的字符,它可以是一個字符常量,也可以是一個字符變量。fp是文件指針變量,它從fopen函數得到返回值。fputc函數的作用是將字符(ch的值)輸出到fp所指向的文件上去。fputc函數也帶回一個值,如果輸出成功,則返回值就是輸出的字符;如果輸出失敗,則返回EOF。EOF是在stdio.h文件中定義的符號常量,其值為-1。
可以將t.txt文件中的內容打印出來,以便證明在t.txt文件中已存入了輸入的信息。
【例22.12】將程序中的兩處調用fgetc簡化為一個。
#include <stdio.h> #include <stdlib.h> void main ( ) { FILE *fp ; char ch ; if ( ( fp = fopen ("t.txt" ,"r" ) ) == NULL ) { printf ( "cannot open infile\n" ); exit (1 ); } ch = fgetc ( fp ); while ( ch != EOF ) { putchar ( ch ); ch = fgetc ( fp ); } putchar ('\n' ); fclose (fp ); }
【解答】使用do~while循環即可,程序運行結果就是寫入t.txt文件的內容。
#include <stdio.h> #include <stdlib.h> void main ( ) { FILE *fp ; char ch ; if ( ( fp = fopen ("t.txt" ,"r" ) ) == NULL ) { printf ( "cannot open infile\n" ); exit (1 ); } do{ ch = fgetc ( fp ); putchar ( ch ); }while ( ch != EOF ); putchar ('\n' ); fclose (fp ); }
fgetc(fp)從指定文件讀入一個字符,該文件必須是以讀或讀寫方式打開。其中,fp為文件指針變量,ch為字符變量。fgetc函數帶回一個字符,賦給ch。如果在執行fgetc讀字符時遇到文件結束符,函數返回一個文件結束符標誌EOF。
注意:EOF不是可輸出字符,因此不能在屏幕上顯示。由於字符的ASCII碼不可能出現-1,因此EOF定義為-1是合適的。當讀入的字符值等於-1(EOF)時,表示讀入的已不是正常的字符而是文件結束符。
【例22.13】將一個磁盤文件中的信息複製到另一個磁盤文件中。
#include <stdio.h> #include <stdlib.h> void main ( ) { FILE *in , *out ; if ( ( in = fopen ("t.txt" ,"r" ) ) == NULL ) { printf ( "cannot open infile\n" ); exit (1 ); } if ( ( out = fopen ("out.txt" ,"w" ) ) == NULL ) { printf ( "cannot open outfile\n" ); exit (1 ); } while ( !feof (in )) fputc ( fgetc (in ),out ); fclose (in ); fclose (out ); }
【例22.14】用命令行的方式實現將一個磁盤文件中的信息複製到另一個磁盤文件中。
【解答】這時要用到main函數的參數,將它編寫在一個文件中,產生可執行文件之後,要在DOS環境下用命令行參數方式運行。
#include <stdio.h> #include <stdlib.h> void main (int argc , char *argv[ ] ) { FILE *in , *out ; if (argc !=3 ){ printf ( "You forgot to enter a filename\n" ); exit (1 ); } if ( ( in=fopen (argv[1] ,"r" )) == NULL ){ printf ( "cannot open infile\n" ); exit (1 ); } if ( (out=fopen (argv[2] ,"w" ) ) == NULL ){ printf ( "cannot open outfile\n" ); exit (1 ); } while ( !feof (in ) ) fputc ( fgetc (in ),out ); fclose (in ); fclose (out ); }
假若本程序的文件名為exam.c,經編譯連接後得到的可執行文件名為exam.exe,則可在DOS命令方式下,輸入以下的命令行。
C> exam file1.c file2.c
執行文件名後面的file1.c和file2.c兩個參數被分別放到指針數組argv〔1〕和argv〔2〕中,argv〔0〕的內容為exam,argc的值等於3(因為此命令行共有3個參數)。如果輸入的參數少於3個,則程序會輸出:「You forgot to enter a filename」。程序執行結果是將file1.c中的信息複製到file2.c中。如前所述,可以用type file1.c和type file2.c命令驗證。
最後說明一點:為了書寫方便,把fputc和fgetc定義為宏名putc和getc。
#define putc (ch ,fp ) fputc (ch ,fp ) #define getc (fp ) fget (fp )
這在stdio.h中已經定義。用putc和getc,跟用fputc和fget是一樣的。一般可以把它們作為相同的函數來對待。
3.二進制文件的操作
現在ANSI C已允許用緩衝文件系統處理二進制文件,而讀入某一個字節中的二進制數據的值有可能是-1,而這又恰好是EOF的值。這就出現了需要讀入有用數據卻被處理為文件結束的情況。為了解決這個問題,ANSI C提供一個feof函數來判斷文件是否真的結束。Feof(fp)用來測試fp所指向文件的當前狀態是否文件結束。如果是文件結束,函數Feof(fp)的值為1(真),否則為0(假)。
打開一個文件後,如果順序讀入一個二進制文件中的數據,可以用
while ( ! feof ( fp ) ) { c = fgetc ( fp ); …… }
判斷讀入文件是否結束。當沒有遇到文件結束時,feof(fp)的值為0,而!feof(fp)為1,則將讀入一個字節的數據賦給整型變量c(當然可以接著對這些數據進行所需處理)。直到遇到文件結束,feof(fp)的值為1,!feof(fp)的值為0,不再執行while循環。這種方法也適用於文本文件。
getc和putc函數可以用來讀寫文件中的一個字符。
【例22.15】找出下面程序中的錯誤。
#include <stdio.h> int main (void ) { char ch ,cs[16] , *str="How are you" ; int i=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"wb" ); // 創建一個二進制只寫文件 while (str[i] !='\0' ) { ch=str[i] ; putc (ch ,fp ); i++ ; } fp=fopen ("ttt.bin" ,"rb" ); // 打開一個二進制只讀文件 i=0 ; while ( feof (fp ) ) { cs[i] = getc ( fp ); // 讀入字符串到字符數組cs i++ ; } cs[i]='\0' ; fclose (fp ); printf ( "%s\n" , cs ); // 輸出How are you ? return 0 ; }
【解答】getc和putc函數用來讀寫文件中的一個字符,是正確的。但while(feof(fp))的用法不對,應改為while(!feof(fp))。修改後讀入字符數組的內容為空,這是因為沒有關閉寫入的文件造成的。要先關閉寫入的文件,再重新以只讀方式打開即可。另外,在文件最後寫入一個字符串結束標誌,讀文件時就不需要處理了,這樣更方便些。
// 改正的程序1 #include <stdio.h> int main (void ) { char ch ,cs[16] , *str="How are you ?" ; int i=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"wb" ); // 創建一個二進制只寫文件 while (str[i] !='\0' ) { ch=str[i] ; putc (ch ,fp ); i++ ; } putc ('\0' ,fp ); // 寫入字符串結束標誌 fclose (fp ); fp=fopen ("ttt.bin" ,"rb" ); // 打開一個二進制只讀文件 i=0 ; while ( !feof (fp ) ) { cs[i] = getc ( fp ); i++ ; } fclose (fp ); printf ( "%s\n" , cs ); return 0 ; }
另一種方法是第1次將文件以"rb+"方式建立一個可以讀寫的二進制文件。當寫完文件後,用rewind(fp)語句將文件指針回到文件開始處,即可讀文件。
// 改正的程序2 #include <stdio.h> int main (void ) { char ch ,cs[16] , *str="How are you ?" ; int i=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"rb+" ); // 創建一個二進制讀寫文件 while (str[i] !='\0' ) { ch=str[i] ; putc (ch ,fp ); i++ ; } putc ('\0' ,fp ); // 寫入字符串結束標誌 rewind (fp ); // 恢復到文件起點 i=0 ; while ( !feof (fp ) ) { cs[i] = getc ( fp ); i++ ; } fclose (fp ); printf ( "%s\n" , cs ); return 0 ; }
fread和fwrite函數是按數據塊的長度來處理輸入輸出的,一般用於二進制文件的輸入輸出,下面是改為使用它們的程序。
// 改正的程序3 #include <stdio.h> #include <string.h> int main (void ) { char cs[16] , *str="How are you ?" ; FILE *fp ; fp=fopen ("ttt.bin" ,"rb+" ); // 創建一個二進制讀寫文件 fwrite (str ,sizeof (str ),1 ,fp ); rewind (fp ); fread (cs ,sizeof (cs ),1 ,fp ); fclose (fp ); printf ( "%s\n" , cs ); return 0 ; }
如果讀文件時使用
fread (cs ,4 ,3 ,fp );
語句,則是分三次依次讀入4個字符,沒有讀入字符結束符,輸出時,「?」後面就是亂碼。如果讀4次,則讀入結束符。語句
fread (cs ,8 ,2 ,fp );
也可以保證讀入結束符。這跟語句
fread (cs ,sizeof (cs ),1 ,fp );
是等效的。它按cs的長度讀到文件結束。cs的長度必須大於str。使用語句
fread (cs ,sizeof (str ),1 ,fp );
是保證讀入原來的長度,當cs<str時,會把數據寫到緊鄰cs存儲區後面的空間,如果沒有破壞有用數據,程序會正常運行,否則會出現運行時錯誤。
【例22.16】找出下面程序中的錯誤。
#include <stdio.h> #include <stdlib.h> #define SIZE 2 struct student_type{ char name[10] ; int num ; int age ; char addr[15] ; }stud[SIZE] ,st[SIZE] ; int main (void ) { FILE *fp ; int i ; for ( i=0 ; i<SIZE ; i++ ) scanf ( "%s %d %d %s" ,stud[i].name ,stud[i].num , stud[i].age ,stud[i].addr ); if ( ( fp=fopen ("stu_list" ,"wb" ) ) == NULL ) { printf ("cannot open file.\n" ); return 1 ; } for ( i=0 ; i<SIZE ; i++ ) fwrite (stud[i] ,sizeof (struct student_type ),1 ,stdin ); fclose (fp ); fp = fopen ( "stu_list" ,"rb" ); for ( i=0 ; i<SIZE ; i++ ) { fread (st[i] ,sizeof ( struct student_type ),1 ,fp ); printf ( "%-10s %4d %4d%15s\n" ,st[i].name ,st[i].num , st[i].age ,st[i].addr ); } fclose (fp ); return 0 ; }
【解答】fread和fwrite函數一般用於二進制文件的輸入輸出。ANSI C標準提出設置fread和fwrite兩個函數,用來讀寫一個數據塊。它們的一般調用形式為:
fread (buffer , size ,count , fp ); fwrite (buffer ,size ,count ,fp );
其中:
buffer:是一個指針。對fread來說,它是讀入數據的存放地址。對fwrite來說,是輸出數據的地址(以上指的均是起始地址)。
size:表示要讀寫的數據一個數據項佔多少字節。
count:表示要讀寫多少個數據項。
fp:文件指針。
如果文件以二進制形式打開,用fread和fwrite函數就可以讀寫任何類型的信息。例如:
fread ( f ,4 ,2 ,fp );
其中f是一個實型數組名。一個實型變量占4個字節。這個函數從fp所指向的文件讀入兩次(每次4個字節)數據,存儲到數組f中。
如果有一個如下的結構類型:
struct student_type{ char name 〔10 〕; int num ; int age ; char addr 〔30 〕; }stud 〔40 〕;
結構數組stud有40個元素,每一個元素用來存放一個學生的數據(包括姓名、學號、年齡、地址)。因為stud是結構數組,所以每次是讀取stud的1個數組元素。stud是整個結構數組的存儲首地址,這裡要求的是指針,對數組元素來講,必須使用「&」,即&stud[i]表示結構數組的每個元素的存儲地址。假設學生的數據已存放在磁盤文件中,可以用下面的for語句和fread函數讀入40個學生的數據。
for ( i=0 ; i<40 ; i++ ) fread ( &stud 〔i 〕,sizeof (struct student_type ),1 ,fp );
下面的for語句和fwrite函數可以將內存中的學生數據輸出到磁盤文件中去,同樣道理,這裡也必須使用&號。
for ( i=0 ; i<40 ; i++ ) fwrite ( &stud 〔i 〕,sizeof (struct student_type ),1 ,fp );
如果fread或fwrite調用成功,則函數返回值為count的值,即輸入或輸出數據項的完整個數。
程序中除了這兩句漏掉「&」號之外,scanf語句也有錯誤。num和age是整數,也必須使用「&」號才行。下面是一個完整的程序,其中增加必要的錯誤處理,並將打印放在關閉文件之後。
// 完整程序 #include <stdio.h> #include <stdlib.h> #define SIZE 40 struct student_type{ char name[10] ; int num ; int age ; char addr[15] ; }stud[SIZE] ,st[SIZE] ; int main (void ) { FILE *fp ; int i ; for ( i=0 ; i<SIZE ; i++ ) scanf ( "%s %d %d %s" ,stud[i].name ,&stud[i].num , &stud[i].age ,stud[i].addr ); if ( ( fp=fopen ("stu_list" ,"wb" ) ) == NULL ) { printf ("cannot open file.\n" ); return 1 ; } for ( i=0 ; i<SIZE ; i++ ) if ( fwrite (&stud[i] ,sizeof (struct student_type ),1 ,fp )!=1 ) printf ( " file write error.\n" ); fclose (fp ); fp = fopen ( "stu_list" ,"rb" ); for ( i=0 ; i<SIZE ; i++ ) { if (fread (&st[i] ,sizeof (struct student_type ),1 ,fp )!=1 ) { if (feof (fp ) ) return 0 ; printf ( " file read error\n" ); } } fclose (fp ); for ( i=0 ; i<SIZE ; i++ ) printf ( "%-10s %4d %4d%15s\n" ,st[i].name ,st[i].num , st[i].age ,st[i].addr ); return 0 ; }
下面是將SIZE定義為2進行驗證的運行示範。
李萍 1001 19 8-201 張明 1003 25 8-305 李萍 1001 19 8-201 張明 1003 25 8-305
由於ANSI C標準決定不採用非緩衝輸出系統,所以在緩衝系統中增加了fread和fwrite兩個函數,用來讀寫一個數據塊。有些目前使用的C編譯不具備這兩個函數,請讀者注意。
4.fprintf和fscanf函數
【例22.17】下面程序給出一個奇怪的輸出,找出並改正錯誤。
#include <stdio.h> int main (void ) { char cs[16] , *str="How are you ?" ; char cs1[5] ,cs2[5] ; int i=3 ,j=0 ; float f1=4.56f ,f2=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"wb" ); // 創建一個二進制只寫文件 fprintf ( fp ,"%s %d%f" ,str ,i ,f1 ); fclose (fp ); fp=fopen ("ttt.bin" ,"rb" ); // 打開一個二進制只讀文件 fscanf ( fp ,"%s%s%s%d%f" ,cs ,cs1 ,cs2 ,&j ,&f2 ); fclose (fp ); printf ( "%d %f\n" , j ,f2 ); printf ( "%s %s %s\n" , cs ,cs1 ,cs2 ); return 0 ; }
【解答】fprintf函數、fscanf函數與printf函數、scanf函數的作用相仿,都是格式化讀寫函數。只有一點不同:fprint函數和fscanf函數的讀寫對像不是終端而是磁盤文件。它們的一般調用方式為:
fprintf (文件指針,格式字符串,輸出列表); fscanf (文件指針,格式字符串,輸入列表);
語句
fprintf ( fp ,"%s %d%f" ,str ,i ,f1 );
把字符串str寫入用的「%s」格式,它與變量i用空格隔開,但i和f1是「%d%f」格式,所以寫入的內容為「How are you?34.560000」。因為在讀文件時,是以空格為分隔符的,所以要讀三次才能把原來的字符串內容讀完。讀整數則讀入34,實數則為0.560000。
最簡單的解決辦法就是使用空格符分割,即
fprintf ( fp ,"%s %d %f" ,str ,i ,f1 );
合理使用寬度修飾符也能解決這個問題,即選用的寬度要保證最左邊有一空格,這樣即可保證寫入數據之間留有分隔符。例如在本例中,語句
fprintf ( fp ,"%s %d%6.3f" ,str ,i ,f1 );
可以得到正確結果,如選%5.3f,則不能區分整數3和實數4.56。
同樣,用以下語句
fscanf ( fp ,"%s%s%s%d%f" ,cs ,cs1 ,cs2 ,&j ,&f2 );
從磁盤文件上讀入ASCII字符。格式符之間無需使用空格,「&」的使用方法與scanf一樣,不再贅述。另外,文件既可以是文本文件,也可以是二進制文件。
【例22.18】下面程序輸出2014-5-122014-5-132014-5-14,請將三個日期分開以便分辨。
#include <stdio.h> void print_msg_one ( FILE * , const char msg ); void print_msg (FILE * , char str ); int main (void ) { char str[32] ; FILE *fp ; fp=fopen ("log.txt" ,"w" ); print_msg_one (fp ,"2014-5-12" ); print_msg_one (fp ,"2014-5-13" ); print_msg_one (fp ,"2014-5-14" ); fclose (fp ); fp=fopen ("log.txt" ,"r" ); print_msg (fp , str ); fclose (fp ); return 0 ; } void print_msg_one ( FILE *fp , const char msg ) { fprintf ( fp ,"%s" ,msg ); } void print_msg (FILE *fp , char str ) { fscanf ( fp ,"%s" , str ); printf ( "%s\n" , str ); }
【解答】寫入文件的三個數據之間沒有分隔符。如果用空格做分隔符,讀取文件時必須知道有幾個字符串。設計一個全局變量num來計算字符串數目,輸出時每次讀取一個字串。
// 改正的程序 #include <stdio.h> void print_msg_one ( FILE * , const char msg ); void print_msg (FILE * , char str ); int num=0 ; // 字符串計數 int main (void ) { char str[16] ; FILE *fp ; fp=fopen ("log.txt" ,"w" ); print_msg_one (fp ,"2014-5-12 " ); // 字符串後面增加空格 print_msg_one (fp ,"2014-5-13 " ); print_msg_one (fp ,"2014-5-14 " ); fclose (fp ); fp=fopen ("log.txt" ,"r" ); print_msg (fp , str ); fclose (fp ); return 0 ; } void print_msg_one (FILE *fp , const char msg ) { fprintf ( fp ,"%s" ,msg ); num++ ; } void print_msg (FILE *fp , char str ) { int i=0 ; for (i=0 ;i<num ;i++ ) { fscanf ( fp ,"%s" , str ); printf ( "%s " , st r ); } putchar ('\n' ); }
用fprintf和fscanf函數對磁盤文件進行讀寫,使用方便,容易理解,但由於在輸入時要將ASCII碼轉換為二進制形式,在輸出時又要將二進制形式轉換成字符,花費時間比較多。因此,在內存與磁盤頻繁交換數據的情況下,建議最好不使用fprint和fscanf函數,而應該使用fread和fwrite函數。