C語言檔案隨機存取fseek()和ftell()函數

2020-07-16 10:04:28
檔案隨機存取是指在某個檔案內直接讀寫任何給定位置資料的能力。通過獲取與設定檔案位置指示符可以實現這一功能,檔案位置指示符指定了檔案中的當前存取位置,該檔案與一個給定的流關聯。

獲取當前檔案位置

下面的函數返回當前檔案的存取位置。當需要標記檔案中的位置,以便以後返回到該位置時,可以使用下面的函數。
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;                          // 設定檔案位置出錯