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.
- O que é a posição de um arquivo aberto e como ela anda a cada leitura.
- Como ver a posição com
ftelle mudá-la comfseek, 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
freadde umProdutoavançou 32 bytes, osizeof(Produto). Ninguém mexeu na posição: o própriofreada 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
sizeofdá umsize_t, que não tem sinal. O cast paralong(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 partida | Conta a partir de | Exemplo | Resultado no estoque.bin |
|---|---|---|---|
SEEK_SET | O começo do arquivo ("set", definir) | fseek(f, 64, SEEK_SET) | Posição 64: o 3º produto |
SEEK_CUR | A posição atual | fseek(f, -32, SEEK_CUR) | Volta um produto |
SEEK_END | O fim do arquivo | fseek(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(¬a, 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;
}
- Quantos produtos existem?
fseekaté o fim,ftelldá 96, e 96 / 32 = 3. - Pergunta e confere. O
scanfprecisa 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). - Pula direto. Para o produto 3,
(3 - 1) * 32= 64. OSEEK_SETconta a partir do começo. - 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".
| Modo | Lê? | Grava? | Se o arquivo existe | Se não existe |
|---|---|---|---|---|
"rb" | sim | não | abre no começo | falha (NULL) |
"r+b" (ou "rb+") | sim | sim | mantém o conteúdo, começa na posição 0 | falha (NULL) |
"w+b" (ou "wb+") | sim | sim | apaga tudo | cria |
"a+b" (ou "ab+") | sim | só no fim | mantém; toda escrita vai para o fim | cria |
#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
fopendevolveNULL. - 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á empos + 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
fwritegravaria 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).
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… | Forma | Observação |
|---|---|---|
| Voltar ao começo | rewind(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 tamanho | fseek até o fim + ftell | Funciona em arquivos comuns. Em POSIX existe também stat(), que não precisa abrir o arquivo |
| Guardar uma posição e voltar depois | long p = ftell(f); … fseek(f, p, SEEK_SET); | Para arquivos gigantes, fgetpos/fsetpos |
- Em arquivos abertos em modo texto (
"r"), o padrão C só garantefseekpara o início, o fim ou uma posição que veio de umftellanterior. No Linux e no macOS a posição é sempre em bytes, mas no Windows a troca de\npor\r\natrapalha as contas. Para pular por conta, use modo binário. - O
ftelldevolvelong. Onde olongtem 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 oferecefseeko/ftello(com o tipooff_t), e o C padrão oferecefgetpos/fsetpos.
Quando dá errado
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.
"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).
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.
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
| Escrita | O 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:
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?
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:
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 origemSEEK_SET(começo),SEEK_CUR(atual) ouSEEK_END(fim);rewind(f)volta ao começo.- Tamanho do arquivo:
fseek(f, 0, SEEK_END)e depoisftell(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 comfseekantes de gravar o que acabou de ler. fseekpara posição negativa falha; para depois do fim, não. Para arquivos de mais de 2 GB, usefseeko/ftellooufgetpos/fsetpos.