本文主要參考了C Primer Plus (5th & 6th Edition)
您可以選擇本文的部分內容來讀,有些內容對於不熟悉MS-DOS的讀者可能過於晦澀難懂。
文件通常是在磁盤或固態硬盤上的一段已命名的存儲區。所有的文件內容都以二進制形式儲存。文件分為文本文件和二進制文件。
文件格式 定義 保存內容 示例 文本文件 文本文件就是最初使用二進制編碼字符(如ASCII或Unicode)表示文本的文件 文本內容 .TXT文件 二進制文件 二進制文件就是文件中的二進制值代表機器語言代碼或數值數據的文件 二進制內容 圖片文件,可執行文件C語言提供兩種途徑訪問文件:文本模式和二進制模式。文本模式,顧名思義就是以文本形式訪問文件,該文件通常是文本文件。不同的系統,處理文本文件的方式不同。UNIX系統使用'\n'表示換行,而早期的MS-DOS使用'\r'和'\n'組合換行,用Ctrl + Z表示文件結尾。舊式的OS X Macintosh卻使用'\r'表示新的一行。這給程序員在不同系統中操作使用文件帶來了諸多不便。還好,C語言提供了轉換機制。例如在早期的MS-DOS中以文本模式打開某個文件file1.txt,它會自動把\r\n組合轉換成'\n',如果要往該文件寫入內容時又會把'\n'轉換成\r\n組合。當以二進制模式打開文件時,程序將訪問到文件的每一個字節,程序員如果需要處理文本文件,就必須根據操作系統的不同而采取不同措施。
文件的結尾標志著文件內容的結束,C語言用宏EOF(End of File)表示文件的結尾,其值通常為-1。
C程序自動打開3個文件,它們是:
文件 通常使用的默認設備 C程序中的表示法 標准輸入 鍵盤 stdin 標准輸出 顯示屏 stdout 標准錯誤輸出 顯示屏 stderr那麼如何更改這些文件的默認設備呢?我們可以通過重定向的方法。
使用帶有MS-DOS6.22系統的計算機,假定才C:\user中有如下文件:
文件defin.txt中有如下內容:
insort.exe是一個插入排序的程序。
重定向輸入為defin.txt,然後執行程序:
發現其實重定向輸入就是把defin.txt的內容與sdtin流相關聯。其中'<'是重定向運算符。這樣的優勢是,如果遇到大量數據輸入時,可以先把數據輸入到文件,再重定向輸入運行程序,十分方便且易於檢查和修改錯誤。可是我們看到,程序運行後死機,原因是程序結尾處有代碼getch();,而stdin流已經與defin.txt相關聯,本例中defin.txt的末尾處沒有可以使getch();執行的內容,且鍵盤不再是stdin流的默認設備,所以,程序將永遠無法退出(單任務純DOS環境)。由此可見,重定向輸入也是有風險的。(同時提醒各位讀者,在設計單任務純DOS下運行的程序時,如無必要,不需要在程序末尾添加暫停指令。必須要有相應的退出指令)
同樣,我們也可以重定向輸出:
這樣,所有的輸出都被發送到defout.txt中。其中'>'是重定向輸出運算符,它把defout.txt和stdout相關聯。打開defout.txt我們可以看到:
這樣,我們就把原本輸出到stdin默認設備---顯示屏上的內容都輸入到文件defout.txt中了。但是,你看不到輸出的內容,所以重定向輸出只在特殊情況下使用,如UNIX服務器。
我們還可以使用組合重定向
其中,<defin.txt和>defout.txt可以調換位置。符號左右的空格如果系統允許可以省略。
需要注意的是,我們不能同時重定向輸入或輸出多個文件或重定向輸入輸出與可執行文件關聯,使用重定向與文件關聯時要保證文件有結尾EOF(End Of File),使用>重定向輸出時,輸出的文件內的數據將被覆蓋。
使用重定向輸入輸出來操作文件不僅十分繁瑣而且還存在相應的危險。C語言為程序員提供了更加直觀方便的文件訪問機制,即程序員可以在程序中直接操作文件而不需要去做類似重定向這樣的“低級”行為。
我們如果要在程序中操作一個文件,這個文件同該程序在相對的同目錄下(如果使用IDE就要到IDE默認路徑創建),就可以像這樣打開文件:
FILE * fp; //聲明文件指針 fp = fopen("file1.txt", "r"); //以只讀模式打開文件 if(fp == NULL) exit(EXIT_FAILURE); //如果打開失敗就退出
fopen()函數在成功打開後返回指向該文件指針,否則返回NULL。接受兩個參數,第一個參數表示文件的名稱(包含文件名的字符串地址),第二個參數表示打開模式(這與“文本模式和二進制模式”是一樣的,只是細分了這些模式。),常用的打開模式如下:
此外C11還增加了x模式,為了不增加讀者的負擔,本文不詳述。
和動態內存分配一樣,我們需要檢測文件是否成功打開,而且在執行完相應操作後有必要及時關閉文件。關閉文件我們用fclose(),和free()一樣簡單,沒有返回值:
fclose(fp);//關閉文件 if(fp != 0) exit(EXIT_FAILURE);//如果關閉失敗就退出
但是,有時候,文件不能正常關閉,例如程序運行時硬盤有故障或被拔出。因此,我們十分有必要在關閉之後檢查一下狀態。
C Primer Plus(Sixth Edition)對這兩個函數的解釋非常清楚:{
getc()和putc()函數與getchar()和putchar()函數類似,不同的是,要告訴getc()和putc()函數使用哪一個文件。下面這條語句的意思是“從標准輸入中獲取一個字符”:
ch = getchar();
而下面這條語句的意思是“從fp指定的文件中獲取一個字符”:
ch = getc(fp);
與此類似,下面語句的意思是“把字符ch放入FILE指針fpout指定的文件中”:
putc(ch, fpout);
}
這裡我們需要注意getc()的返回值是int類型的,這樣是為了返回EOF,雖然有些系統把char類型默認定義signed char,但為了保證程序最大的可移植性,上面代碼中ch字符實際上是int類型的。
C程序只有在讀到超過文件末尾之後才會發現文件結尾(EOF),所以為了避免空文件被錯誤讀取,我們應在執行相應操作之前嘗試讀取文件,再根據是否讀到EOF 來執行相應操作。
這裡插一段:有些讀者朋友會問getc()和fgetc()以及putc()和fputc()之間有什麼關系?其實在查閱相關文檔之後,兩組函數幾乎是一模一樣的,只不過或許getc()和putc()是fgetc()和fputc()的宏實現,原話如下:
所以在調用getc()和putc()時,其參數不能是具有副作用的表達式。例如get(fp++)這樣的,這是不允許的!
fprintf(),fscanf()和printf(),scanf()類似,只不過在原有基礎上加了一個參數來確定輸出或輸入的目標文件。例如,如果有一個FILE指針fp已經指定一個有效的文件,並以"w"模式打開它,我們這樣做可以將Hello World輸出到這個文件上:
fprintf(fp, "Hello World");
同樣,如果有一個字符串"string"被保存在FILE指針fp指定的有效文件的最開始,並以"r"模式打開它,我們這樣做將"string"輸入到數組st上:
fscanf(fp, "%s", st);
請留意這裡的輸入輸出,它們並不指從標准輸入輸出設備獲得輸入輸出,而是一種傳遞的關系:
我們通過C Primer Plus(Fifth Edition)上的一個相關示例來演示這兩個函數以及rewind()的具體應用:
fgets()函數和fputs()函數之前已在《字符串的輸入輸出》這篇博文介紹過,只不過現在將它的用途擴展到全體文件。我們假定fp是一個有效的FILE指針,那麼從fp指定的文件中讀取一段長度為SIZE - 1的字符串(不包括'\0')到數組st,我們可以這樣:
fgets(st, SIZE, stdin); //fgets()函數讀取SIZE-1個字符或遇到'\n'結束,並把最後一個字符或'\n'之後的字符賦值為'\0',也就是說在某些情況下它保存了'\n'。
同理,fputs()將字符串"Hello World\n"輸出到fp所指定的文件:
fputs(fp, "Hello World\n"); //因為fgets()保存了'\n'所以fput()不會自動在字符串末尾加上'\n'
如果部分讀者讀到這裡可能會吃不消,因為我們發現,與文件操作這塊內容相關的函數非常多,很難記憶,不過沒有關系,你不必去死記硬背,只要記住函數的大致功能,用到時再去查C標准庫參考,久而久之就慢慢會記住了。
ftell()函數返回一個long類型的值,該值表示文件當前位置距離文件開始處的字節數目,以此來確定文件指針當前指向的位置。它接受一個參數,該參數表示一個有效的文件指針。如下代碼所示,我們假定fp是一個有效的FILE指針:
long int addr; addr = ftell(fp);
addr就返回文件的當前位置,如下圖所示:
由此可見,ftell()將文件當成數組來處理,我們就可以用處理數組的方法來處理文件。但是,這有一個重要的前提,就是文件必須用二進制模式打開,否則將會出現毫無意義的結果。
fseek()函數可以改變文件的當前位置,它實現的前提也應是以二進制模式打開文件。它接受三個參數,第一個參數指明要進行操作的文件指針,第二個參數我們稱作偏移量,這是一個long型的參數,如果往文件開始處偏移就是負值,否則為正。第三個參數是模式,用來表示偏移的起點。模式有三種:
我們通過書中的示例來解釋這兩個函數的應用(因為是以二進制模式打開,所以針對不同的系統,讀取文本文件就要有不同的實現代碼):
我們先前了解到,如果C程序以二進制模式訪問一個文件,那麼它將可以訪問該文件的所有字節。文件定位的示例中演示了這個特性。那麼,為什麼需要以二進制模式對文件進行操作呢?假設我們要在文件中存儲1/3這個值,我們選擇文本模式,那麼存儲的精度就會大大降低,也更加地麻煩(因為要將數字轉換成字符串就必須指定一個精度,讀取時也必須指定同樣的精度,如果將1/3以0.33存入文件,下次讀取就不能恢復它原來的精度)。所以,我們最好的選擇就是用相同的位格式來存儲值,我們可以使用sizeof(double)個字節的空間來存儲1/3,這和程序在內存中存儲double變量的位格式相同,相當於拷貝了一個值為1/3的double變量到文件中,讀取時按照原本寫入時的精度去讀取,就可以恢復最大的精度。為了完成上述操作,我們可以使用fread()和fwrite()函數。
fwrite()的原形是:
size_t fwrite(const void *
ptr, size_t
size, size_t
nmemb, FILE *
stream);
其中,ptr指定要寫入的數據塊的地址(原地址),size表示寫入數據塊的大小,nmemb表示寫入數據塊的個數(這能很方便地寫入數組),stream指定要寫入的文件(目標地址)。例如我們要把1/3這個數以最大精度寫入文件pro.dat,我們需要這樣做(這一節只給出核心代碼):
FILE * fp; double num = 1.0 / 3.0; fp = fopen("pro.dat", "wb"); //以二進制寫模式打開文件 if( !fp ) exit(EXIT_FAILURE); fwrite(&num, sizeof(double), 1, fp); //寫入二進制數據 if( fclose(fp) != 0 ) //關閉文件 exit(EXIT_FAILURE);
然後,我們查看相對與程序同目錄的文件夾發現pro.dat創建成功,並且大小為8字節,正是當前系統double類型變量的大小。
現在,如果我們要讀取pro.dat文件裡的內容,我們就要使用fread()函數了,因為以文本模式打開這個文件會出現亂碼。fread()函數的原形如下:
size_t fread(void *
ptr, size_t
size, size_t
nmemb, FILE *
stream);
同樣是四個參數,第一個參數指名要讀取到的數據塊地址(目標地址),第二三個參數與fwrite()的中對應參數相同,表示讀取的大小,最後一個參數指明要讀取的文件。
下面的代碼演示了如何使用pro.dat文件內容:
FILE * fp; double get; fp = fopen("pro.dat", "rb"); //以二進制讀模式打開文件 if( !fp ) exit(EXIT_FAILURE); fread(&get, sizeof(double), 1, fp); //讀取文件內容到get if( fclose(fp) != 0 ) //及時關閉文件 exit(EXIT_FAILURE); printf("get * 3.0 = %lf",get * 3.0); //輸出
如無意外,代碼運行之後會顯示:
這篇文章我概括地寫出了有關C語言文件的基本操作,如果您手頭上正好有一本C Primer Plus(第5或6版),請最好結合著書看本文。
通過本文,我們知道了:
〉〉什麼是文件?
〉〉MS-DOS系統下重定向的有關操作和反映問題
〉〉文本的打開和關閉fopen()和fclose()
〉〉文本模式I/O:getc()和putc(),fprintf()和fscanf(),fgets()和fputs()
〉〉文件定位:fseek()和ftell()
〉〉二進制模式I/O:fread()和fwrite()
在本文的編撰過程中,由於本文的篇幅較大,圖片較多,部分內容偏難,我耗費了很長時間才完成。文中的錯誤也是不可避免的,如果您在讀完之後發現有什麼錯誤,或是有什麼建議,歡迎批評指出!