在對文件操作之前,可以先判別內存是否有給定的文件,也可以直接刪除指定的文件。在打開和關閉指定文件時,也需要判別是否正確打開和關閉了文件。
1.判別文件是否存在
【例22.1】程序是要判別文件是否存在,雖然存在文件TEST.TXT,但其運行結果正相反,沒有回答「有」,也沒說「沒有」。請找出並改正錯誤。
#include <stdio.h> #include <io.h> int file_state (char *filename ); int main (void ) { char filename[16] ; printf (" 請輸入要查找文件的名字:" ); scanf ("%s" ,filename ); printf (" 有%s 文件嗎? %s\n" , filename , file_state (filename ) ? " 有" : " 沒有" ); return 0 ; } int file_state (char *filename ) { return (access (filename , 0 )); }
【解答】由主程序可知,問題出現在函數file_state的返回值不滿足「?:」的判別條件。
file_state函數很簡單,直接返回access函數的返回值。而access函數是返回0和-1兩種情況,所以引起主函數輸出結果錯誤。
函數access用來對指定內存文件或文件夾進行檢查,被定義在頭文件io.h中。
函數原型:int access (const char * pathname , int mode );
功能:確定文件或文件夾的訪問權限,即檢查某個文件的存取方式(只讀或只寫方式等)。如果指定的存取方式有效,則函數返回0,否則函數返回-1。
用法:int access(const char*filenpath,int mode);
或者int_access(const char*path,int mode);
參數filenpath:文件或文件夾的路徑,當前目錄直接使用文件或文件夾名
備註:當該參數為文件的時候,access函數能使用mode參數所有的值,當該參數為文件夾的時候,access函數值能判斷文件夾是否存在。在WIN NT中,所有的文件夾都有讀和寫的權限。
Mode:要判斷的模式。在頭文件unistd.h中的預定義如下:
#define R_OK 4 /* Test for read permission. */ #define W_OK 2 /* Test for write permission. */ #define X_OK 1 /* Test for execute permission. */ #define F_OK 0 /* Test for existence. */
具體含義如下:
R_OK:只判斷是否有讀權限。
W_OK:只判斷是否有寫權限。
X_OK:判斷是否有執行權限。
F_OK:只判斷是否存在。
本程序沒有包含unistd.h(有些系統沒有這個頭文件),只能直接使用數值0。
因為access函數的返回值是0和-1,所以得到錯誤的結構。「?:」要求正確時為1,不正確為0,所以將file_state的返回值改為成功返回1,失敗返回0。將返回語句改為
return (access (filename , 0 )==0 );
即可。當access返回0,則上式返回1;access返回-1,則上式返回0,這就符合要求了。
// 改正後的程序 #include <stdio.h> #include <io.h> int file_state (char *filename ); int main (void ) { char filename[16] ; printf (" 請輸入要查找文件的名字:" ); scanf ("%s" ,filename ); printf (" 有%s 文件嗎? %s\n" , filename , file_state (filename ) ? " 有" : " 沒有" ); return 0 ; } int file_state (char *filename ) { return (access (filename , 0 )==0 ); }
目錄中有文件TEST.TXT,運行結果如下:
請輸入要查找文件的名字: text.txt 有text.txt 文件嗎? 沒有 請輸入要查找文件的名字: test.txt 有test.txt 文件嗎? 有 請輸入要查找文件的名字: TEST.TXT 有TEST.TXT 文件嗎? 沒有
【例22.2】直接利用返回值進行判斷的例子。
#include <stdio.h> #include <io.h> int main (void ) { if ( (access ( "test.txt" , 0 )) != -1 ) // 檢查文件是否存在 { printf ( " 存在 test.txt 文件。\n" ); if ( (access ( "test.txt" , 2 )) != -1 ) // 檢查文件是否允許寫操作 printf ( " 允許對 test.txt 進行寫操作。\n" ); } return 0 ; }
運行結果如下:
存在 test.txt 文件。 允許對 test.txt 進行寫操作。
2.刪除文件
【例22.3】演示使用remove函數刪除文件的例子。
#include <stdio.h> int main (void ) { char filename[16] ; printf (" 請輸入要查找文件的名字:" ); scanf ("%s" ,filename ); if (remove (filename ) == 0 ) printf (" 刪除了%s 文件。\n" ,filename ); else printf (" 沒有刪除%s 文件。\n" ,filename ); return 0 ; }
運行示範如下:
請輸入要查找文件的名字: tt.txt 沒有刪除tt.txt 文件。 請輸入要查找文件的名字: try.txt 刪除了try.txt 文件。
remove函數用來刪除一個文件,在Visual C++6.0中可以用stdio.h也可以用io.h,前者更普遍些。如果刪除成功,remove返回0,否則返回EOF(-1)。
函數原型: int remove ( const char *filename );
【例22.4】找出下面程序中的錯誤。
#include <stdio.h> #include <io.h> int main (void ) { char filename[16] ; //int remove1=0 ; printf (" 請輸入要查找文件的名字:" ); scanf ("%s" ,filename ); if (access (filename , 0 )) { int remove = 1 ; } if (remove ) { printf (" 請刪除%s 文件。\n" ,filename ); } return 0 ; }
【解答】第1次掃瞄給出警告,第2次生成可執行文件。但程序總是輸出刪除文件。首先修改if(access(filename,0))語句,但運行結果不變。remove是C語言函數名,不能重名使用,改為remove1。修改後又出現新問題,remove1在第2個if語句仍然是沒定義的,所以將定義放在外面並且初始化為0值(否則仍然錯誤,因為remove不為1時,則成為沒初始化的值)。
// 改正後的程序 #include <stdio.h> #include <io.h> int main (void ) { char filename[16] ; int remove1=0 ; printf (" 請輸入要查找文件的名字:" ); scanf ("%s" ,filename ); if (access (filename , 0 ) != -1 ) { remove1 = 1 ; } if (remove1 ) { printf (" 請刪除%s 文件。\n" ,filename ); } return 0 ; }
運行示範如下:
請輸入要查找文件的名字: me 請輸入要查找文件的名字: try.txt 請刪除try.txt 文件。
3.打開文件
C語言中的文件是一個邏輯概念,可以用來表示從磁盤文件到終端等所有東西。C語言中的文件並不是由記錄(record)組成的,而是字符的序列,即由一個一個字符(字節)的數據順序組成,所以文件的存取是以字符(字節)為單位的。根據文件中數據的組織形式,文件可分為ASCII文件和二進制文件。ASCII文件又稱文本(text)文件,它的每一個字節放一個ASCII代碼,代表一個字符。二進制文件則把內存中的數據按其在內存中的存儲形式原樣輸出到磁盤上存放。如果有一個整數1 000,在內存中占2個字節,如果按ASCII碼形式輸出,則占4個字節;而按二進制形式輸出,在磁盤上只佔2個字節。用ASCII碼形式輸出與字符一一對應,一個字節代表一個字符,因而便於對字符進行逐個處理,也便於輸出字符。但這種形式一般占存儲空間較多,而且要花費轉換時間(二進制形式與ASCII碼間的轉換)。用二進制形式輸出數據,可以節省外存空間和轉換時間,但一個字節不對應一個字符,不能直接輸出字符形式。
在一個程序開始執行時,3個預定的文字流:stdin,stdout和stderr就被打開。它們與和系統相連接的標準I/O設備有關。在一些C編譯器中還打開stdprn(標準打印機)和stdaux(標準輔助設備)。對大多數計算機系統來說,stdaux是控制台。包括DOS在內的大多數操作系統都允許I/O重定向,使向某文件上讀寫的東西重定向到其他設備,但不應該試圖直接去打開或關閉該文件。
每一個與文件相結合的流,都有一個類型名為FILE的文件控制信息的結構,這個結構定義在頭部文件stdio.h中。
關於文件的處理,首先要注意的是實際的外部文件名稱如何與實際讀寫數據的語句取得聯繫。對文件讀寫之前應該打開該文件,在使用結束之後應關閉該文件。
文件指針是貫穿緩衝型I/O系統的主線。一個文件指針是一個指向文件有關信息的指針,這些信息定義了文件的許多東西,它包括文件名、狀態和當前位置。從概念上講,文件指針標誌一個指定的磁盤文件,用來告訴系統的每個緩衝型函數應該到什麼地方去完成操作。文件指針實際是一個指向目標結構的指針變量,該目標結構與FILE結構類型相同,在stdio.h中定義。
根據以上特點可知,使用文件必須注意正確打開文件和及時關閉文件,必須判斷寫入的數據大小及解決相應的讀寫措施和編程中如何正確使用涉及文件的有關判斷條件(表達式)及相應判斷語句。
ANSI C規定了標準輸入輸出函數庫,用fopen函數來實現打開文件。
fopen函數的調用方式通常為:
FILE *fp ;
其中,fp=fopen(文件名,使用文件方式)。例如:
fp = fopen ( "A1" ,"r" )
表示要打開名字為A1的文件,使用文件方式為「讀入」。fopen函數返回指向A1文件的指針並賦給文件指針變量fp,這樣fp就和A1相聯繫了,或者說fp指向A1文件。
文件指針變量fp是用文件控制信息結構FILE來定義的。由此可以知道,通過調用fopen函數打開一個文件時,通知編譯系統以下3條信息。
(1)需要打開的文件名,也就是準備訪問的文件的名字。
(2)讓哪一個指針變量指向被打開的文件。
(3)使用文件的方式(讀還是寫等)。
典型的文件使用方式如表22-1所示。
表22-1 使用文件方式表
文件的使用方法說明如下。
(1)用"r"方式打開的文件只能用於向計算機內存輸入,而不能用於向該文件輸入數據。而且該文件應該已經存在,不能打開一個並不存在的用於"r"方式的文件(用於向計算機內存輸入的文件),否則出錯。
(2)用"w"方式打開的文件只能用於向該文件寫數據,而不能用來向計算機內存輸入。如果原來不存在該文件,則在打開時新建立一個以指定名字命名的文件。如果原來已存在一個以該文件名命名的文件,則在打開時將該文件刪去,然後重新建立一個新文件。
(3)如果希望向文件末尾添加新的數據(不希望刪除原有數據),則應該用"a"方式打開。但此時該文件必須已存在,否則將得到出錯信息。打開時,位置指針移到文件末尾。
(4)用"r+"、"w+"、"a+"方式打開的文件可以用來輸入和輸出數據。用"r+"方式時該文件應該已經存在,以便能向計算機內存輸入數據。用"w+"方式時則新建立一個文件,先向此文件寫數據,然後可以讀此文件中的數據。用"a+"方式打開的文件,原來的文件不被刪去,位置指針移到文件末尾,可以添加也可以讀。
(5)如果不能實現打開的任務,fopen函數將會返回一個出錯信息。出錯的原因可能是:用"r"方式打開一個並不存在的文件;磁盤出故障;磁盤已滿無法建立新文件等。結果fopen函數將帶回一個空指針值NULL(NULL在stdio.h文件中已被定義為0)。常用方法為
if ( ( fp = fopen ( "file1" ,"r" ) ) == NULL ) { printf ("cannot open this file.\n" ); exit (1 ); }
在打開一個文件後先檢查打開是否出錯,如果有錯就在終端上輸出「cannot open this file」。exit函數的作用是關閉所有文件,終止正調用的過程。待程序員檢查出錯誤並改正後再運行。
(6)用以上方式可以打開文本文件或二進制文件。ANSI C規定用同一種緩衝文件系統來處理文本文件和二進制文件,但目前使用的有些C編譯系統可能不完全提供所有這些功能(如有的只能用"r"、"w"、"a"方式),有的C版本不用"r+"、"w+"、"a+"而用"rw"、"wr"、"ar"等,請注意所用系統的規定。
(7)在用文本文件向計算機內存輸入時,將回車換行符轉換為一個換行符,在輸出時把換行符轉換成回車和換行兩個字符。在用二進制文件時,不進行這種轉換,在內存中的數據形式與輸出到外部文件中的數據形式完全一致,一一對應。
(8)在程序開始運行時,系統自動打開3個標準文件:標準輸入、標準輸出、標準出錯輸出。通常3個文件都與終端相聯繫。因此以前所用到的從終端輸入或輸出,都不需要打開終端文件。系統自動定義了三個文件指針stdin、stdout和stderr,分別指向終端輸入、終端輸出和標準出錯輸出(也從終端輸出)。如果程序中指定要從stdin所指的文件輸入數據,就是指從終端鍵盤輸入數據。
由此可見,打開文件主要是按規定格式書寫即可。因為文件名有時是通過程序獲得的,所以常常出現的問題不是打開語句,而是文件名。下面就舉例說明文件名容易出現的問題。
【例22.5】判斷給定的文件名是否可以使用。
#include <stdio.h> #include <string.h> int filename ( const char name[ ] ) { static const char *name_list={ "test.txt" , "try.txt" , "student.txt" , "list.txt" , NULL } ; int i ,n=0 ; for (i=0 ;name_list[i] !=0 ;i++ ){ if (strcmp (name ,name_list[i] )==0 ) return 1 ; } return 0 ; } int main (void ) { printf (" 文件test.txt=%d\n" ,filename ("test.txt" )); printf (" 文件student.txt=%d\n" ,filename ("student.txt" )); printf (" 文件st.txt=%d\n" ,filename ("st.txt" )); printf (" 文件list.txt=%d\n" ,filename ("list.txt" )); return 0 ; }
【解答】程序if語句的表達式有問題。strcmp函數是返回0和非0(其實是-1)。程序中的兩個字符串不相等時返回非0,也就是表達式的值為1,也執行「return1;」語句。把這條語句改為
if (strcmp (name ,name_list[i] )==0 )
即可。運行結果如下:
文件test.txt=1 文件student.txt=1 文件st.txt=0 文件list.txt=1
【例22.6】下面的函數用來獲取臨時文件名,找出並改正錯誤。
#include <stdio.h> char *tmp_name ( void ) { char name[32] ; const char DIR="/var/temp/temp" ; static int sequence=0 ; // 序列號 ++sequence ; // 序列號順增 sprintf ( name , "%s.%d" , DIR , sequence ); return (name ); } int main (void ) { char *a_name=tmp_name (); printf ("Name :%s\n" ,a_name ); a_name=tmp_name (); printf ("Name :%s\n" ,a_name ); return 0 ; }
【解答】函數tmp_name中定義一個局部字串變量name,而且把一個指針返回給這個局部變量name。函數tmp_name結束時,該函數內的全有非靜態局部變量都會被重新分配存儲空間,當然也包括name。這樣一來,返回的指針就指向了一個隨機的內存區域。隨之而來的下一個函數調用可能會改寫這塊內存區域,這就使a_name變成一個危險的內存區域。將「char name[32];」改為「static char name[32];」即可。修改後的運行結果將為:
Name :/var/temp/temp.1 Name :/var/temp/temp.2
一定要注意函數返回值的方法是否正確,同時注意調用方法是否正確。
【例22.7】找出並改正程序中的錯誤。
#include <stdio.h> char *tmp_name ( void ) { static char name[32] ; const char DIR="/var/temp/temp" ; static int sequence=0 ; // 序列號 ++sequence ; // 序列號順增 sprintf ( name , "%s.%d" , DIR , sequence ); return (name ); } int main (void ) { char *a1_name=tmp_name (); char *a2_name=tmp_name (); printf ("Name :%s\n" ,a1_name ); printf ("Name :%s\n" ,a2_name ); return 0 ; }
【解答】函數tmp_name是上例改正後的程序,沒有錯誤,所以只能是主函數使用方法不當造成兩個輸出
Name a1 :/var/temp/temp.2 Name a2 :/var/temp/temp.2
都一樣的錯誤結果。錯誤就是因為雖然有兩個指針,但都是指向相同的文件名變量name。第1次是使用a1_name調用tmp_name,得到「Name:/var/temp/temp.1」。但第2次調用時,a2_name也是調用tmp_name。雖然得到「Name:/var/temp/temp.2」,但a1_name也指向name,因此第二次調用覆蓋了第1次調用結果所佔用的內存,即使用a2_name的name覆蓋了原來a1_name的name,使兩者輸出相同。
一種解決的辦法是為它們準備各自的字符數組以存儲自己的名字,例如:
char *tmp , a1_name[32] ,a2_name[32] ; tmp=tmp_name (); strcpy (a1_name ,tmp ); tmp=tmp_name (); strcpy (a2_name ,tmp );
這要包含頭文件string.h。另一種是各自解決自己的內存分配。
// 為各自分配自己的內存 #include <stdio.h> #include <stdlib.h> char *tmp_name ( void ) { char *name ; const char DIR="/var/temp/temp" ; static int sequence=0 ; // 序列號 ++sequence ; // 序列號順增 name= (char* )malloc (32 ); sprintf ( name , "%s.%d" , DIR , sequence ); return (name ); } int main (void ) { char *a1_name ,*a2_name ; a1_name=tmp_name (); a2_name=tmp_name (); printf ("Name a1 :%s\n" ,a1_name ); printf ("Name a2 :%s\n" ,a2_name ); return 0 ; }
4.關閉文件
為防止誤用文件,在使用完文件之後,應馬上關閉它。關閉就是使文件指針變量不指向該文件,也就是文件指針變量與文件「脫鉤」,此後不能再通過該指針對其關聯的文件進行讀寫操作,除非再次打開,使該指針變量重新指向該文件。用fclose函數關閉文件時,fclose函數調用的一般形式為
fclose (文件指針);
例如:
fclose ( fp );
用fopen函數打開文件時所返回的指針賦給了fp,因此調用fclose函數時再通過fp把該文件關閉。應該養成在程序終止之前關閉所有文件的習慣,如果不關閉文件將會丟失數據。
在向文件寫數據時,是先將數據輸出到緩衝區,待緩衝區充滿後才正式輸出給文件。如果當數據未充滿緩衝區而程序已經結束運行時,就會將緩衝區中的數據丟失。用fclose函數關閉文件,可以避免這個問題,這就要先把緩衝區中的數據輸出到磁盤文件,然後才釋放文件指針變量。
fclose函數也帶回一個值:當順利地執行了關閉操作,則返回值為0;如果返回值為非零值,則表示關閉時有錯誤。可以用ferror函數來測試。
注意文件關閉時的正確判別條件是
if (fclose (fp )== EOF )
而常犯的誤判是使用
if (fclose (fp )== NULL )
這是使用出錯標誌判斷,因為C語言規定如果這個文件被成功關閉,fclose返回0,否則返回EOF(-1)。
【例22.8】判斷關閉文件實例。
#include <stdio.h> #include <string.h> int main (void ) { FILE *fp=NULL ; const char*buf="0123456789" ; fp=fopen ("TRY.FIL" ,"w" ); // 創建一個包含10 個字節的文件 fwrite (buf ,strlen (buf ),1 ,fp ); // 將buf 內容寫入到文件中 if (fclose (fp )== EOF ) printf (" 文件非正常關閉!\n" ); else printf (" 文件正常關閉!\n" ); return 0 ; }