C Arquivos Binários (fread e fwrite)

Até aqui, os arquivos do guia eram de texto: o número 1234567 virava os sete caracteres 1, 2, 3… e você podia abrir o arquivo no editor de textos. Existe outro jeito: copiar para o arquivo os bytes da memória, do jeitinho que o programa guarda a variável. Esse é o arquivo binário. Nesta aula você vai gravar o estoque da lojinha da Ana inteiro com uma linha de código, ler de volta, espiar os bytes com xxd e entender quando esse atalho é uma ótima ideia e quando é uma armadilha.

Você vai aprender
  • A diferença entre arquivo de texto e arquivo binário, vista byte a byte.
  • Como gravar com fwrite e ler com fread, e o que cada um dos quatro argumentos significa.
  • Como gravar um array de structs de uma vez e ler de volta, inteiro ou um registro por vez.
  • Como olhar os bytes de um arquivo com xxd e od.
  • Por que um arquivo binário pode não funcionar em outro computador (tamanho dos tipos, ordem dos bytes, ponteiros).
  • Por que você precisa conferir o retorno de fread e fwrite.

Esta aula usa fopen, fclose e structs. Se precisar, reveja Arquivos (fopen e fclose) e Structs. 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 o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: anotar × fotocopiar

Imagine que a Ana quer guardar a ficha de um produto. Ela tem dois jeitos:

  • Anotar à mão num caderno: "Caderno, 12 unidades, R$ 15,90". Qualquer pessoa lê, mas dá trabalho escrever e, depois, alguém precisa ler e interpretar a anotação para pôr os números de volta na ficha.
  • Tirar uma fotocópia da ficha. É instantâneo e a cópia é idêntica, mas só serve para quem tem uma ficha do mesmo modelo para comparar.

O arquivo de texto é a anotação: o fprintf transforma cada número em caracteres, e o fscanf (ou strtol) transforma de volta. O arquivo binário é a fotocópia: o fwrite copia os bytes da memória para o disco, sem conversão, e o fread copia do disco de volta para a memória.

Arquivo de textoArquivo binário
Grava comfprintf, fputsfwrite
Lê comfgets, fscanffread
O número 1234567 ocupa7 bytes (um por algarismo)4 bytes (o tamanho de um int)
Abre no editor de textos?SimNão: aparecem símbolos estranhos
Funciona em outro computador?Quase sempreSó se os tipos tiverem o mesmo tamanho e formato

1. O mesmo número, dois arquivos

Para ver a diferença de verdade, o programa abaixo grava o número 1234567 duas vezes: em numero.txt com fprintf e em numero.bin com fwrite.

#include <stdio.h>

int main(void) {
    int numero = 1234567;

    FILE *txt = fopen("numero.txt", "w");
    if (txt == NULL) {
        printf("Não consegui criar numero.txt\n");
        return 1;
    }
    fprintf(txt, "%d", numero);
    fclose(txt);

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

    printf("Pronto: numero.txt e numero.bin\n");
    return 0;
}
fopen("numero.bin", "wb")
Abre para escrita (w, de write) em modo binário (b, de binary). No Linux e no macOS o b não muda nada; no Windows ele é essencial (veja o quadro sobre o Windows). Use sempre: deixa claro que o arquivo é binário.
fwrite(
"file write", escrever no arquivo. Copia bytes da memória para o arquivo, sem transformar nada em texto.
&numero
1º argumento: onde estão os bytes. É o endereço da variável (o &, como no scanf; veja Memória e Endereços).
sizeof numero
2º argumento: o tamanho de cada item, em bytes. Um int tem 4 bytes. Nunca escreva o 4 à mão: o sizeof acerta em qualquer computador.
1
3º argumento: quantos itens gravar. Aqui, um int.
bin
4º argumento: em qual arquivo. Repare que o arquivo vem no fim, ao contrário do fprintf.
$ gcc -std=c23 -Wall -Wextra numero.c -o numero
$ ./numero
Pronto: numero.txt e numero.bin
$ ls -l numero.txt numero.bin
-rw-rw-r-- 1 ana ana 4 out 17 10:32 numero.bin       « 4 bytes: um int
-rw-rw-r-- 1 ana ana 7 out 17 10:32 numero.txt       « 7 bytes: sete algarismos
$ cat numero.txt; echo
1234567

O numero.txt é legível. Já o numero.bin, se você mandar o cat mostrar, aparece como lixo na tela, porque seus bytes não são letras. Para ver o que tem dentro, use um visualizador de bytes.

Espiando os bytes com xxd e od

O xxd faz um hex dump ("despejo em hexadecimal"): mostra cada byte como dois algarismos hexadecimais (de 00 a ff). O od (octal dump) faz o mesmo e ainda sabe interpretar os bytes como números.

$ xxd numero.txt
00000000: 3132 3334 3536 37                        1234567
$ xxd numero.bin
00000000: 87d6 1200                                ....
$ od -A d -t d4 numero.bin
0000000     1234567
0000004
00000000:
A posição (em hexadecimal) do primeiro byte da linha. O primeiro byte do arquivo é a posição 0.
3132 3334 3536 37
Os bytes do arquivo de texto: 31 é o código do caractere '1', 32 é o '2', e assim por diante (a tabela ASCII, da aula Caracteres). À direita, o xxd mostra esses bytes como letras.
87d6 1200
Os 4 bytes do int: 87 d6 12 00. Nenhum deles é uma letra imprimível, por isso a coluna da direita mostra pontos.
od -A d -t d4
Mostra as posições em decimal (-A d) e lê o arquivo de 4 em 4 bytes como inteiros com sinal (-t d4). Ele reconstruiu o 1234567: os bytes estão lá, só não estão "escritos".

Por que 87 d6 12 00? Em hexadecimal, 1234567 é 0x0012D687. Os bytes estão de trás para frente: o byte menos importante (87) vem primeiro. Esse jeito de guardar se chama little-endian ("a ponta pequena primeiro") e é o dos processadores Intel, AMD e da maioria dos ARM. Voltaremos a isso na seção de portabilidade.

Dica: od -A d -t x1 arquivo mostra os mesmos bytes em hexadecimal, um por um (87 d6 12 00), e od -c arquivo mostra cada byte como caractere quando dá. Use o que achar mais fácil de ler; o xxd vem junto com o editor vim, e o od existe em qualquer Linux.

2. Gravar um array de structs com fwrite

A grande vantagem do fwrite aparece com structs. Com texto, você teria de escrever cada campo com o seu formato (%s, %d, %.2f) e, na hora de ler, separar tudo de novo. Com binário, o estoque inteiro vai para o disco numa linha:

#include <stdio.h>

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

int main(void) {
    Produto estoque[3] = {
        {"Caderno", 12, 15.90},
        {"Caneta", 40, 2.50},
        {"Borracha", 25, 1.75},
    };

    FILE *f = fopen("estoque.bin", "wb");
    if (f == NULL) {
        printf("Não consegui criar estoque.bin\n");
        return 1;
    }

    size_t gravados = fwrite(estoque, sizeof(Produto), 3, f);
    fclose(f);

    if (gravados != 3) {
        printf("Erro: só gravei %zu de 3 produtos\n", gravados);
        return 1;
    }
    printf("Gravei %zu produtos de %zu bytes cada.\n", gravados, sizeof(Produto));
    return 0;
}
LinhaO que faz
typedef struct { … } Produto;A ficha de um produto: nome (até 19 letras e o '\0'), quantidade e preço (veja typedef).
fwrite(estoque, …)O nome do array já é o endereço do primeiro elemento, então não leva & (aula Ponteiros e Arrays).
sizeof(Produto)Cada item tem o tamanho de um Produto: 32 bytes neste computador.
3Três itens, um depois do outro: 3 × 32 = 96 bytes.
size_t gravados = …O fwrite devolve quantos itens gravou por inteiro. Se for menos que 3, algo deu errado (disco cheio, por exemplo).
%zuO formato do printf para size_t (aula Formatos do printf).
$ gcc -std=c23 -Wall -Wextra gravar.c -o gravar
$ ./gravar
Gravei 3 produtos de 32 bytes cada.
$ ls -l estoque.bin
-rw-rw-r-- 1 ana ana 96 out 17 10:32 estoque.bin

96 bytes, como a conta previa. Agora veja como esses bytes estão organizados:

$ xxd estoque.bin
00000000: 4361 6465 726e 6f00 0000 0000 0000 0000  Caderno.........
00000010: 0000 0000 0c00 0000 cdcc cccc cccc 2f40  ............../@
00000020: 4361 6e65 7461 0000 0000 0000 0000 0000  Caneta..........
00000030: 0000 0000 2800 0000 0000 0000 0000 0440  ....(..........@
00000040: 426f 7272 6163 6861 0000 0000 0000 0000  Borracha........
00000050: 0000 0000 1900 0000 0000 0000 0000 fc3f  ...............?

Cada produto ocupa exatamente duas linhas do xxd (32 bytes). Olhando o primeiro:

  1. Bytes 0 a 19 (nome): 43 61 64 65 72 6e 6f são as letras de "Caderno", seguidas do 00 que termina a string e de zeros até completar os 20 bytes do array. O nome curto ainda ocupa 20 bytes: no arquivo binário, cada campo tem tamanho fixo.
  2. Bytes 20 a 23 (quantidade): 0c 00 00 00. 0x0c é 12, de novo com o byte pequeno primeiro.
  3. Bytes 24 a 31 (preco): cd cc cc cc cc cc 2f 40. É o double 15.90 no formato interno dos números com vírgula (o padrão IEEE 754, da aula float e double). Não dá para ler de olho, e não precisa: o fread põe esses bytes de volta num double.

3. Ler de volta com fread

O fread ("file read", ler do arquivo) é o espelho do fwrite: os mesmos quatro argumentos, mas os bytes vão do arquivo para a memória.

#include <stdio.h>

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

int main(void) {
    Produto estoque[10];

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

    size_t lidos = fread(estoque, sizeof(Produto), 10, f);
    fclose(f);

    printf("Li %zu produtos:\n", lidos);
    for (size_t i = 0; i < lidos; i++) {
        printf("%-10s %3d  R$ %6.2f\n", estoque[i].nome,
               estoque[i].quantidade, estoque[i].preco);
    }
    return 0;
}
Produto estoque[10];
Espaço para até 10 produtos. O programa não sabe quantos existem no arquivo.
"rb"
Leitura (r, de read) em modo binário.
fread(estoque, sizeof(Produto), 10, f)
"Leia até 10 itens de sizeof(Produto) bytes e ponha a partir de estoque." O arquivo só tem 3, e não tem problema: o fread lê o que houver.
size_t lidos
Quantos itens completos foram lidos: 3. É esse número, e não o 10, que o laço usa.
%-10s
O nome alinhado à esquerda numa coluna de 10 caracteres (veja Formatos do printf).
$ gcc -std=c23 -Wall -Wextra ler.c -o ler
$ ./ler
Li 3 produtos:
Caderno     12  R$  15.90
Caneta      40  R$   2.50
Borracha    25  R$   1.75

Outro programa, compilado separadamente, leu o arquivo e recuperou os três produtos sem converter nada. Ele funciona porque declara a mesma struct Produto que o gravar.c. Os dois programas precisam concordar, byte a byte, sobre o formato da ficha.

Um registro por vez

Quando o arquivo pode ser grande, ou você não sabe quantos registros tem, leia um por vez num laço, até o fread devolver 0:

#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;
    double total = 0.0;
    while (fread(&p, sizeof p, 1, f) == 1) {
        printf("%-10s %3d x R$ %5.2f\n", p.nome, p.quantidade, p.preco);
        total += p.quantidade * p.preco;
    }

    if (ferror(f)) {
        printf("Erro de leitura!\n");
    } else {
        printf("Valor do estoque: R$ %.2f\n", total);
    }
    fclose(f);
    return 0;
}
while (fread(&p, sizeof p, 1, f) == 1)
Lê um produto para dentro de p. Enquanto conseguir (devolver 1), o laço continua. No fim do arquivo, o fread devolve 0 e o laço para.
&p
Aqui é uma variável comum, então precisa do &. Com array (estoque), não.
ferror(f)
Depois do laço: o fread parou porque o arquivo acabou ou porque houve um erro (disco com defeito, pendrive arrancado)? O ferror responde "verdadeiro" só no segundo caso. O irmão dele, feof(f), responde se chegou ao fim.
$ gcc -std=c23 -Wall -Wextra ler1.c -o ler1
$ ./ler1
Caderno     12 x R$ 15.90
Caneta      40 x R$  2.50
Borracha    25 x R$  1.75
Valor do estoque: R$ 334.55

Experimente: este programa grava 4 notas em notas.bin e lê de volta num array novo. Clique em Rodar. Depois troque o 4 do fread por 10 (e o tamanho de copia para 10): quantas notas ele diz que leu? E se o fwrite gravar só 2?

#include <stdio.h>

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

    // 1) grava as 4 notas de uma vez
    FILE *f = fopen("notas.bin", "wb");
    if (f == NULL) {
        printf("Não consegui criar notas.bin\n");
        return 1;
    }
    size_t gravadas = fwrite(notas, sizeof(int), 4, f);
    fclose(f);
    printf("Gravei %zu notas.\n", gravadas);

    // 2) lê de volta num array novo
    int copia[4] = {0};
    f = fopen("notas.bin", "rb");
    if (f == NULL) {
        printf("Não consegui abrir notas.bin\n");
        return 1;
    }
    size_t lidas = fread(copia, sizeof(int), 4, f);
    fclose(f);

    printf("Li %zu notas:", lidas);
    for (size_t i = 0; i < lidas; i++) {
        printf(" %d", copia[i]);
    }
    printf("\n");
    return 0;
}

4. Confira sempre o retorno

fread e fwrite nunca avisam por conta própria que algo deu errado: eles só devolvem um número, e é você quem precisa olhar. Para ver por que isso importa, a Ana cortou o estoque.bin no byte 80 (como se a gravação tivesse sido interrompida no meio do terceiro produto). O comando truncate -s 80 faz esse corte:

$ cp estoque.bin backup.bin               « guarda uma cópia antes
$ truncate -s 80 estoque.bin
$ ls -l estoque.bin
-rw-rw-r-- 1 ana ana 80 out 17 10:33 estoque.bin
$ ./ler
Li 2 produtos:
Caderno     12  R$  15.90
Caneta      40  R$   2.50
$ ./ler1
Caderno     12 x R$ 15.90
Caneta      40 x R$  2.50
Valor do estoque: R$ 290.80
$ cp backup.bin estoque.bin               « devolve o arquivo inteiro

80 bytes são dois produtos completos (64 bytes) e metade do terceiro. O fread devolveu 2: ele só conta itens inteiros, e o pedaço que sobrou foi descartado. Os dois programas se comportaram bem porque usam o número devolvido. Um programa que ignorasse o retorno e mostrasse sempre 3 produtos imprimiria lixo no terceiro.

O que voltouO que significaO que fazer
Igual à quantidade pedidaDeu tudo certoSeguir em frente
Menor (no fread)O arquivo acabou (feof) ou houve erro (ferror)Usar só os itens lidos; se for erro, avisar
Menor (no fwrite)Erro de escrita: disco cheio, sem permissão, pendrive removidoAvisar e não fingir que salvou (o motivo está em errno)
O fclose também pode falhar. O fwrite costuma guardar os bytes num buffer (uma área de espera na memória) e só manda para o disco depois. Se o disco estiver cheio, o erro pode aparecer só no fclose, que devolve EOF em vez de 0. Em programas que salvam dados importantes, confira também: if (fclose(f) != 0) { … }.

5. Portabilidade: a fotocópia só serve para a mesma ficha

O arquivo binário guarda os bytes como este computador e este compilador os organizam. Outro computador pode organizar diferente. Há três motivos.

O tamanho dos tipos muda

O C só garante tamanhos mínimos (aula Inteiros). Um long tem 8 bytes no Linux de 64 bits e 4 bytes no Windows e no editor do guia. Veja a mesma struct nos dois:

#include <stdio.h>

typedef struct {
    long codigo;
    int quantidade;
} Item;

int main(void) {
    printf("sizeof(long) = %zu\n", sizeof(long));
    printf("sizeof(Item) = %zu\n", sizeof(Item));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra portavel.c -o portavel
$ ./portavel
sizeof(long) = 8
sizeof(Item) = 16

No editor do guia (Clang em WebAssembly de 32 bits), o mesmo programa mostra sizeof(long) = 4 e sizeof(Item) = 8. Um arquivo de Item gravado num lado seria lido pela metade no outro. Repare também que 8 + 4 deu 16, e não 12: o compilador pode deixar bytes de enchimento (padding) entre os campos ou no fim da struct, para alinhar os números na memória (veja Structs). Esse enchimento também vai para o arquivo.

Solução: nos campos gravados em binário, use os tipos de tamanho fixo de <stdint.h>: int32_t tem 32 bits (4 bytes) em qualquer lugar, int64_t tem 64 bits (aula Inteiros de Tamanho Fixo).

A ordem dos bytes muda

Você viu que 1234567 ficou 87 d6 12 00, com o byte pequeno primeiro (little-endian). Alguns processadores antigos ou de rede (e vários formatos de arquivo e protocolos da internet) usam a ordem contrária, big-endian: 00 12 d6 87. Um arquivo gravado num e lido no outro daria um número completamente diferente. Hoje quase todo computador e celular é little-endian, por isso o problema aparece pouco, mas aparece quando você lê um formato definido por outra pessoa (imagens, áudio, arquivos de rede).

Ponteiros não fazem sentido no disco

Um ponteiro é um endereço da memória deste programa, nesta execução. Se a struct tem um char *nome, o fwrite grava o endereço, e não o texto. O exemplo está em Quando dá errado.

Texto ou binário: qual usar?

SituaçãoPrefiraPor quê
Uma pessoa vai ler ou editar o arquivoTexto (CSV, uma informação por linha)Abre em qualquer editor e em planilhas
Outro programa, outra linguagem ou outro computador vai lerTextoNão depende de tamanho de tipo nem de ordem dos bytes
O mesmo programa grava e lê, na mesma máquina (um "save" de jogo, um cache)BinárioUma linha para gravar, uma para ler, sem conversão
Muitos números e velocidade importaBinário com int32_t/int64_tMenor e mais rápido, com tamanhos garantidos
Precisa pular direto para o registro número 500Binário com registros de tamanho fixoA posição é uma conta simples (aula fseek e ftell)
Por que o b importa no Windows. No Windows, um arquivo aberto em modo texto ("r", "w") troca cada '\n' por dois bytes (\r\n) ao gravar e faz o caminho contrário ao ler. Num arquivo binário, qualquer byte 0a (o valor de '\n') no meio de um número seria "corrigido" e estragaria o dado. Com "wb" e "rb", nenhum byte é trocado. No Linux e no macOS não existe essa troca, mas escrever o b deixa o programa correto em todos os sistemas.

Quando dá errado

Falha de segmentação (imagem do núcleo gravada) logo no fread
#include <stdio.h>

int main(void) {
    int notas[3];
    FILE *f = fopen("notas.bin", "rb");
    fread(notas, sizeof(int), 3, f);
    printf("Primeira nota: %d\n", notas[0]);
    fclose(f);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra semteste.c -o semteste
$ ./semteste
Falha de segmentação     (imagem do núcleo gravada) ./semteste

O que significa: o arquivo notas.bin não existe nesta pasta, o fopen devolveu NULL, e o fread tentou ler de um ponteiro nulo. O compilador não avisa nada: o erro só aparece ao rodar. No editor do guia, a mesma falha termina com o código de saída 139.

Como consertar: confira o fopen sempre, antes de usar o arquivo: if (f == NULL) { … return 1; }. Para dizer por que não abriu, use perror (aula errno e perror).

warning: ‘sizeof’ on array function parameter ‘lista’ will return size of ‘Produto *’
void salvar(FILE *f, Produto lista[], int n) {
    fwrite(lista, sizeof(lista), n, f);
}
$ gcc -std=c23 -Wall -Wextra param.c -o param
param.c: In function ‘salvar’:
param.c:10:25: warning: ‘sizeof’ on array function parameter ‘lista’ will return size of ‘Produto *’ [-Wsizeof-array-argument]
   10 |     fwrite(lista, sizeof(lista), n, f);
      |                         ^
param.c:9:30: note: declared here
    9 | void salvar(FILE *f, Produto lista[], int n) {
      |                      ~~~~~~~~^~~~~~~
$ ./param
$ ls -l copia.bin
-rw-rw-r-- 1 ana ana 24 out 17 10:33 copia.bin      « deveria ter 96 bytes

O que significa: dentro da função, lista não é um array, é um ponteiro (aula Ponteiros e Arrays). sizeof(lista) deu 8 (o tamanho de um ponteiro), e o programa gravou 3 × 8 = 24 bytes de pedaços de produtos. O Clang do editor diz o mesmo: sizeof on array function parameter will return size of 'Produto *' instead of 'Produto[]'.

Como consertar: o tamanho de cada item é o tamanho do tipo: sizeof(Produto) ou sizeof lista[0].

O texto não está no arquivo: struct com ponteiro
typedef struct {
    char *nome;          // ponteiro, e não array!
    int quantidade;
} Item;
/* … */
    Item it = {"Caderno", 12};
    fwrite(&it, sizeof it, 1, f);
$ ./ponteiro
sizeof(Item) = 16
$ xxd item.bin
00000000: 0400 3344 6e62 0000 0c00 0000 0000 0000  ..3Dnb..........

O que significa: não há "Caderno" no arquivo. Os primeiros 8 bytes são o endereço onde o texto estava na memória (e ele muda a cada execução); depois vem o 12 (0c) e o enchimento. Outro programa que lesse esse arquivo teria um ponteiro para um lugar que não existe, e o mais provável é uma falha de segmentação ao usar nome.

Como consertar: nas structs que vão para arquivo binário, use arrays de tamanho fixo (char nome[20];). Se o texto pode ser grande, grave antes o tamanho (um int32_t) e depois os caracteres, e leia na mesma ordem.

Lixo na leitura: os dois programas não concordam sobre a struct

A Ana mudou a ordem dos campos no programa de leitura (int quantidade; antes de char nome[20];) e leu o mesmo estoque.bin:

$ ./ler_errado
rno        1701077315 x R$ 15.90
ta         1701732675 x R$  2.50
acha       1920102210 x R$  1.75

O que significa: o fread não sabe o que é nome nem quantidade; ele só copia 32 bytes. Os 4 primeiros bytes ("Cade") foram parar em quantidade (1701077315 é "Cade" lido como número), e o nome começou no 5º byte ("rno"). O preço por acaso caiu no lugar certo. Nenhum erro, nenhum aviso: só dados errados.

Como consertar: escreva a struct num só lugar, num arquivo .h que os dois programas incluem (aula Arquivos .h e .c). Se o formato mudar, arquivos antigos deixam de servir: muitos programas gravam no começo do arquivo um número de versão para detectar isso.

O arquivo de texto ficou cheio de símbolos estranhos

O que significa: você usou fwrite com um número e esperava ver o número escrito. O fwrite grava os bytes da memória, que não são letras.

Como consertar: se o arquivo é para gente ler, use fprintf (aula Escrever Arquivos). Se é binário mesmo, olhe com xxd ou od, e não com cat.


Referência rápida

EscritaO que faz
fopen(nome, "wb")Cria (ou apaga e recria) um arquivo binário para escrita
fopen(nome, "rb")Abre um arquivo binário para leitura
fopen(nome, "ab")Abre para acrescentar no fim (cria se não existir)
fopen(nome, "r+b")Abre para ler e escrever, sem apagar (aula fseek)
fwrite(ptr, tam, n, f)Grava n itens de tam bytes a partir de ptr; devolve quantos gravou
fread(ptr, tam, n, f)Lê até n itens de tam bytes para ptr; devolve quantos leu inteiros
fwrite(&x, sizeof x, 1, f)Grava uma variável
fwrite(v, sizeof v[0], n, f)Grava n elementos de um array
while (fread(&x, sizeof x, 1, f) == 1)Lê um registro por vez até o fim
feof(f) / ferror(f)Depois de uma leitura curta: acabou o arquivo? / houve erro?
xxd arq, od -A d -t x1 arqNo terminal: mostra os bytes do arquivo em hexadecimal

Exercícios

1. Gravar uma variável

Complete para gravar a variável double saldo no arquivo f:

fwrite(, sizeof saldo, 1, f);

2. O modo certo

Complete o modo do fopen para ler um arquivo binário:

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

3. Tamanho do arquivo

Uma struct tem sizeof igual a 32. Quantos bytes tem um arquivo com 25 dessas structs gravadas com fwrite?

bytes

4. O arquivo tem 2 registros completos e o programa chama fread(v, sizeof v[0], 5, f). O que o fread devolve?

5. O xxd mostrou 0c00 0000 no lugar de um int. Que número é esse num computador little-endian?

6. Qual struct é segura para gravar com fwrite e ler em outra execução do programa?

Missão final: o boletim da turma

Missão: a parte que grava a turma em turma.bin já está pronta. Escreva a leitura: abra o arquivo com "rb" (conferindo o NULL), leia um aluno por vez com fread num laço, mostre cada um e calcule a média. A saída deve ser:

Ana: 9.5
Bruno: 6.0
Carla: 8.0
Davi: 6.5
Média: 7.50
#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);

    // agora leia turma.bin de volta, um aluno por vez
    double soma = 0.0;
    int lidos = 0;

    if (lidos > 0) {
        printf("Média: %.2f\n", soma / lidos);
    }
    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);

    // agora leia turma.bin de volta, um aluno por vez
    double soma = 0.0;
    int lidos = 0;

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

    if (lidos > 0) {
        printf("Média: %.2f\n", soma / lidos);
    }
    return 0;
}

Se quiser ir além: troque o fwrite para gravar só 3 alunos e veja a média mudar sozinha, porque o laço de leitura se adapta ao que existe no arquivo.


Resumo

  • Arquivo de texto guarda os números escritos (fprintf); arquivo binário guarda os bytes da memória (fwrite). Use os modos "wb", "rb", "ab".
  • fwrite(ptr, tamanho, quantidade, arquivo) e fread(ptr, tamanho, quantidade, arquivo); o tamanho vem de sizeof.
  • Um array de structs vai e volta com uma linha. Para ler sem saber quantos há, use while (fread(&x, sizeof x, 1, f) == 1).
  • Os dois devolvem quantos itens inteiros transferiram: confira sempre. Depois de uma leitura curta, feof e ferror dizem o motivo.
  • xxd e od mostram os bytes. Os processadores comuns são little-endian: 12 vira 0c 00 00 00.
  • Binário não é portátil: o tamanho dos tipos, o enchimento e a ordem dos bytes mudam. Use int32_t/int64_t, arrays de tamanho fixo e nunca ponteiros. Para trocar dados com outros programas, prefira texto.