1. 程式人生 > >C語言中對檔案操作的小結

C語言中對檔案操作的小結

在C語言中,檔案的操作是通過FILE結構體進行了,具體實現時,先利用fopen返回一個指向FILE結構體的指標:
FILE *fopen( const char *filename, const char *mode );
filename:檔名,mode:開啟的模式,規定了是可讀、可寫、追加之類的屬性。
"r":可讀,如果檔案不存在,fopen呼叫失敗
"w":可寫,如果檔案存在,那麼原來的內容會被銷燬。
"a":在檔案尾追加,在新的資料寫到檔案裡之前,不改變EOF標記,如果檔案不存在,建立一個新的檔案。
"r+":可讀可寫,檔案必須存在。
"w+":開啟一個空檔案用來讀寫,如果檔案存在,則內容被銷燬。
"a+":可讀可追加,在新的資料寫到檔案裡之前,改變EOF標記;如果檔案不存在,建立一個新的檔案。
如果呼叫失敗,返回一個空指標。

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
buffer:寫入檔案的內容;size:每一項的大小;count:寫入了多少項;stream:指向檔案的指標。返回值為寫入的總的位元組數。

先看一個簡單的例子:

int main()
{
	FILE *pFile = fopen("1.txt","w");
	fwrite("hello,world!",1,strlen("hello,world!"),pFile);
	return 0;
}
這個程式看似平平,但還是大有文章可做:如果單步除錯的話,執行完pFile後,就會在程式對應的資料夾下面產生一個名為“1.txt”的檔案(大小為0);執行完fwrite後,大小還是為0,;只有當程式完全執行完以後,hello,world!才會被寫入檔案。這是為什麼呢?因為C語言對檔案的操作使用了緩衝檔案系統:為每個正在使用的檔案開闢了一段記憶體,當我們向硬碟上寫資料時,是先寫到記憶體裡的(這與相容DC有異曲同工之妙),直到記憶體滿了,或者是我們通知系統要關閉這個檔案了,才把記憶體裡的資料拷貝到硬碟上。為什麼這麼設計呢?因為記憶體之間的操作速度要遠遠快於記憶體到硬碟的速度。如果我們每寫一個字元後就把它儲存在硬碟上,代價太大了。回過頭來繼續看,當整個程式執行完後,關閉這個檔案,此時,才會把記憶體裡的資料拷貝到硬碟上。


其實,我們完全可以手動的關閉檔案:
int fclose( FILE *stream );
stream:指向檔案的指標。如果成功,返回0;失敗返回EOF。這樣,當執行完關閉檔案後,檔案裡面就有值了。


有時候,我們會反覆讀寫一個檔案,而且每次讀寫後都希望立即看到結果。這時候每次讀完就關閉,然後重新開啟的話實在太麻煩了,有沒有簡單的辦法呢?可以使用fflush來重新整理流:
int fflush( FILE *stream );
stream:指向檔案的指標。


如果我們在接著向檔案中寫入資料:
fwrite("歡迎訪問",1,strlen("歡迎訪問"),pFile);
我們會發現,新寫入的資料會在原來檔案的末尾後加上。可系統是如何知道原來檔案的末尾在哪裡呢?
我們先看看FILE結構體:
struct _iobuf {
        char *_ptr;	//檔案輸入的下一個位置 
        int   _cnt;	//當前緩衝區的相對位置 
        char *_base;	//指基礎位置(應該是檔案的其始位置) 
        int   _flag;	//檔案標誌
        int   _file;	//檔案的有效性驗證
        int   _charbuf;	//檢查緩衝區狀況,如果無緩衝區則不讀取
        int   _bufsiz;	//檔案的大小
        char *_tmpfname;//臨時檔名
        };
typedef struct _iobuf FILE;
注意,這只是VS2010中對FILE的實現!標準庫中並沒有規定FILE中必須是什麼樣的,之規定了我們可通過哪些函式去呼叫、訪問它。但是從這個例項中,我們可以看出:結構體的第一個成員是一個指向檔案輸出的下一個位置的指標,我們可以通過fseek來移動檔案的指標,它指向檔案下一個要寫入的位置:

int fseek( FILE *stream, long offset, int origin );
stream:指向檔案的指標,offset:偏移量;origin:初始位置,它有3種取法:
SEEK_CUR:當前位置
SEEK_END:檔案尾
SEEK_SET:檔案頭


如果我們這樣使用:

	fwrite("hello,world!",1,strlen("hello,world!"),pFile);
	fflush(pFile);
	fseek(pFile,0,SEEK_SET);
	fwrite("歡迎訪問",1,strlen("歡迎訪問"),pFile);
那麼檔案中的內容就變為了:“歡迎訪問rld!”

我們使用ftell函式獲取當前檔案指標的位置:
long ftell( FILE *stream );
返回值為與檔案頭的偏移量。
通過它,我們可以獲得檔案的長度。

讀取檔案使用的是fread函式:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
buffer指明瞭讀取的檔案儲存在哪裡,size表明每個項的大小,count表明了讀多少項,stream是指向FILE型別的指標。返回值為實際讀取的位元組數。

當然,在你讀取檔案之前,最好確保檔案指標指向的是檔案的頭部,通過rewind函式來讓指標復位:
void rewind( FILE *stream );
stream:指向檔案的指標
下面看一個綜合的例子:
int main()
{
	FILE *pFile = fopen("1.txt","w+");
	fwrite("hello,world!",1,strlen("hello,world!"),pFile);
	fflush(pFile);
	fseek(pFile,0,SEEK_SET);		//檔案指標設為起點,寫操作覆蓋原來的內容
	fwrite("歡迎訪問",1,strlen("歡迎訪問"),pFile);
	fseek(pFile,0,SEEK_END);		//檔案指標設為終點
	int len = ftell(pFile);			//獲取檔案位元組數
	char* ch = (char*)malloc(sizeof(char)* (len+1));	//分配記憶體,多一個位元組
	memset(ch,0,(len+1));			//清0
//	fseek(pFile,0,SEEK_SET);		//檔案指標指向頭部
	rewind(pFile);
	fread(ch,1,len,pFile);			//讀取檔案
	fclose(pFile);
	printf("%s",ch);				
	return 0;
}
需要說明一點,因為%s是遇到一個空字串後才停止的,所以我們分配的記憶體比檔案的位元組數多了一個,然後全部清0,這樣完成fread以後,剩下的那個位元組剛好為0,用來表示字串的結束。




基本的內容就是這麼多,下面看一個細節問題:
int main()
{
	FILE *pFile = fopen("2.txt","w+");
	char ch[3];
	ch[0] = 'a';
	ch[1] = 10;		
	ch[2] = 'b';
	fwrite(ch,1,3,pFile);
	fflush(pFile);
	char buf[100];
	memset(buf,0,100);
	rewind(pFile);
	fread(buf,1,3,pFile);
	fclose(pFile);
	printf("%s",buf);
	return 0;
}
我們明明寫了3個字元,為什麼檔案大小會是4個位元組呢?
我們看看檔案的16進位制:61 0D 0A 62 ,其中61、62對應的是a和b,0A對應的是10,那麼0D對應的是什麼呢?答案是回車字元,這個字元是系統自動加進去的。而在讀取檔案時,我們也並沒有讀取個位元組,只用讀取3個位元組,就能正確獲取內容了。
與這個問題相關的一組概念是:二進位制檔案和文字檔案。C語言中,預設是以文字的方式開啟檔案的,如果我們使用二進位制檔案方式開啟:
FILE *pFile = fopen("2.txt","w+b");
也不會出什麼問題,只不過檔案的大小為3個位元組,對應的16進製為:61 0A 62。但是如果你在寫入時使用的是文字檔案,而讀取時使用的是二進位制檔案,就會出錯,因為它會把回車當做一個字元輸出。總而言之,讀和寫的方式要對應。