檔案隨機存取是指在某個檔案內直接讀寫任何給定位置資料的能力。通過獲取與設定
檔案位置指示符可以實現這一功能,
檔案位置指示符指定了檔案中的當前存取位置,該檔案與一個給定的流關聯。
獲取當前檔案位置
下面的函數返回當前檔案的存取位置。當需要標記檔案中的位置,以便以後返回到該位置時,可以使用下面的函數。
long ftell(FILE*fp);
ftell()返回 fp 流的檔案位置。對一個二進位制流來說,它與該位置之前的字元數量是相同的,也就是當前字元位置距離檔案頭部的偏差。當發生錯誤時,ftell()返回 -1。
int fgetpos(FILE*restrict fp,fpos_t*restrict ppos);
fgetpos()將 fp 流的檔案位置指示符寫入 ppos 所參照的物件,該物件型別為 fpos_t。如果 fp 是一個寬字元導向流,那麼 fgetpos()所儲存的指示符也會包含流當前的轉換狀態。當發生錯誤時,fgetpos()返回非 0 值;當執行成功時,返回 0。
下面的範例記錄檔案 messages.txt 中以 # 字元開頭的所有行的位置:
#define ARRAY_LEN 1000
long arrPos[ARRAY_LEN] = { 0L };
FILE *fp = fopen( "messages.txt", "r" );
if ( fp != NULL)
{
int i = 0, c1 = 'n', c2;
while ( i < ARRAY_LEN && ( c2 = getc(fp) ) != EOF )
{
if ( c1 == 'n' && c2 == '#' )
arrPos[i++] = ftell( fp ) - 1;
c1 = c2;
}
/* ... */
}
設定檔案存取位置
下面的函數修改檔案位置指示符。
int fsetpos(FILE*fp,const fpos_t*ppos);
將檔案位置指示符和轉換狀態設定成 ppos 所參照物件中儲存的值。ppos 所參照物件內的這些值必須通過呼叫函數 fgetpos()才能獲得。如果成功,fsetpos()返回 0,並清除該流的 EOF 標記。如果發生錯誤,則返回非 0 值。
int fseek(FILE*fp,long offset,int origin);
將檔案位置指示符設定為以引數 origin 作為參考點,offset 作為偏差。
三種可能的參考點均被定義為宏值,引數 offset 指定位置只可能是相對這三種參考點中的一種。
表 1 列出了這些宏,以及在 ANSI C 定義它們之前,曾用於 origin 的傳統取值。這些 offset 值可以是負的,但是,最終結果所獲得的檔案位置必須大於等於 0。
表1 fseek中的引數origin
宏名稱 |
origin的傳統取值 |
偏差相對於的參考點 |
SEEK_SET |
0 |
檔案開頭 |
SEEK_CUR |
1 |
當前檔案位置 |
SEEK_END |
2 |
檔案結尾 |
當處理文字流時(在可區分文字流和二進位制流的系統上),應該使用通過呼叫函數 ftell()獲得的值作為 offset 引數,並且讓 origin 的值為 SEEK_SET。
函數 ftell()與 fseek()、fgetpos()與 fsetpos()並非互相相容的,因為 fgetpos()和 fsetpos()用來指示檔案位置的 fpos_t 物件,可以不是算術型別。
如果成功的話,fseek()會清除流的 EOF 標記並返回 0。非 0 的返回值表示發生錯誤。函數 rewind()將檔案位置指示符設定成檔案開頭,並清除流的 EOF 與錯誤標記:
void rewind( FILE *fp );
如果不考慮對錯誤標記的影響,那麼呼叫 rewind(fp)等同於:
(void)fseek( fp, 0L, SEEK_SET )
如果該檔案已被以讀寫模式開啟,那麼在成功呼叫 fseek()、fsetpos()或 rewind()之後,就可以進行讀寫操作。
下面的例子使用一個索引表來儲存檔案中記錄的位置。這個方法允許直接地存取需要被更新的記錄。
// setNewName():在索引表中找關鍵字,並且更新檔案中關鍵字所對應的記錄
// 包含這些記錄的檔案,必須以“讀寫模式”開啟;也就是採用模式字串"r+b"
// 引數:—指向被開啟資料檔案的指標;—關鍵字;—新名稱
// 返回值:指向更新記錄的指標,當未找到時,返回NULL
// ---------------------------------------------------------------
#include <stdio.h>
#include <string.h>
#include "Record.h" // 定義型別Record_t, IndexEntry_t:
// typedef struct { long key; char name[32];
// /* ... */ } Record_t;
// typedef struct { long key, pos; } IndexEntry_t;
extern IndexEntry_t indexTab[]; // 索引表
extern int indexLen; // 表條目的數量
Record_t *setNewName( FILE *fp, long key, const char *newname )
{
static Record_t record;
int i;
for ( i = 0; i < indexLen; ++i )
{
if ( key == indexTab[i].key )
break; // 找到指定的鍵
}
if ( i == indexLen )
return NULL; // 沒有找到
// 將檔案位置設定到該記錄:
if (fseek( fp, indexTab[i].pos, SEEK_SET ) != 0 )
return NULL; // 定位失敗
// 讀取記錄:
if ( fread( &record, sizeof(Record_t), 1, fp ) != 1 )
return NULL; // 讀取錯誤
if ( key != record.key ) // 測試鍵值
return NULL;
else
{ // 更新記錄
size_t size = sizeof(record.name);
strncpy( record.name, newname, size-1 );
record.name[size-1] = '';
if ( fseek( fp, indexTab[i].pos, SEEK_SET ) != 0 )
return NULL; // 設定檔案位置出錯
if ( fwrite( &record, sizeof(Record_t), 1, fp ) != 1 )
return NULL; // 寫入檔案出錯
return &record;
}
}
在寫操作之前的第二個 fseek()呼叫,可以用下面程式碼替換,以相對於之前的位置,移動檔案指標:
if (fseek( fp, -(long)sizeof(Record_t), SEEK_CUR ) != 0 )
return NULL; // 設定檔案位置出錯