最近實在是太忙了,這篇整整就推遲了1個月了,實在是對不起。之前本打算這個模塊就結束了,文件操作就不寫了,但是文件操作又是一個很重要的東西,而且也剛好能夠總結之前我們學習的所有知識。同時也爲了將文件操作這個初學者認爲很神祕的東西給本質化。因此,本篇將逐一介紹C語言的文件操作。(本模塊的命名本來是想C/C++一塊兒講解的,但是由於工作、畢業論文、業餘時間的充電、還有要完成那個未知的夢等,因此因爲時間問題C++就只能放在以後有機會再寫了,因此本篇將是本模塊的最後一篇,之後將不會再連載了,請大家諒解。)
好了,回到正題,先來看文件操作中的文件。所謂文件(file)一般指存儲在外部介質上數據的集合,比如我們經常使用的mp3、mp4、txt、bmp、jpg、exe、rmvb等等。這些文件各有各的用途,我們通常將它們存放在磁盤或者可移動盤等介質中。那麼,爲什麼這裏面又有這麼多種格式的文件呢?原因很簡單,它們各有各的用途,區分就在於這些文件裏面存放的數據集合所遵循的存儲規則不一樣。舉個例子比如bmp圖片文件,爲什麼他能夠表示一張圖片,因爲它有固定的格式,哪一段到哪一段,哪個偏移到哪個偏移應該存放什麼數據是規定好了的。比如有文件頭,一般是一個結構體,存放的文件的一些信息,如圖片的大小,像素等等。再後來有數據區。然後我們要顯示一張圖片,就只需要按照前面所說的規則將文件頭結構和數據塊讀出來,然後將這些數據在屏幕上用顏色表示出來,就成了一張圖片。其它文件格式也類似。
這裏要說一個更重要的例子,對我們理解文件有好處。那麼這個文件就是exe文件(這裏只討論windows平臺),通常我們認爲它是一個可執行程序,這無疑是增加了它的神祕度。從本質上來講exe無非是一種固定的文件格式罷了。既然這樣,它就有一套自己的存儲規則。跟前面的圖片文件一樣有規則。此時,你可能會問:你這麼說那我就可以純手工(直接填寫數據填充文件)寫出一個exe可執行文件了? 面對你這個問題,我只能說你已經習慣思考了,已經習慣給自己提問了,已經很聰明瞭。那麼答案是肯定的,你完全可以用一個編輯器直接填寫數據寫出一個helloworld.exe文件或者helloworld.dll文件。因爲這些具有一定格式規則的文件一般是二進制存儲的,於是我們可以用一個二進制編輯器新建一個二進制文件,然後向裏面填寫數據。然後雙擊運行輸出“helloworld”字符串。你可能會覺得很有成就感,我之前就寫過一個exe和dll。這裏exe和dll的文件格式也就是著名的PE文件格式。有興趣你可以去查閱相關資料,此非本文重點。
總結上面的認識,文件無非就是一段數據的集合,這些數據可以是有規則的集合,也可以是無序的集合。操作系統也就是以文件爲單位對數據進行管理的。也就是說,要訪問外部介質上的數據,必須先按照文件名進行查找,然後從該文件中讀取數據。要想寫數據到外部介質,必須得建立一個文件,然後再寫入。因此,這樣來看,你眼前的文件將是一堆一堆數據而已,也沒有什麼類型文件之分了,類型只是爲了區分而已,假如你把一個exe文件的擴展名改爲txt,把它用記事本打開,同樣是可行的,只是會執行exe文件裏面的東西而已。(這裏又不得不提到一點,如果你是一名程序員或者愛好者,那麼你不應該將你的文件擴展名給隱藏了,要讓它顯示出來,如果你隱藏了,無非是增加了它的神祕感,同時在文件操作上不方便。通過上面的本質,我相信你能體會到我爲什麼這麼說。)
說到這裏,你應該知道文件是什麼了,那麼再來看二進制文件和ASCII文本文件,爲什麼要分爲這兩種呢?
首先、文本文件方式存儲多用於我們需要明顯知道文件裏面的內容時,比如ini、h、c等文件都是文本文件,這種文件存儲的是字符(ASCII碼),比如一個整數10000,類型是short,佔2字節,存儲文本形式將佔用5個字節,一共5個字符。你可以想想更多的例子,體會文本文件方便之處(提示:這裏的文本文件不是說是txt文件,而是指所有以文本格式存儲的文件。)
其次、二進制文件方式多用於直接將內存裏面的數據形式原封不動存放到文件裏,比如上面的short 10000,在內存中佔2字節,存儲內容爲10000的二進制數,存到文件後還是佔2字節,內容也是10000的二進制。這種方式可以整塊數據一塊兒存儲,同時還可以將內存數據映射到文件裏。
由上面兩點,C語言操作文件可以是字節流或者二進制流。它把數據看成是一連串字符(字節),而不需要考慮邊界。C語言對文件的存取是以字節爲單位的。輸入輸出的數據流的開始和結束僅受程序控制而不受物理符號(如回車換行符)控制。這種文件通常稱爲流式文件,大大增加了靈活性。我們可以產生很多自己的文件格式,在遊戲程序裏面,用得比較多的就是資源包的格式,一般就是自定義的存取規則。我之前也寫了一個包文件,存取只需要遵循規則,原理是非常簡單的。大家可以試試在腦子裏面構造一個包文件。
在ANSI C標準中,使用的是“緩衝文件系統”。所謂緩衝文件系統指系統自動地在內存區爲每一個正在使用的文件名開闢一個緩衝區,從內存向磁盤輸出數據必須先送到內存中的緩衝區,裝滿後再一起送到磁盤去。反向也是如此。這裏需要說明兩個詞:“輸入”“輸出”。輸入表示從文件裏讀數據到程序裏,輸出表示從程序裏寫數據到文件中。
瞭解了文件及文件存儲形式,下面該正式進入文件的讀寫了,不要太激動,還是慢慢來。細節往往決定成敗。在緩衝文件系統中,有一個很重要的一個東西就是文件指針,每個使用的文件都會在內存中開闢一個區,用於存放文件的有關信息,這些文件信息就保存在一個結構體變量中的,這個結構體是由系統定義的,名爲FILE,先來看看VC2005在stdio.h下FILE結構體的定義:
struct _iobuf
{
char *_ptr; // 指向buffer中第一個未讀的字節
int _cnt; // 記錄剩餘未讀字節的個數
char *_base; // 指向一個字符數組,即這個文件的緩衝
int _flag; // FILE結構所代表的打開文件的一些屬性
int _file; // 用於獲取文件描述,可以使用fileno函數獲得此文件的句柄。
int _charbuf; // 單字節的緩衝,即緩衝大小僅爲1個字節,如果爲單字節緩衝,_base將無效
int _bufsiz; // 記錄這個緩衝的大小
char *_tmpfname; // temporary file (i.e., one created by tmpfile()
// call). delete, if necessary (don't have to on
// Windows NT because it was done by the system when
// the handle was closed). also, free up the heap
// block holding the pathname.
};
typedef struct _iobuf FILE;
好了,上面的結構體就是這樣定義的。這裏不得不再次提到緩衝:
緩衝模式 |
常量(mode) |
備註 |
無緩衝模式 |
_IONBF |
該文件不使用任何緩衝,也可以說是字節緩衝 只能保存一個字節。 |
行緩衝模式 |
_IOLBF |
僅對文本模式打開的文件有效,所謂行,即是指每收到一個換行符(/n或/r/n),就將緩衝flush掉 |
全緩衝模式 |
_IOFBF |
僅當緩衝滿時才進行flush |
上面結構體中的_flag就標記了緩衝的信息(我們關心這三個):
#define _IOYOURBUF 0x0100 // 使用用戶通過setbuf提供的buffer
#define _IOMYBUF 0x0008 // 這個文件使用內部的緩衝
#define _IONBF 0x0004 // 無緩衝模式
#define _IOLBF 0x0040 // 行緩衝模式
#define _IOFBF 0x0000 // 全緩衝模式
同時,_flag也標記了讀寫模式,比如"r+"、"w+"等。
#define _IOREAD 0x0001 // 只讀
#define _IOWRT 0x0002 // 只寫
#define _IORW 0x0080 // 可讀可寫
上面的3中模式就是"r"、"w"、"+"任意組合起來表示的意思。
正因爲使用緩衝模式,是爲了避免頻繁的系統調用開銷,有了緩衝就不需要每次都訪問實際的文件。當然緩衝也會帶來隱患,比如寫文件時,先是到緩衝,如果此時系統崩潰或者進程意外退出時,有可能導致文件數據的丟失。因此C語言提供了幾個基本的函數,彌補緩衝帶來的問題:
int fflush( FILE* stream ) // flush指定文件的緩衝,若參數爲NULL,則flush所有文件的緩衝。
int setvbuf( FILE *stream, char* buf, int mode, size_t size ) // 設定緩衝類型,如上面的表格。
void setbuf( FILE* stream, char* buf ) // 設置文件的緩衝,等價於( void )setvbuf( stream, buf, _IOFBF, BUFSIZ ).
所謂flush一個緩衝,是指對寫緩衝而言,將緩衝內的數據全部寫入實際的文件,並將緩衝清空,這樣可以保證文件處於最新的狀態。之所以需要flush,是因爲寫緩衝使得文件處於一種不同步的狀態,邏輯上一些數據已經寫入了文件,但實際上這些數據仍然在緩衝中,如果此時程序意外地退出(發生異常或斷電等),那麼緩衝裏的數據將沒有機會寫入文件。flush可以在一定程度上避免這樣的情況發生。
在這個表中我們還能看到C語言支持兩種緩衝,即行緩衝(Line Buffer)和全緩衝(Full Buffer)。全緩衝是經典的緩衝形式,除了用戶手動調用fflush外,僅當緩衝滿的時候,緩衝纔會被自動flush掉。而行緩衝則比較特殊,這種緩衝僅用於文本文件,在輸入輸出遇到一個換行符時,緩衝就會被自動flush,因此叫行緩衝。
終於把概念性的東西和準備步驟做完了,下面該看看具體的讀寫文件了。有了前面的準備工作,讀寫文件將不是難事了,因爲有現成的庫函數供我們使用,我們下面的段落將是如何使用這些庫函數和一些注意事項而已了。
首先看如何打開文件,先看代碼:
#include <stdio.h>
int main( void )
{
FILE* pReadFile = fopen( "E://mytest.txt", "r" ); // 打開文件
if ( pReadFile == NULL )
return 0;
fclose( pReadFile ); // 關閉文件
return 0;
}
上面的這段代碼,只是一個簡單的打開文件,如果成功打開後直接關閉。這裏打開的是一文本文件,是以只讀的方式打開。使用fopen函數打開,第一個參數是文件路徑,第二個參數是讀寫模式,返回值爲0表示打開失敗。先看看讀寫模式:
文件使用方式 |
含義 |
"r"(只讀) |
爲輸入打開一個文本文件,不存在則失敗 |
"w"(只寫) |
爲輸出打開一個文本文件,不存在則新建,存在則刪除後再新建 |
"a"(追加) |
向文本文件尾部增加數據,不存在則創建,存在則追加 |
'rb"(只讀) |
爲輸入打開一個二進制文件,不存在則失敗 |
"wb"(只寫) |
爲輸入打開一個二進制文件,不存在則新建,存在則刪除後新建 |
"ab"(追加) |
向二進制文件尾部增加數據,不存在則創建,存在則追加 |
"r+"(讀寫) |
爲讀寫打開一個文本文件,不存在則失敗 |
"w+" (讀寫) |
爲讀寫建立一個新的文本文件,不存在則新建,存在則刪除後新建 |
"a+"(讀寫) |
爲讀寫打開一個文本文件,不存在則創建,存在則追加 |
"rb+"(讀寫) |
爲讀寫打開一個二進制文件,不存在則失敗 |
"wb+"(讀寫) |
爲讀寫建立一個新的二進制文件,不存在則新建,存在則刪除後新建 |
"ab+"(讀寫) |
爲讀寫打開一個二進制文件,不存在則創建,存在則追加 |
一、讀寫字符
C語言爲從文件中讀寫一個字符提供了兩個函數:
int __cdecl fgetc( FILE* stream ); // 從文件讀入一個字符
int __cdecl fputc( int ch, FILE* stream ); // 寫入一個字符到文件
看例子:
#include <stdio.h>
int main( void )
{
char cInput;
FILE* pReadFile = fopen( "E://mytest.txt", "r" ); // 打開文件
if ( pReadFile == NULL )
return 0;
while ( ( cInput = fgetc( pReadFile ) ) != EOF ) // 從文件讀入一個字符,如果到文件尾部,則返回EOF(-1)
printf( "%c", cInput );
fclose( pReadFile ); // 關閉文件
return 0;
}
假如mytest.txt文件的內容是:
masefee
hello
world
三行,那麼我們逐個讀入每個字符,直到EOF結束,EOF很簡單,其實就是#define EOF (-1),WINDOWS爲了能夠返回失敗爲-1,因此fgetc的返回值使用是int類型。同時-1也不是某個字符的ASCII,所以不影響,一舉兩得。上面程序while循環不斷從文件中讀取單個字符,遇到換行符(WINDOWS下回車符('/r')爲13, 換行符('/n')爲10),printf輸出後變處理成換行符了,因此文件裏面3行,逐個讀入程序裏在終端顯示後還是3行。代碼很簡單,就不用多說了。這裏需要提到一點:
問題一:當第一次執行了fgetc後,我們看看pReadFile指針裏面的內容與剛執行了fopen函數後的內容有所變化,爲什麼?
再來看fputc函數:
#include <stdio.h>
int main( void )
{
int i = 0;
char szOutput[ 32 ] = "masefee/nhello";
FILE* pWriteFile = fopen( "E://mytest.txt", "w" ); // 打開文件
if ( pWriteFile == NULL )
return 0;
while ( szOutput[ i ] != 0 )
{
fputc( szOutput[ i ], pWriteFile ); // 寫入一個字符到文件
i++;
}
fclose( pWriteFile ); // 關閉文件
return 0;
}
我特意在szOutput數組裏寫了一個'/n'字符,此字符就是換行符newline,意圖是當輸出到e之後,便輸出一個換行符,讓字符串換行。因此最終mytest.txt文件裏面的內容如下:
masefee
hello
到這裏,你可能會想到第一個fgetc的例子是我們預先在文件中輸入3行字符,然後讀入到程序中。我們在用記事本輸入3行文本的時候,每當換行的時候我們敲鍵盤是按的回車。
問題二:既然我們敲的是回車,爲什麼在文件裏存儲的是'/n'而不是'/r'?
同時,到這裏想到第一個問題,我們又來觀察一下,當剛使用fopen函數時,pWriteFile裏面的內容是:
pWriteFile 0x00437bb0
_ptr 0x00000000
_cnt 0
_base 0x00000000
_flag 2
_file 3
_charbuf 0
_bufsiz 0
_tmpfname 0x00000000
而執行了fputs函數,到換行符後我們再看pWriteFile裏面的內容:
pWriteFile 0x00437bb0
_ptr 0x00385019
_cnt 4087
_base 0x00385010
_flag 10
_file 3
_charbuf 0
_bufsiz 4096
_tmpfname 0x00000000
然後我們再看看_base所在內存的值:
6d 61 73 65 66 65 65 0a 68
m a s e f e e /n h
從這個現象我們能夠意識到,FILE結構裏面_base所指向的緩衝區,_cnt表示還剩下多少個字節沒有寫。還可以意識到,我們在不設置任何參數時,默認情況下是採用的全緩衝模式,填充4096字節後自動會寫入到文件,在這裏我們沒有那麼多字節,因此在fclose函數執行後,文件裏便寫入了值。你可以打斷點在fclose上,等程序斷下來後,觀察你磁盤裏面的mytest.txt是空的,當執行了fclose後大小就變了。這也能體現緩衝區的一個現象。
同樣,如果你想立即將緩衝區的數據寫到文件裏,可以在fclose函數前面加上:
fflush( pWriteFile );
當執行完此函數後,數據便寫進了文件,最後再關閉文件。
【C++語言入門篇】系列: