C Posição no Arquivo (fseek e ftell)

Quando você lê um arquivo, o C anda por ele como um dedo acompanhando as linhas de um livro: cada leitura começa onde a anterior parou. Até aqui, o dedo sempre andou para a frente, do começo ao fim. Nesta aula você vai aprender a perguntar onde o dedo está (ftell), a pular para qualquer lugar (fseek) e a voltar ao começo (rewind). Com isso, a Ana vai descobrir o tamanho do arquivo do estoque, ler direto o 3º produto e corrigir a quantidade de um produto sem regravar o arquivo inteiro.

Você vai aprender
  • O que é a posição de um arquivo aberto e como ela anda a cada leitura.
  • Como ver a posição com ftell e mudá-la com fseek, a partir do começo (SEEK_SET), do lugar atual (SEEK_CUR) ou do fim (SEEK_END).
  • Como descobrir o tamanho de um arquivo e voltar ao começo com rewind.
  • Como ler o registro número N de um arquivo binário com uma conta simples.
  • Como abrir com "r+b" e atualizar um registro no lugar.
  • Os erros clássicos: esquecer de voltar antes de gravar, abrir com "w+b" e apagar tudo, pular para fora do arquivo.

Os exemplos usam o estoque.bin da aula Arquivos Binários: 3 produtos de 32 bytes cada (Caderno, Caneta e Borracha), 96 bytes no total. Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: o marcador de páginas

Pense num livro com um marcador. Quando você abre o arquivo, o marcador está na página 1 (posição 0). Cada leitura avança o marcador o tanto que foi lido. Você pode:

  • olhar em que página o marcador está (ftell, "file tell", diga a posição);
  • mudar o marcador de lugar sem ler nada (fseek, "file seek", procure no arquivo);
  • voltar o marcador para a primeira página (rewind, "rebobinar", como uma fita).

No C, a posição é contada em bytes a partir do começo do arquivo. O primeiro byte é a posição 0. Num arquivo de 96 bytes, as posições vão de 0 a 95, e a posição 96 significa "logo depois do último byte", ou seja, o fim.


1. ftell: onde estou?

O programa abaixo mostra a posição logo depois de abrir, pula para o fim e volta ao começo:

#include <stdio.h>

int main(void) {
    FILE *f = fopen("estoque.bin", "rb");
    if (f == NULL) {
        printf("Não consegui abrir estoque.bin\n");
        return 1;
    }

    printf("Ao abrir, a posição é %ld\n", ftell(f));

    fseek(f, 0, SEEK_END);
    long tamanho = ftell(f);
    printf("No fim, a posição é %ld\n", tamanho);

    rewind(f);
    printf("Depois do rewind: %ld\n", ftell(f));

    fclose(f);
    return 0;
}
ftell(f)
Devolve a posição atual, em bytes, como um long (por isso o formato %ld). Se der erro, devolve -1.
fseek(f, 0, SEEK_END)
Leve o marcador para "0 bytes a partir do fim": exatamente o fim do arquivo.
long tamanho = ftell(f);
No fim, a posição é igual ao tamanho do arquivo em bytes. Esse é o jeito clássico de medir um arquivo em C.
rewind(f)
Volta para a posição 0. É o mesmo que fseek(f, 0, SEEK_SET), e ainda limpa o indicador de erro do arquivo.
$ gcc -std=c23 -Wall -Wextra tamanho.c -o tamanho
$ ./tamanho
Ao abrir, a posição é 0
No fim, a posição é 96
Depois do rewind: 0
$ ls -l estoque.bin
-rw-rw-r-- 1 ana ana 96 out 17 10:33 estoque.bin       « o mesmo número

2. A posição anda sozinha a cada leitura

Agora veja o marcador andando enquanto o programa lê os produtos um por um, e depois voltando um registro com SEEK_CUR:

#include <stdio.h>

typedef struct {
    char nome[20];
    int quantidade;
    double preco;
} Produto;

int main(void) {
    FILE *f = fopen("estoque.bin", "rb");
    if (f == NULL) {
        printf("Não consegui abrir estoque.bin\n");
        return 1;
    }

    Produto p;
    printf("posição %2ld: ", ftell(f));
    while (fread(&p, sizeof p, 1, f) == 1) {
        printf("li %s\n", p.nome);
        printf("posição %2ld: ", ftell(f));
    }
    printf("fim do arquivo\n");

    fseek(f, -(long)sizeof p, SEEK_CUR);
    printf("voltei um registro: posição %ld\n", ftell(f));
    fread(&p, sizeof p, 1, f);
    printf("li de novo %s\n", p.nome);

    fclose(f);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra posicao.c -o posicao
$ ./posicao
posição  0: li Caderno
posição 32: li Caneta
posição 64: li Borracha
posição 96: fim do arquivo
voltei um registro: posição 64
li de novo Borracha
posição 0, 32, 64, 96
Cada fread de um Produto avançou 32 bytes, o sizeof(Produto). Ninguém mexeu na posição: o próprio fread a empurra para a frente.
fseek(f, -(long)sizeof p, SEEK_CUR)
Ande 32 bytes para trás a partir de onde está (SEEK_CUR, "current", atual). Deslocamento negativo = para trás; positivo = para a frente.
(long)
O sizeof dá um size_t, que não tem sinal. O cast para long (aula Conversão de Tipos) vem antes do sinal de menos, para o resultado ser -32 mesmo.

Os três pontos de partida

O fseek recebe três coisas: o arquivo, um deslocamento (quantos bytes) e o ponto de partida (a partir de onde contar):

Ponto de partidaConta a partir deExemploResultado no estoque.bin
SEEK_SETO começo do arquivo ("set", definir)fseek(f, 64, SEEK_SET)Posição 64: o 3º produto
SEEK_CURA posição atualfseek(f, -32, SEEK_CUR)Volta um produto
SEEK_ENDO fim do arquivofseek(f, -32, SEEK_END)Posição 64: o último produto

O fseek devolve 0 quando deu certo e um valor diferente de zero quando falhou (por exemplo, posição negativa). Os três nomes são constantes de <stdio.h>; não precisa de outro #include.

Experimente: o programa grava 5 notas e depois usa fseek/ftell para medir o arquivo e ler a nota da posição 2. Rode. Depois troque n para 4 e, em seguida, troque o fseek da leitura por fseek(f, -(long)sizeof(int), SEEK_END): qual nota aparece?

#include <stdio.h>

int main(void) {
    int notas[5] = {7, 9, 6, 10, 8};

    FILE *f = fopen("notas.bin", "wb");
    if (f == NULL) {
        printf("Não consegui criar notas.bin\n");
        return 1;
    }
    fwrite(notas, sizeof(int), 5, f);
    fclose(f);

    f = fopen("notas.bin", "rb");
    if (f == NULL) {
        printf("Não consegui abrir notas.bin\n");
        return 1;
    }

    fseek(f, 0, SEEK_END);
    long tamanho = ftell(f);
    printf("notas.bin tem %ld bytes = %ld notas\n", tamanho, tamanho / (long)sizeof(int));

    long n = 2;                                // a 3ª nota (começa em 0)
    fseek(f, n * (long)sizeof(int), SEEK_SET);
    int nota;
    if (fread(&nota, sizeof nota, 1, f) == 1) {
        printf("Nota na posição %ld: %d\n", n, nota);
    }

    fclose(f);
    return 0;
}

3. Ler o registro N

Aqui está a grande vantagem de um arquivo binário com registros de tamanho fixo: para ler o produto número N, não precisa ler os anteriores. A posição é uma conta:

posição = (N - 1) × sizeof(Produto)   (contando os produtos a partir de 1)

O 1º produto começa no byte 0, o 2º no byte 32, o 3º no byte 64. É como achar a casa número 300 numa rua em que todos os terrenos têm a mesma largura: você não precisa contar as casas, só multiplicar.

#include <stdio.h>

typedef struct {
    char nome[20];
    int quantidade;
    double preco;
} Produto;

int main(void) {
    FILE *f = fopen("estoque.bin", "rb");
    if (f == NULL) {
        printf("Não consegui abrir estoque.bin\n");
        return 1;
    }

    fseek(f, 0, SEEK_END);
    long total = ftell(f) / (long)sizeof(Produto);

    printf("Qual produto (1 a %ld)? ", total);
    long n;
    if (scanf("%ld", &n) != 1 || n < 1 || n > total) {
        printf("Número inválido.\n");
        fclose(f);
        return 1;
    }

    fseek(f, (n - 1) * (long)sizeof(Produto), SEEK_SET);
    Produto p;
    if (fread(&p, sizeof p, 1, f) != 1) {
        printf("Não consegui ler o produto %ld.\n", n);
        fclose(f);
        return 1;
    }
    printf("%s: %d unidades a R$ %.2f\n", p.nome, p.quantidade, p.preco);

    fclose(f);
    return 0;
}
  1. Quantos produtos existem? fseek até o fim, ftell dá 96, e 96 / 32 = 3.
  2. Pergunta e confere. O scanf precisa devolver 1 (leu um número) e o número tem de estar entre 1 e o total. Sem essa conferência, o programa pularia para fora do arquivo (veja Quando dá errado).
  3. Pula direto. Para o produto 3, (3 - 1) * 32 = 64. O SEEK_SET conta a partir do começo.
  4. Lê um registro e confere o retorno.
$ gcc -std=c23 -Wall -Wextra registro.c -o registro
$ ./registro
Qual produto (1 a 3)? 3
Borracha: 25 unidades a R$ 1.75
$ ./registro
Qual produto (1 a 3)? 7
Número inválido.
$ ./registro
Qual produto (1 a 3)? abc
Número inválido.

Com 3 produtos isso parece pouca coisa, mas num arquivo com um milhão de registros a diferença é enorme: o programa lê 32 bytes, e não 32 milhões.


4. Atualizar um registro no lugar ("r+b")

A Ana vendeu 15 canetas. Ela quer mudar só a quantidade do 2º produto, sem reescrever o arquivo todo. Para isso o arquivo precisa estar aberto para ler e escrever ao mesmo tempo, sem ser apagado: o modo "r+b".

ModoLê?Grava?Se o arquivo existeSe não existe
"rb"simnãoabre no começofalha (NULL)
"r+b" (ou "rb+")simsimmantém o conteúdo, começa na posição 0falha (NULL)
"w+b" (ou "wb+")simsimapaga tudocria
"a+b" (ou "ab+")simsó no fimmantém; toda escrita vai para o fimcria
#include <stdio.h>

typedef struct {
    char nome[20];
    int quantidade;
    double preco;
} Produto;

int main(void) {
    FILE *f = fopen("estoque.bin", "r+b");
    if (f == NULL) {
        printf("Não consegui abrir estoque.bin\n");
        return 1;
    }

    long n = 1;                       // o 2º produto (a contagem começa em 0)
    long pos = n * (long)sizeof(Produto);

    Produto p;
    fseek(f, pos, SEEK_SET);
    if (fread(&p, sizeof p, 1, f) != 1) {
        printf("Produto não encontrado.\n");
        fclose(f);
        return 1;
    }
    printf("Antes:  %s, %d unidades\n", p.nome, p.quantidade);

    p.quantidade -= 15;               // a Ana vendeu 15 canetas

    fseek(f, pos, SEEK_SET);          // volta para o começo do registro
    fwrite(&p, sizeof p, 1, f);
    printf("Depois: %s, %d unidades\n", p.nome, p.quantidade);

    fclose(f);
    return 0;
}
"r+b"
Leitura e escrita, binário, sem apagar. Se o arquivo não existir, o fopen devolve NULL.
long pos = n * (long)sizeof(Produto);
A posição do registro, guardada numa variável porque vamos precisar dela duas vezes.
fseek … fread
Pula até o registro e lê. Depois do fread, o marcador está em pos + 32: no começo do próximo produto.
fseek(f, pos, SEEK_SET); // volta
A linha mais importante: volta o marcador para o começo do registro lido. Sem ela, o fwrite gravaria por cima do produto seguinte.
fwrite(&p, sizeof p, 1, f)
Grava os 32 bytes por cima dos antigos. O resto do arquivo não muda e o tamanho continua 96.
$ gcc -std=c23 -Wall -Wextra atualizar.c -o atualizar
$ cp estoque.bin backup.bin               « uma cópia, para poder repetir
$ ./atualizar
Antes:  Caneta, 40 unidades
Depois: Caneta, 25 unidades
$ ls -l estoque.bin
-rw-rw-r-- 1 ana ana 96 out 17 10:37 estoque.bin      « continua com 96 bytes
$ ./ler1
Caderno     12 x R$ 15.90
Caneta      25 x R$  2.50
Borracha    25 x R$  1.75
Valor do estoque: R$ 297.05
$ xxd -s 32 -l 32 estoque.bin
00000020: 4361 6e65 7461 0000 0000 0000 0000 0000  Caneta..........
00000030: 0000 0000 1900 0000 0000 0000 0000 0440  ...............@

O ler1 (da aula anterior) confirma a mudança. No xxd, as opções -s 32 (seek, pule 32 bytes) e -l 32 (length, mostre 32) são o fseek do terminal: mostram só o 2º registro. A quantidade, que era 28 (40), agora é 19 (25 em hexadecimal).

Regra do C para os modos "+": depois de ler, chame fseek (ou rewind) antes de gravar; depois de gravar, chame fseek ou fflush antes de ler. Mesmo que você queira continuar na mesma posição, use fseek(f, 0, SEEK_CUR). A biblioteca usa um buffer compartilhado para leitura e escrita, e o fseek é o momento em que ela arruma esse buffer. Sem ele, o resultado não é garantido pelo padrão.

Formas diferentes, e qual usar

Para…FormaObservação
Voltar ao começorewind(f) ou fseek(f, 0, SEEK_SET)rewind é mais curto, mas não devolve nada; o fseek permite conferir o retorno
Ir para o fim (para acrescentar)fseek(f, 0, SEEK_END) ou abrir com "ab"Com "ab", toda escrita vai para o fim automaticamente
Saber o tamanhofseek até o fim + ftellFunciona em arquivos comuns. Em POSIX existe também stat(), que não precisa abrir o arquivo
Guardar uma posição e voltar depoislong p = ftell(f); … fseek(f, p, SEEK_SET);Para arquivos gigantes, fgetpos/fsetpos
Arquivos de texto e arquivos gigantes.
  • Em arquivos abertos em modo texto ("r"), o padrão C só garante fseek para o início, o fim ou uma posição que veio de um ftell anterior. No Linux e no macOS a posição é sempre em bytes, mas no Windows a troca de \n por \r\n atrapalha as contas. Para pular por conta, use modo binário.
  • O ftell devolve long. Onde o long tem 4 bytes (Windows, e o editor do guia), o maior valor é 2147483647: arquivos de mais de 2 GB não cabem. Para eles, o POSIX oferece fseeko/ftello (com o tipo off_t), e o C padrão oferece fgetpos/fsetpos.

Quando dá errado

Gravou por cima do produto seguinte: faltou voltar antes do fwrite

A Ana apagou a linha fseek(f, pos, SEEK_SET); // volta do atualizar.c e rodou de novo, com o estoque original (Caneta com 40):

$ cp backup.bin estoque.bin
$ ./semvolta
Antes:  Caneta, 40 unidades
Depois: Caneta, 25 unidades
$ ./ler1
Caderno     12 x R$ 15.90
Caneta      40 x R$  2.50
Caneta      25 x R$  2.50
Valor do estoque: R$ 353.30

O que significa: o fread deixou o marcador no começo do 3º registro, e o fwrite gravou ali. A Borracha sumiu, a Caneta apareceu duas vezes e a original continua com 40. O programa não deu erro nenhum: só estragou o arquivo. (Além disso, gravar logo depois de ler, sem fseek, já desobedece à regra dos modos "+".)

Como consertar: antes de cada fwrite num registro que você acabou de ler, volte com fseek(f, pos, SEEK_SET) ou fseek(f, -(long)sizeof p, SEEK_CUR). E faça uma cópia do arquivo antes de testar programas que gravam.

"O arquivo tem 0 bytes": abriu com "w+b" no lugar de "r+b"
    FILE *f = fopen("estoque.bin", "w+b");   // queria ler e escrever...
    /* … */
    fseek(f, 0, SEEK_END);
    printf("O arquivo tem %ld bytes\n", ftell(f));
$ ./apagou
O arquivo tem 0 bytes
$ ls -l estoque.bin
-rw-rw-r-- 1 ana ana 0 out 17 10:37 estoque.bin

O que significa: todo modo com w apaga o arquivo no momento do fopen, com ou sem +. Os 96 bytes se foram antes de qualquer leitura.

Como consertar: para alterar um arquivo que já existe, use "r+b". Ele falha se o arquivo não existir, o que é bom: você fica sabendo. Se perdeu o arquivo, só uma cópia salva o dia (cp backup.bin estoque.bin).

Pulou para fora do arquivo: fseek não reclama de tudo
    int r = fseek(f, -32, SEEK_SET);
    printf("fseek para -32 devolveu %d; posição: %ld\n", r, ftell(f));

    r = fseek(f, 10 * 32, SEEK_SET);
    printf("fseek para 320 devolveu %d; posição: %ld\n", r, ftell(f));
    char buf[32];
    size_t lidos = fread(buf, sizeof buf, 1, f);
    printf("fread devolveu %zu\n", lidos);
$ ./negativo
fseek para -32 devolveu -1; posição: 0
fseek para 320 devolveu 0; posição: 320
fread devolveu 0

O que significa: uma posição negativa é impossível, e o fseek falha (devolve -1, e o motivo fica em errno: "Invalid argument"; veja errno). Já uma posição depois do fim é permitida: o fseek devolve 0, a posição vira 320 num arquivo de 96 bytes, e só o fread seguinte descobre que não há nada ali. Se você gravasse nessa posição, o arquivo cresceria até 352 bytes e o buraco no meio ficaria cheio de zeros.

Como consertar: confira o número do registro antes de pular, como o registro.c faz (n < 1 || n > total), e confira o retorno do fread.

O registro N veio "torto": conta sem o sizeof
    long n = 2;
    fseek(f, n, SEEK_SET);          // byte 2, e não registro 2!
    Produto p;
    if (fread(&p, sizeof p, 1, f) == 1) {
        printf("%s: %d unidades a R$ %.2f\n", p.nome, p.quantidade, p.preco);
    }
$ ./torto
derno: -858980352 unidades a R$ 33831004655561742192915845909075364043037996517412568492545798035386535233773788655356174056737560610406038487995706440750308981483203810083690603614203493220352.00

O que significa: se você escreve fseek(f, n, SEEK_SET) pensando em "registro n", o fseek entende "byte n". Com n = 2, a leitura começou no 3º byte do Caderno: o nome saiu cortado ("derno") e a quantidade e o preço foram montados com pedaços de campos diferentes, como no exemplo ler_errado da aula Arquivos Binários.

Como consertar: o deslocamento é sempre em bytes: n * (long)sizeof(Produto).


Referência rápida

EscritaO que faz
ftell(f)Posição atual em bytes (long); -1 se der erro
fseek(f, desloc, SEEK_SET)Vai para desloc bytes a partir do começo
fseek(f, desloc, SEEK_CUR)Anda desloc bytes a partir de onde está (negativo = para trás)
fseek(f, desloc, SEEK_END)Vai para desloc bytes a partir do fim (0 = o fim)
rewind(f)Volta para a posição 0 e limpa o indicador de erro
fseek(f, 0, SEEK_END); long t = ftell(f);Tamanho do arquivo em bytes
t / (long)sizeof(Reg)Quantos registros cabem no arquivo
fseek(f, n * (long)sizeof(Reg), SEEK_SET)Vai para o registro n (contando de 0)
fopen(nome, "r+b")Lê e grava sem apagar; exige fseek entre leitura e escrita

Exercícios

1. Ir para o fim

Complete para levar o marcador ao fim do arquivo:

fseek(f, 0, );

2. O registro 4

Cada registro tem 32 bytes e a contagem começa em 0. Em que posição começa o registro 4?

byte

3. Ler e gravar sem apagar

Complete o modo para abrir um arquivo binário existente para leitura e escrita, sem apagar o conteúdo:

FILE *f = fopen("jogo.sav", "");

4. O arquivo tem 200 bytes. Depois de fseek(f, -50, SEEK_END), quanto vale ftell(f)?

5. O programa leu um registro com fread, mudou um campo e chamou fwrite logo em seguida. O que acontece?

6. Qual chamada falha (devolve um valor diferente de 0)?

Missão final: corrigir a nota do Bruno

Missão: a professora errou a nota do Bruno: é 7.0, e não 6.0. No trecho marcado, abra turma.bin com "r+b", pule até o registro 1 com fseek, leia o aluno, troque a nota, volte para o começo do registro e grave. O resto do programa mostra a turma, que deve ficar assim:

Ana: 9.5
Bruno: 7.0
Carla: 8.0
Davi: 6.5
#include <stdio.h>

typedef struct {
    char nome[16];
    double nota;
} Aluno;

int main(void) {
    Aluno turma[4] = {
        {"Ana", 9.5}, {"Bruno", 6.0}, {"Carla", 8.0}, {"Davi", 6.5},
    };
    FILE *f = fopen("turma.bin", "wb");
    if (f == NULL) {
        printf("Não consegui criar turma.bin\n");
        return 1;
    }
    fwrite(turma, sizeof(Aluno), 4, f);
    fclose(f);

    Aluno a;
    // ---- corrija a nota do Bruno (registro 1) aqui ----

    // ----------------------------------------------------

    f = fopen("turma.bin", "rb");
    if (f == NULL) {
        printf("Não consegui abrir turma.bin\n");
        return 1;
    }
    while (fread(&a, sizeof a, 1, f) == 1) {
        printf("%s: %.1f\n", a.nome, a.nota);
    }
    fclose(f);
    return 0;
}
#include <stdio.h>

typedef struct {
    char nome[16];
    double nota;
} Aluno;

int main(void) {
    Aluno turma[4] = {
        {"Ana", 9.5}, {"Bruno", 6.0}, {"Carla", 8.0}, {"Davi", 6.5},
    };
    FILE *f = fopen("turma.bin", "wb");
    if (f == NULL) {
        printf("Não consegui criar turma.bin\n");
        return 1;
    }
    fwrite(turma, sizeof(Aluno), 4, f);
    fclose(f);

    Aluno a;
    // ---- corrija a nota do Bruno (registro 1) aqui ----
    f = fopen("turma.bin", "r+b");
    if (f == NULL) {
        printf("Não consegui abrir turma.bin\n");
        return 1;
    }
    long pos = 1 * (long)sizeof(Aluno);
    fseek(f, pos, SEEK_SET);
    if (fread(&a, sizeof a, 1, f) == 1) {
        a.nota = 7.0;
        fseek(f, pos, SEEK_SET);
        fwrite(&a, sizeof a, 1, f);
    }
    fclose(f);
    // ----------------------------------------------------

    f = fopen("turma.bin", "rb");
    if (f == NULL) {
        printf("Não consegui abrir turma.bin\n");
        return 1;
    }
    while (fread(&a, sizeof a, 1, f) == 1) {
        printf("%s: %.1f\n", a.nome, a.nota);
    }
    fclose(f);
    return 0;
}

Teste o erro de propósito: tire o segundo fseek da sua solução e rode. O Bruno continua com 6.0 e aparece um quinto aluno, "Bruno: 7.0", no fim da lista. No gcc do laboratório, o mesmo erro gravou por cima do registro seguinte (veja Quando dá errado). Duas bibliotecas, dois estragos diferentes para o mesmo código: é isso que "o padrão não garante o resultado" quer dizer na prática. A biblioteca do editor leu o arquivo inteiro para o buffer de uma vez, então, para o sistema, o marcador já estava no fim.


Resumo

  • Todo arquivo aberto tem uma posição (em bytes, começando em 0) que cada leitura e escrita empurra para a frente.
  • ftell(f) diz a posição; fseek(f, deslocamento, origem) muda, com origem SEEK_SET (começo), SEEK_CUR (atual) ou SEEK_END (fim); rewind(f) volta ao começo.
  • Tamanho do arquivo: fseek(f, 0, SEEK_END) e depois ftell(f).
  • Registro N de tamanho fixo: fseek(f, N * (long)sizeof(Reg), SEEK_SET). Confira se N existe antes.
  • Para alterar no lugar, abra com "r+b" (nunca "w+b", que apaga) e volte com fseek antes de gravar o que acabou de ler.
  • fseek para posição negativa falha; para depois do fim, não. Para arquivos de mais de 2 GB, use fseeko/ftello ou fgetpos/fsetpos.