C语言文件随机访问fseek()和ftell()函数

文件随机访问是指在某个文件内直接读写任何给定位置数据的能力。通过获取与设定文件位置指示符可以实现这一功能,文件位置指示符指定了文件中的当前访问位置,该文件与一个给定的流关联。

获取当前文件位置

下面的函数返回当前文件的访问位置。当需要标记文件中的位置,以便以后返回到该位置时,可以使用下面的函数。
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] = '\0';

    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;                          // 设定文件位置出错