Na aula passada, gravamos texto — caracteres legíveis, como você os leria num editor. Hoje descemos um nível: vamos gravar os bytes crus dos nossos dados, exatamente como eles vivem na memória. É a diferença entre escrever o número 1000000 como sete caracteres de texto ou como os quatro bytes de um int. Arquivos binários são mais compactos, mais rápidos e permitem salvar structs inteiras de uma vez — mas exigem precisão cirúrgica, porque um byte fora do lugar corrompe tudo. Ao final, você saberá também navegar livremente por um arquivo, saltando para qualquer posição.
Texto versus binário: a diferença fundamental
Imagine gravar o número inteiro 12345. No modo texto (o da aula anterior), ele vira a sequência de cinco caracteres '1', '2', '3', '4', '5' — cinco bytes, um por dígito, legíveis num editor de texto. No modo binário, ele é gravado como os quatro bytes que representam um int na memória — ilegíveis num editor comum, mas uma cópia exata da representação interna. A escrita binária não "traduz" o dado para caracteres; ela despeja os bytes tal como estão.
Cada abordagem tem seu lugar. O texto é legível por humanos, portável entre sistemas e fácil de inspecionar — ideal para configurações, logs e dados que pessoas leem. O binário é compacto, rápido de ler e escrever (sem conversão), e preserva a estrutura exata dos dados — ideal para grandes volumes, dados numéricos e persistir structs. Para abrir um arquivo em modo binário, acrescenta-se "b" ao modo: "wb" para escrever, "rb" para ler.
fwrite: gravando bytes crus
A função fwrite grava um bloco de bytes da memória diretamente no arquivo. Ela recebe quatro argumentos: o endereço dos dados, o tamanho de cada elemento, quantos elementos, e o FILE *:
#include <stdio.h>
int main(void) {
int numeros[5] = {10, 20, 30, 40, 50};
FILE *f = fopen("dados.bin", "wb"); // 'b' de binário
if (f == NULL) return 1;
// grava 5 ints de uma vez: endereço, tamanho de cada, quantidade, arquivo
size_t gravados = fwrite(numeros, sizeof(int), 5, f);
printf("Elementos gravados: %zu\n", gravados); // 5
fclose(f);
return 0;
}
Leia a chamada com atenção, porque ela reúne tudo que você aprendeu sobre memória: fwrite(numeros, sizeof(int), 5, f) diz "pegue o bloco que começa no endereço numeros, com 5 elementos de sizeof(int) bytes cada, e grave-o em f". O fwrite devolve quantos elementos conseguiu gravar — comparar esse retorno com o esperado (5) é como você detecta erros de escrita. O arquivo resultante tem exatamente 20 bytes (5 × 4), sem nenhuma formatação ou separador.
fread: lendo bytes de volta
A leitura é o espelho perfeito da escrita. fread tem a mesma assinatura de fwrite e recupera os bytes para a memória:
#include <stdio.h>
int main(void) {
int numeros[5];
FILE *f = fopen("dados.bin", "rb");
if (f == NULL) return 1;
size_t lidos = fread(numeros, sizeof(int), 5, f);
printf("Elementos lidos: %zu\n", lidos); // 5
for (int i = 0; i < 5; i++) {
printf("%d ", numeros[i]); // 10 20 30 40 50 — recuperados!
}
printf("\n");
fclose(f);
return 0;
}
O fread lê os bytes do arquivo e os coloca de volta na memória, exatamente como estavam. A simetria é a chave: você lê com os mesmos parâmetros de tamanho e tipo que usou para escrever. Se gravou int, leia int; se gravou 5, leia até 5. Qualquer descasamento entre a escrita e a leitura — ler como double o que foi gravado como int, por exemplo — produz lixo, porque os bytes seriam reinterpretados sob o tipo errado.
O verdadeiro poder: gravar structs inteiras
Aqui o binário mostra por que vale a pena. Com fwrite, você grava uma struct completa — todos os seus campos de uma vez — em uma única chamada. Isso é impossível de fazer de forma tão direta com texto:
#include <stdio.h>
#include <string.h>
typedef struct {
char nome[50];
int idade;
double saldo;
} Cliente;
int main(void) {
Cliente c;
strcpy(c.nome, "Ana Silva");
c.idade = 30;
c.saldo = 1500.75;
// grava a struct inteira de uma vez
FILE *f = fopen("cliente.bin", "wb");
if (f == NULL) return 1;
fwrite(&c, sizeof(Cliente), 1, f); // &c: endereço; 1 elemento do tipo Cliente
fclose(f);
// relê a struct inteira
Cliente lido;
f = fopen("cliente.bin", "rb");
if (f == NULL) return 1;
fread(&lido, sizeof(Cliente), 1, f);
fclose(f);
printf("%s, %d anos, saldo R$ %.2f\n", lido.nome, lido.idade, lido.saldo);
return 0;
}
Numa única chamada fwrite(&c, sizeof(Cliente), 1, f), gravamos os três campos da struct — nome, idade e saldo — de uma vez, como um bloco contíguo de bytes. E fread os recupera igualmente de uma vez. Isso é extraordinariamente conveniente para persistir registros: um cadastro de clientes, por exemplo, poderia ser um vetor de structs gravado com um único fwrite. Note o &c — passamos o endereço da struct, porque fwrite trabalha com o bloco de memória a partir desse ponto.
Uma ressalva honesta sobre portabilidade
Preciso ser transparente sobre uma limitação. Arquivos binários gravados com fwrite de structs são específicos da máquina e do compilador. A ordem dos bytes (o endianness), o alinhamento e o padding que o compilador insere entre campos podem variar entre sistemas. Um arquivo binário gravado no seu computador pode não ser lido corretamente em outro de arquitetura diferente. Para dados que precisam trafegar entre sistemas, usa-se formatos padronizados (como texto, JSON, ou formatos binários bem definidos com serialização explícita campo a campo). Para uso local — salvar e reler no mesmo programa e máquina — o fwrite de structs é prático e eficiente. Conhecer essa fronteira é o que distingue quem usa a ferramenta com consciência.
fseek e ftell: navegando pelo arquivo
Até agora, lemos arquivos do começo ao fim, sequencialmente. Mas arquivos permitem acesso aleatório: você pode saltar para qualquer posição. Duas funções controlam isso. fseek move a "cabeça de leitura" para uma posição específica, e ftell informa a posição atual:
#include <stdio.h>
int main(void) {
int numeros[5] = {10, 20, 30, 40, 50};
FILE *f = fopen("dados.bin", "wb");
if (f == NULL) return 1;
fwrite(numeros, sizeof(int), 5, f);
fclose(f);
// reabrir e pular direto para o 4º elemento (índice 3)
f = fopen("dados.bin", "rb");
if (f == NULL) return 1;
// salta 3 ints a partir do início: SEEK_SET = início do arquivo
fseek(f, 3 * sizeof(int), SEEK_SET);
printf("Posição atual (bytes): %ld\n", ftell(f)); // 12
int valor;
fread(&valor, sizeof(int), 1, f);
printf("4º elemento: %d\n", valor); // 40 — sem ler os anteriores!
fclose(f);
return 0;
}
O fseek(f, 3 * sizeof(int), SEEK_SET) posiciona a leitura exatamente no quarto inteiro, pulando os três primeiros sem lê-los. O terceiro argumento é o ponto de referência: SEEK_SET (início do arquivo), SEEK_CUR (posição atual) ou SEEK_END (fim do arquivo). Essa capacidade de saltar é o que permite, por exemplo, acessar diretamente o registro nº 500 de um cadastro sem percorrer os 499 anteriores — a base de bancos de dados simples baseados em registros de tamanho fixo. Um truque comum com SEEK_END: fseek(f, 0, SEEK_END) seguido de ftell(f) informa o tamanho total do arquivo em bytes.
O que vem a seguir
Hoje descemos ao nível dos bytes: aprendemos a gravar e ler dados binários com fwrite e fread, a persistir structs inteiras numa única chamada, e a navegar livremente pelo arquivo com fseek e ftell — além de entender a fronteira de portabilidade que o binário impõe. Com texto e binário dominados, encerramos a espinha dorsal da persistência em C. Na próxima aula, faremos uma pausa nos temas de I/O para explorar a biblioteca padrão do C de forma mais ampla — as funções prontas de <stdlib.h>, <math.h>, <ctype.h> e <time.h> que resolvem tarefas comuns e que você não precisa reinventar.
Fontes e leituras recomendadas
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 7.5 e Apêndice B sobre a biblioteca de I/O
- cppreference —
freadefwrite— https://en.cppreference.com/w/c/io/fread - cppreference —
fseek,ftelle posicionamento — https://en.cppreference.com/w/c/io/fseek - Modern C, Jens Gustedt — seção sobre I/O binário e portabilidade — https://gustedt.gitlabpages.inria.fr/modern-c/
- CERT C — FIO série, sobre uso correto de I/O binário e verificação de retornos — https://wiki.sei.cmu.edu/confluence/display/c
Exercícios
Exercício 1
Escreva um programa que grave um vetor de 10 double em um arquivo binário ("wb") com fwrite, e depois um segundo programa (ou uma segunda fase) que os leia de volta ("rb") com fread e os imprima. Confirme que os valores foram preservados.
Ver resposta
✓ Resposta:
#include <stdio.h>
int main(void) {
double v[10] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.1};
// escrita
FILE *f = fopen("reais.bin", "wb");
if (f == NULL) return 1;
fwrite(v, sizeof(double), 10, f);
fclose(f);
// leitura
double lidos[10];
f = fopen("reais.bin", "rb");
if (f == NULL) return 1;
fread(lidos, sizeof(double), 10, f);
fclose(f);
for (int i = 0; i < 10; i++) printf("%.1f ", lidos[i]);
printf("\n");
return 0;
}
Exercício 2
Defina uma struct Produto (com nome, codigo e preco), preencha uma instância e grave-a num arquivo binário com uma única chamada fwrite. Depois releia-a com fread e imprima os campos.
Ver resposta
✓ Resposta:
#include <stdio.h>
#include <string.h>
typedef struct {
char nome[40];
int codigo;
double preco;
} Produto;
int main(void) {
Produto p;
strcpy(p.nome, "Caderno");
p.codigo = 101;
p.preco = 12.90;
FILE *f = fopen("produto.bin", "wb");
if (f == NULL) return 1;
fwrite(&p, sizeof(Produto), 1, f);
fclose(f);
Produto lido;
f = fopen("produto.bin", "rb");
if (f == NULL) return 1;
fread(&lido, sizeof(Produto), 1, f);
fclose(f);
printf("%s (cod. %d) — R$ %.2f\n", lido.nome, lido.codigo, lido.preco);
return 0;
}
Exercício 3
Grave um vetor de 5 structs Produto em um arquivo binário com uma única chamada fwrite (dica: passe 5 como contagem). Releia todas de uma vez e imprima. Explique por que uma só chamada basta.
Ver resposta
✓ Resposta:
#include <stdio.h>
#include <string.h>
typedef struct {
char nome[40];
int codigo;
double preco;
} Produto;
int main(void) {
Produto v[5];
for (int i = 0; i < 5; i++) {
sprintf(v[i].nome, "Item%d", i + 1);
v[i].codigo = 100 + i;
v[i].preco = (i + 1) * 10.0;
}
FILE *f = fopen("produtos.bin", "wb");
if (f == NULL) return 1;
fwrite(v, sizeof(Produto), 5, f); // uma única chamada grava as 5
fclose(f);
Produto lidos[5];
f = fopen("produtos.bin", "rb");
if (f == NULL) return 1;
fread(lidos, sizeof(Produto), 5, f);
fclose(f);
for (int i = 0; i < 5; i++)
printf("%s (cod. %d) — R$ %.2f\n", lidos[i].nome, lidos[i].codigo, lidos[i].preco);
return 0;
}
Uma só chamada basta porque um vetor de structs ocupa memória contígua: as 5 structs estão lado a lado, formando um único bloco de 5 * sizeof(Produto) bytes. O fwrite simplesmente copia esse bloco inteiro de uma vez, e o fread o recupera da mesma forma — a contiguidade da memória é o que torna a operação em bloco possível.
Exercício 4
Usando fseek e ftell, escreva uma função que retorne o tamanho em bytes de um arquivo qualquer. Dica: posicione no fim com fseek(f, 0, SEEK_END) e leia a posição com ftell.
Ver resposta
✓ Resposta:
#include <stdio.h>
long tamanho_arquivo(const char *caminho) {
FILE *f = fopen(caminho, "rb");
if (f == NULL) return -1;
fseek(f, 0, SEEK_END); // posiciona no fim
long tamanho = ftell(f); // posição do fim = tamanho em bytes
fclose(f);
return tamanho;
}
int main(void) {
printf("Tamanho: %ld bytes\n", tamanho_arquivo("produtos.bin"));
return 0;
}
Ao mover a posição para o fim do arquivo (SEEK_END) e perguntar onde estamos (ftell), obtemos a distância em bytes do início até o fim — que é exatamente o tamanho do arquivo.
Exercício 5
Explique por que um arquivo binário criado com fwrite de uma struct em uma máquina pode não ser lido corretamente em outra máquina de arquitetura diferente. Cite pelo menos dois fatores técnicos envolvidos.
Ver resposta
✓ Resposta: Um arquivo binário de struct pode não ser portável entre arquiteturas por, pelo menos, dois fatores: (1) Endianness (ordem dos bytes) — máquinas diferentes armazenam os bytes de um inteiro multibyte em ordens diferentes (little-endian versus big-endian). Um int de valor 1 gravado numa ordem seria lido como um número completamente diferente na outra. (2) Padding e alinhamento — o compilador insere bytes de preenchimento (padding) entre os campos de uma struct para alinhá-los a fronteiras de memória convenientes para o processador. As regras de alinhamento variam entre arquiteturas e compiladores, então o mesmo tipo de struct pode ter tamanhos e disposições de bytes diferentes em máquinas distintas — fazendo os campos "caírem" em posições erradas ao reler. (Um terceiro fator seria o tamanho variável de tipos como long, que pode ter 4 ou 8 bytes conforme o sistema.) Por isso, para dados que cruzam máquinas, prefere-se serialização explícita ou formatos padronizados.