Arquivos binários são uma forma de armazenar dados em disco de maneira direta, sem formatação textual. Diferentemente dos arquivos de texto, que armazenam caracteres legíveis, os binários guardam a representação exata dos dados em memória. Isso permite leitura e escrita mais rápidas e econômicas, especialmente quando lidamos com grandes volumes de dados ou com estruturas complexas.

Nesta aula, vamos explorar as funções fundamentais para manipulação de arquivos binários em C: fread e fwrite, além de funções de posicionamento como fseek e ftell. Também veremos como gravar e ler estruturas inteiras de uma só vez, e discutiremos a portabilidade, um aspecto crucial quando se trabalha com dados binários entre diferentes plataformas.

fread e fwrite

As funções fread e fwrite são as principais para leitura e escrita de dados binários em C. Elas estão declaradas em <stdio.h> e possuem as seguintes assinaturas:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

O primeiro argumento é um ponteiro para a área de memória onde os dados serão lidos ou escritos. O segundo, size, é o tamanho em bytes de cada elemento. O terceiro, nmemb, é o número de elementos a serem lidos ou escritos. O último é o ponteiro para o arquivo. Ambas retornam o número de elementos efetivamente lidos/escritos. Em caso de erro ou fim de arquivo, o valor retornado pode ser menor que o solicitado.

Um exemplo clássico é gravar um vetor de inteiros em um arquivo binário e depois lê-lo de volta:

#include <stdio.h>

int main() {
    int dados[5] = {10, 20, 30, 40, 50};
    FILE *fp = fopen("dados.bin", "wb");
    if (fp == NULL) { perror("Erro ao abrir"); return 1; }

    // Escreve 5 inteiros, cada um com tamanho sizeof(int)
    size_t escritos = fwrite(dados, sizeof(int), 5, fp);
    if (escritos != 5) { perror("Erro na escrita"); fclose(fp); return 1; }
    fclose(fp);

    // Leitura
    int lidos[5];
    fp = fopen("dados.bin", "rb");
    if (fp == NULL) { perror("Erro ao abrir"); return 1; }
    size_t lidos_n = fread(lidos, sizeof(int), 5, fp);
    if (lidos_n != 5) { perror("Erro na leitura"); fclose(fp); return 1; }
    fclose(fp);

    for (int i = 0; i < 5; i++) printf("%d ", lidos[i]);
    printf("\n");
    return 0;
}

Note que usamos os modos "wb" e "rb" para abrir o arquivo em modo binário. Em sistemas Unix, a distinção entre texto e binário é irrelevante, mas em Windows é essencial, pois o modo texto pode fazer conversões de caracteres de nova linha. Sempre use os modos binários para arquivos binários.

Posicionamento (fseek, ftell)

Quando trabalhamos com arquivos binários, muitas vezes precisamos acessar dados em posições específicas, seja para sobrescrever um registro, ler um elemento no meio do arquivo ou pular um cabeçalho. As funções fseek e ftell nos permitem controlar a posição atual no arquivo.

fseek move o indicador de posição do arquivo para um deslocamento especificado a partir de uma origem. Sua assinatura é:

int fseek(FILE *stream, long offset, int whence);

O parâmetro whence pode ser SEEK_SET (início do arquivo), SEEK_CUR (posição atual) ou SEEK_END (fim do arquivo). O offset é o número de bytes a partir dessa origem. Retorna 0 em caso de sucesso, ou um valor diferente de zero em erro.

ftell retorna a posição atual do indicador de arquivo, em bytes a partir do início:

long ftell(FILE *stream);

Vejamos um exemplo: suponha que temos um arquivo com 10 inteiros e queremos ler o sexto elemento (índice 5). Podemos usar fseek para pular diretamente para a posição desejada:

#include <stdio.h>

int main() {
    FILE *fp = fopen("dados.bin", "rb");
    if (fp == NULL) { perror("Erro"); return 1; }

    // Pula 5 inteiros a partir do início
    if (fseek(fp, 5 * sizeof(int), SEEK_SET) != 0) {
        perror("Erro no fseek");
        fclose(fp);
        return 1;
    }

    int valor;
    if (fread(&valor, sizeof(int), 1, fp) != 1) {
        perror("Erro na leitura");
        fclose(fp);
        return 1;
    }

    printf("Sexto elemento: %d\n", valor);
    fclose(fp);
    return 0;
}

Essas funções são essenciais para implementar acesso aleatório em arquivos binários, como em bancos de dados simples ou sistemas de registro.

Estruturas em arquivo

Uma das grandes vantagens dos arquivos binários é a capacidade de gravar e ler estruturas inteiras de uma só vez. Isso é feito passando o endereço da estrutura para fwrite ou fread, com o tamanho de sizeof(estrutura).

Considere a seguinte estrutura para armazenar informações de um aluno:

typedef struct {
    int matricula;
    char nome[50];
    float notas[3];
} Aluno;

Podemos gravar um vetor de alunos em um arquivo binário e depois ler de volta:

#include <stdio.h>

#define NUM_ALUNOS 3

typedef struct {
    int matricula;
    char nome[50];
    float notas[3];
} Aluno;

int main() {
    Aluno turma[NUM_ALUNOS] = {
        {1, "Maria", {8.5, 9.0, 7.5}},
        {2, "João", {7.0, 6.5, 8.0}},
        {3, "Ana", {9.5, 8.0, 9.0}}
    };

    FILE *fp = fopen("alunos.bin", "wb");
    if (fp == NULL) { perror("Erro"); return 1; }
    fwrite(turma, sizeof(Aluno), NUM_ALUNOS, fp);
    fclose(fp);

    // Leitura
    Aluno leitura[NUM_ALUNOS];
    fp = fopen("alunos.bin", "rb");
    if (fp == NULL) { perror("Erro"); return 1; }
    fread(leitura, sizeof(Aluno), NUM_ALUNOS, fp);
    fclose(fp);

    for (int i = 0; i < NUM_ALUNOS; i++) {
        printf("Matrícula: %d, Nome: %s, Notas: %.2f %.2f %.2f\n",
               leitura[i].matricula, leitura[i].nome,
               leitura[i].notas[0], leitura[i].notas[1], leitura[i].notas[2]);
    }
    return 0;
}

É importante notar que a estrutura pode conter padding (bytes de alinhamento) entre os campos, e o tamanho total pode não ser exatamente a soma dos tamanhos dos membros. Use sempre sizeof(Aluno) para garantir que toda a estrutura seja lida/escrita corretamente.

Portabilidade

Arquivos binários são dependentes da plataforma. Isso significa que um arquivo gravado em um sistema pode não ser lido corretamente em outro, devido a diferenças em:

  • Endianness: A ordem dos bytes na representação de inteiros e floats. Processadores x86 são little-endian, enquanto alguns outros (como alguns ARM) podem ser big-endian.
  • Tamanho dos tipos: O tamanho de int, long, etc., pode variar entre sistemas (ex.: 32 bits vs 64 bits).
  • Alinhamento e padding: O compilador pode inserir bytes extras para alinhar campos, e isso pode variar.
  • Codificação de caracteres: Embora não seja comum em binários, strings podem ser armazenadas de formas diferentes.

Para garantir portabilidade, algumas estratégias são:

  • Usar tipos de tamanho fixo, como int32_t, uint16_t, etc., definidos em <stdint.h>.
  • Controlar o endianness manualmente, convertendo para uma ordem conhecida (ex.: little-endian) antes de gravar e convertendo de volta na leitura.
  • Evitar gravar estruturas com padding diretamente; em vez disso, gravar campo a campo ou usar serialização.
  • Documentar o formato do arquivo para que outras implementações possam segui-lo.

Um exemplo de gravação com tipos de tamanho fixo:

#include <stdio.h>
#include <stdint.h>

int main() {
    int32_t valor = 123456;  // sempre 32 bits
    FILE *fp = fopen("fixo.bin", "wb");
    if (fp == NULL) { perror("Erro"); return 1; }
    fwrite(&valor, sizeof(valor), 1, fp);
    fclose(fp);
    return 0;
}

Em resumo, a portabilidade exige cuidado e planejamento. Para aplicações críticas, considere usar bibliotecas de serialização ou formatos como JSON, XML, ou até mesmo o formato hdf5.

Boas Práticas

Ao trabalhar com arquivos binários, siga estas recomendações:

  • Sempre verifique o retorno de fopen, fread, fwrite, fseek para tratar erros adequadamente.
  • Feche o arquivo com fclose quando não precisar mais dele.
  • Use modos binários ("rb", "wb", "ab") para evitar conversões de nova linha.
  • Prefira tipos de tamanho fixo para portabilidade.
  • Evite gravar ponteiros ou endereços de memória, pois eles são específicos de cada execução.
  • Documente o layout do arquivo (campos, tamanhos, ordens) para facilitar manutenção e interoperabilidade.

Referências

Exercícios

  1. Escreva um programa que crie um arquivo binário chamado numeros.bin contendo os números de 1 a 100 (como int). Depois, leia o arquivo e imprima a soma de todos os números.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        FILE *fp = fopen("numeros.bin", "wb");
        if (fp == NULL) { perror("Erro"); return 1; }
        for (int i = 1; i <= 100; i++) {
            fwrite(&i, sizeof(int), 1, fp);
        }
        fclose(fp);
    
        fp = fopen("numeros.bin", "rb");
        if (fp == NULL) { perror("Erro"); return 1; }
        int valor, soma = 0;
        while (fread(&valor, sizeof(int), 1, fp) == 1) {
            soma += valor;
        }
        fclose(fp);
        printf("Soma: %d\n", soma);
        return 0;
    }
  2. Crie uma função que receba um nome de arquivo e um índice, e retorne o valor do inteiro nesse índice (posição índice * sizeof(int)). Use fseek e fread.

    ✓ Resposta:
    #include <stdio.h>
    
    int ler_int_em_indice(const char *nome, long indice) {
        FILE *fp = fopen(nome, "rb");
        if (fp == NULL) { perror("Erro"); return -1; }
        if (fseek(fp, indice * sizeof(int), SEEK_SET) != 0) {
            perror("Erro no fseek");
            fclose(fp);
            return -1;
        }
        int valor;
        if (fread(&valor, sizeof(int), 1, fp) != 1) {
            perror("Erro na leitura");
            fclose(fp);
            return -1;
        }
        fclose(fp);
        return valor;
    }
    
    int main() {
        printf("%d\n", ler_int_em_indice("numeros.bin", 9)); // deve imprimir 10
        return 0;
    }
  3. Defina uma estrutura Ponto com campos x e y (int). Grave um vetor de 5 pontos em um arquivo binário. Depois, leia apenas o terceiro ponto e imprima suas coordenadas.

    ✓ Resposta:
    #include <stdio.h>
    
    typedef struct { int x, y; } Ponto;
    
    int main() {
        Ponto pontos[5] = {{1,2},{3,4},{5,6},{7,8},{9,10}};
        FILE *fp = fopen("pontos.bin", "wb");
        if (fp == NULL) { perror("Erro"); return 1; }
        fwrite(pontos, sizeof(Ponto), 5, fp);
        fclose(fp);
    
        fp = fopen("pontos.bin", "rb");
        if (fp == NULL) { perror("Erro"); return 1; }
        fseek(fp, 2 * sizeof(Ponto), SEEK_SET); // pula 2 pontos
        Ponto p;
        fread(&p, sizeof(Ponto), 1, fp);
        fclose(fp);
        printf("Terceiro ponto: (%d, %d)\n", p.x, p.y);
        return 0;
    }
  4. Explique por que arquivos binários podem não ser portáveis entre sistemas diferentes. Cite pelo menos duas causas e como mitigá-las.

    ✓ Resposta:

    Os arquivos binários podem não ser portáveis porque dependem da arquitetura do sistema. Duas causas principais são:

    • Endianness: A ordem dos bytes de inteiros e floats pode variar (little-endian vs big-endian). Mitigação: converter para uma ordem padrão (ex.: little-endian) antes de gravar e na leitura converter de volta.
    • Tamanho dos tipos: O tamanho de int, long, etc., pode variar (32 vs 64 bits). Mitigação: usar tipos de tamanho fixo como int32_t, uint16_t.

    Outras causas incluem padding de estruturas e codificação de caracteres. Para mitigar, pode-se gravar campo a campo e documentar o formato.

  5. Escreva um programa que leia um arquivo binário contendo 10 números inteiros e exiba a posição do arquivo (em bytes) antes e depois de cada leitura, usando ftell.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        FILE *fp = fopen("numeros.bin", "rb");
        if (fp == NULL) { perror("Erro"); return 1; }
    
        int valor;
        for (int i = 0; i < 10; i++) {
            long pos_antes = ftell(fp);
            fread(&valor, sizeof(int), 1, fp);
            long pos_depois = ftell(fp);
            printf("Leitura %d: antes=%ld, depois=%ld\n", i+1, pos_antes, pos_depois);
        }
        fclose(fp);
        return 0;
    }