1. 文件和流的關系
C將每個文件簡單地作為順序字節流(如下圖)。每個文件用文件結束符結束,或者在特定字節數的地方結束,這個特定的字節數可以存儲在系統維護的管理數據結構中。當打開文件時,就建立了和文件的關系。
在開始執行程序的時候,將自動打開3個文件和相關的流:標准輸入流、標准輸出流和標准錯誤。流提供了文件和程序的通信通道。例如,標准輸入流使得程序可以從鍵盤讀取數據,而標准輸出流使得程序可以在屏幕上輸出數據。打開一個文件將返回指向FILE結構(在stdio.h中定義)的指針,它包含用於處理文件的信息,也就是說,這個結構包含文件描述符。文件描述符是操作系統數組(打開文件列表的索引)。每個數組元素包含一個文件控制塊(FCB, File Control Block),操作系統用它來管理特定的文件。
標准輸入、標准輸出和標准錯誤是用文件指針stdin、stdout和stderr來處理的。
2. C語言文件操作的底層實現簡介
2.1 FILE結構體
C語言的stdio.h頭文件中,定義了用於文件操作的結構體FILE。這樣,我們通過fopen返回一個文件指針(指向FILE結構體的指針)來進行文件操作。可以在stdio.h(位於visual studio安裝目錄下的include文件夾下)頭文件中查看FILE結構體的定義,如下:
TC2.0中:
typedef struct { short level; /* fill/empty level of buffer */ unsigned flags; /* File status flags */ char fd; /* File descriptor */ unsigned char hold; /* Ungetc char if no buffer */ short bsize; /* Buffer size */ unsigned char *buffer; /* Data transfer buffer */ unsigned char *curp; /* Current active pointer */ unsigned istemp; /* Temporary file indicator */ short token; /* Used for validity checking */ } FILE; /* This is the FILE object */ VC6.0中: #ifndef _FILE_DEFINED struct _iobuf {
char *_ptr; //文件輸入的下一個位置
int _cnt; //當前緩沖區的相對位置
char *_base; //指基礎位置(即是文件的起始位置)
int _flag; //文件標志
int _file; //文件的有效性驗證
int _charbuf; //檢查緩沖區狀況,如果無緩沖區則不讀取
int _bufsiz; //
char *_tmpfname; //臨時文件名
};
typedef struct _iobuf FILE; #define _FILE_DEFINED #endif
2.2 C語言文件管理的實現
C程序用不同的FILE結構管理每個文件。程序員可以使用文件,但是不需要知道FILE結構的細節。實際上,FILE結構是間接地操作系統的文件控制塊
(FCB)來實現對文件的操作的,如下圖:
上面圖中的_file實際上是一個描述符,作為進入打開文件表索引的整數。
2.3 操作系統文件管理簡介
從2.2中的圖可以看出,C語言通過FILE結構可以間接操作文件控制塊(FCB)。為了加深對這些的理解,這里科普下操作系統對打開文件的管理。
文件是存放在物理磁盤上的,包括文件控制塊(FCB)和數據塊。文件控制塊通常包括文件權限、日期(創建、讀取、修改)、擁有者、文件大小、數據塊信息。數據塊用來存儲實際的內容。對於打開的文件,操作系統是這樣管理的:
1
|
系統維護了兩張表,一張是系統級打開文件表,一張是進程級打開文件表(每個進程有一個)。
|
系統級打開文件表復制了文件控制塊的信息等;進程級打開文件表保存了指向系統級文件表的指針及其他信息。
系統級文件表每一項都保存一個計數器,即該文件打開的次數。我們初次打開一個文件時,系統首先查看該文件是否已在系統級文件表中,如果不在,則創建該項信息,否則,計數器加1。當我們關閉一個文件時,相應的計數也會減1,當減到0時,系統將系統級文件表中的項刪除。
進程打開一個文件時,會在進程級文件表中添加一項。每項的信息包括當前文件偏移量(讀寫文件的位置)、存取權限、和一個指向系統級文件表中對應文件項的指針。系統級文件表中的每一項通過文件描述符(一個非負整數)來標識。
聯系2.2和2.3上面的內容,可以發現,應該是這樣的:FILE結構體中的_file成員應該是指向進程級打開文件表,然后,通過進程級打開文件表可以找到系統級打開文件表,進而可以通過FCB操作物理磁盤上面的文件。
2.4 文件操作的例子
#include<stdio.h>
int main() { printf("Hello World!\n"); return 0; }
運行結果如下:
通過這個程序可以看出,應該是每打開一次文件,哪怕多次打開的都是同一個文件,進程級打開文件表中應該都會添加一個記錄。如果是打開的是同一個文件,這多條記錄對應着同一個物理磁盤文件。由於每一次打開文件所進行的操作都是通過進程級打開文件表中不同的記錄來實現的,這樣,相當於每次打開文件的操作是相對獨立的,這就是上面的程序的運行結果中,兩次讀取文件的結果是一樣的(而不是第二次讀取從第一次結束的位置進行)。
另外,還可以看出,程序運行的時候,默認三個流是打開的stdin,stdout和stderr,它們的_file描述符分別是0、1和2。也可以看出,該程序打開的文件描述符依次從3開始遞增。
3.順序訪問文件
3.1 順序寫入文件
先看一個例子:
#include <stdio.h>
int main() { int account;//賬號 char name[30];//賬號名 double balance;//余額 FILE *cfPtr; if ((cfPtr=fopen("clients.dat","w"))==NULL) { printf("File could not be opened.\n"); } else { printf("Enter the account, name and the balance:\n"); printf("Enter EOF to end input.\n"); printf("? "); scanf("%d%s%lf",&account,name,&balance); while(!feof(stdin)) { fprintf(cfPtr,"%d %s %.2f\n",account,name,balance); printf("? "); scanf("%d%s%lf",&account,name,&balance); } fclose(cfPtr); } return 0; }
運行結果:
從上面的例子中可以看出,寫入文件大致需兩步:定義文件指針和打開文件。
函數fopen有兩個參數:文件名和文件打開模式。文件打開模式‘w’說明文件時用於寫入的。如果以寫入模式打開的文件不存在,則fopen將創建該文件。如果打開現有的文件來寫入,則將拋棄文件原有的內容而沒有任何警告。在程序中,if語句用於確定文件指針cfPtr是否是NULL(沒有成功打開文件時fopen的返回值)。如果是NULL,則將輸出錯誤消息,然后程序終止。否則,處理輸入並寫入到文件中。
foef(stdin)用來確定用戶是否從標准輸入輸入了文件結束符。文件結束符通知程序沒有其他數據可以處理了。foef的參數是指向測試是否為文件結束符的FILE指針。一旦輸入了文件結束符,函數將返回一個非零值;否則,函數返回0。當沒有輸入文件結束符時,程序繼續執行while循環。
fprintf(cfPtr,"%d %s %.2f\n",account,name,balance);向文件clients.dat中寫入數據。稍后通過用於讀取文件的程序,就可以提取數據。函數fprintf和printf等價,只是fprintf還需要一個指向文件的指針,所有數據都寫入到這個文件中。
在用戶輸入文件結束之后,程序用fclose關閉clients.dat文件,並結束運行。函數fclose也接收文件指針作為參數。如果沒有明確地調用函數fclose,則操作系統通常在程序執行結束的稍后關閉文件。這是操作系統“內務管理”的一個示例,但是,這樣可能會帶來一些難以預料的問題,所以一定要注意在使用結束之后關閉文件。
3.2 文件打開模式
模式 | 說明 |
r | 打開文件,進行讀取。 |
w | 創建文件,以進行寫入。如果文件已經存在,則刪除當前內容。 |
a | 追加,打開或創建文件以在文件尾部寫入。 |
r+ | 打開文件以進行更新(讀取和寫入)。 |
w+ | 創建文件以進行更新。如果文件已經存在,則刪除當前內容。 |
a+ | 追加,打開或者創建文件以進行更新,在文件尾部寫入。 |
3.3 順序讀取文件
下面的例子讀取的是上一個例子中寫入數據生成的文件。
上面的例子中,只需將第一個例子中的文件打開模式從w變為r,就可以打開文件讀取數據。
同樣地,fscanf(cfPtr,"%d%s%lf",&account,name,&balance);函數從文件中讀取一條記錄。函數fscanf和函數scanf等價看,只是fscanf接收將從中讀取數據的文件指針作為參數。在第一次執行前面的語句時,account的值為100,name的值是Jones,而balance等於24.98。每次執行第二條fscanf語句時,將從文件中讀取另一條記錄,而account,name和balance將有新值。當到達文件結束位置時,關閉文件,而程序終止。
要從文件中順序檢索數據,程序通常從文件的開始來讀取,而且連續讀取所有數據,直至找到期望的數據。在程序執行過程中,有可能會多次處理文件中的數據(重新從文件的開頭處理數據)。這時候就要用到函數rewind(cfPtr);,它可以使程序的文件位置指針(表示文件中將要讀取或者寫入的下一個字節的位置)重新設置到文件的開頭(也就是偏移量為0的字節)。注意,文件位置指針並不是指針,它是指定文件中將進行下一次讀取或者寫入的位置的整數值,有時候也稱其為文件偏移量,它是FILE結構的成員。
4.隨機訪問文件
文件中用格式化輸入函數fprintf所創建的記錄的長度並不是完全一致的。然而,在隨機訪問文件中,單個記錄的長度通常是固定的,而且可以直接訪問(這樣速度更快)而無需通過其他記錄來查找。這使得隨機文件訪問適合飛機訂票系統,銀行系統,銷售點系統和其他需要快速訪問特定數據的事務處理系統。我們可以有很多方法來實現隨機訪問文件,但是這里我們將把討論的范圍限制在使用固定長度記錄的簡單方法上。
函數fwrite把從內存中特定位置開始的指定數量的字節寫入到文件位置指針指定的文件位置,函數fread從文件位置指針指定的文件位置處把指定數量的字節復制到指定的內存位置。fwrite和fread可以從磁盤上讀取數據數組,以及向磁盤上寫入數據數組。fread和fwrite的第三個參數是從磁盤中讀取或者寫入到磁盤上的數組元素的個數。
文件處理程序很少向文件中寫入字段。通常情況下,它們一次寫入一個struct。
4.1 創建隨機訪問的文件
#include<stdio.h>
struct clientData { int acctNum; char lastName[15]; char firstName[10]; double balance; }; int main() { int i; struct clientData blankClient={0,"","",0.0}; FILE *cfPtr; if ((cfPtr = fopen("credit.dat","wb"))== NULL) { printf("File could not be opened.\n"); } else { for (i=1;i<=100;i++) { fwrite(&blankClient,sizeof(struct clientData),1,cfPtr); } fclose(cfPtr); } return 0; }
fwrite(&blankClient,sizeof(struct clientData),1,cfPtr);用於向文件中寫入一個數據塊,其會在cfPtr指向的文件中寫入大小為sizeof(struct clientData)的結構blankClient。當然,也可以寫入對象數組的多個元素,只需把數組名傳給第一個參數,把要寫入的元素個數寫入第三個參數即可。
4.2 隨機向隨機訪問文件中寫入數據
運行結果:
fseek(cfPtr,(client.acctNum-1)*sizeof(struct clientData),SEEK_SET);將cfPtr所引用文件的位置指針移動到由(client.acctNum-1)*sizeof(struct clientData)計算所得到的字節位置處,這個表達式的值稱為偏移量或者位移。負號常量SEEK_SET說明,文件位置指針指向的位置是相對於文件開頭的偏移量。
ANSI標准制定了fseek的函數原型為int fseek(FILE *stream, long int offset, int whence);其中offset是stream指向的文件中從位置whence開始的字節數。參數whence可以有三個值:SEEK_SET, SEEKCUR或者SEEK_END,分別對應文件的開頭當前位置和結尾。
4.2 從隨機訪問文件中讀取數據