文件随机访问是指在某个文件内直接读写任何给定位置数据的能力。通过获取与设定文件位置指示符可以实现这一功能,文件位置指示符指定了文件中的当前访问位置,该文件与一个给定的流关联。
下面的函数返回当前文件的访问位置。当需要标记文件中的位置,以便以后返回到该位置时,可以使用下面的函数。
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。
宏名称 | 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; // 设定文件位置出错