Input e Output Binario in Linguaggio C

Per poter lavorare con file binari, la libreria standard del linguaggio C mette a disposizione due funzioni: fwrite e fread.

Queste due funzioni permettono ad un programma di leggere e scrivere blocchi di dati binari in modo efficiente in un unico passaggio. Esse sono particolarmente utili quando si lavora con strutture di dati complesse o grandi quantità di dati, poiché consentono di evitare la necessità di convertire i dati in formato testuale.

Concetti Chiave
  • Le funzioni fwrite() e fread() sono utilizzate per scrivere e leggere blocchi di dati binari in linguaggio C.
  • Queste funzioni sono particolarmente utili per lavorare con strutture di dati complesse o grandi quantità di dati.
  • fwrite() scrive un blocco di dati dalla memoria ad uno stream di output, mentre fread() legge un blocco di dati da uno stream di input alla memoria.
  • È importante comprendere la differenza tra i parametri size e count in entrambe le funzioni per gestire correttamente il numero di byte letti o scritti.
  • Queste funzioni possono essere utilizzate anche per salvare e leggere strutture dati definite dall'utente, ma è necessario prestare attenzione ai campi di tipo puntatore all'interno delle strutture.

Funzione di Output Binario: fwrite

La funzione fwrite() è progettata per copiare un array dalla memoria ad uno stream di output, come un file aperto in modalità binaria.

La sua firma è la seguente:

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

Il suo primo argomento, ptr, è un puntatore al blocco di memoria o array che si desidera scrivere. Il secondo argomento, size, specifica la dimensione in byte di ogni elemento da scrivere, mentre il terzo argomento, count, indica il numero di elementi da scrivere. Infine, il quarto argomento, stream, è un puntatore al file aperto in modalità binaria in cui i dati verranno scritti.

Supponiamo, ad esempio, di avere un array statico di interi che vogliamo scrivere su un file binario. Ecco come potremmo farlo:

int array[] = {1, 2, 3, 4, 5};
size_t dimensione_elemento = sizeof(int);
size_t num_elementi = sizeof(array) / sizeof(array[0]);

fwrite(array, dimensione_elemento, num_elementi, file_binario);

In questo esempio, fwrite() scrive l'intero array di interi nel file binario specificato da file_binario. La funzione restituisce il numero di elementi effettivamente scritti, che dovrebbe essere uguale a num_elementi se l'operazione ha avuto successo. Da notare che il numero di elementi restituito non è il numero di byte scritti, ma il numero di elementi specificati nel terzo argomento della funzione.

Ovviamente, non siamo obbligati a scrivere l'array per intero. Possiamo scegliere di scrivere solo una parte di esso, specificando un valore inferiore per count. Ad esempio, per scrivere solo i primi tre elementi dell'array, potremmo fare così:

fwrite(array, dimensione_elemento, 3, file_binario);

In caso di errore durante l'operazione di scrittura, fwrite() restituisce un valore inferiore a count, indicando il numero di elementi effettivamente scritti prima dell'errore.

Funzione di Input Binario: fread

La funzione fread() è progettata per leggere un blocco di dati binari da uno stream di input, come un file aperto in modalità binaria, e copiarli in un'area di memoria. I suoi parametri sono simili a quelli di fwrite(). La sua firma è la seguente:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

Il primo argomento, ptr, è un puntatore all'area di memoria in cui i dati letti verranno memorizzati. Il secondo argomento, size, specifica la dimensione in byte di ogni elemento da leggere, mentre il terzo argomento, count, indica il numero di elementi da leggere. Infine, il quarto argomento, stream, è un puntatore al file aperto in modalità binaria da cui i dati verranno letti.

Supponiamo di voler effettuare l'operazione inversa rispetto all'esempio precedente, leggendo i dati da un file binario in un array di interi. Ecco come potremmo farlo:

int array[5];
size_t dimensione_elemento = sizeof(int);
size_t num_elementi_letto = fread(array, dimensione_elemento, 5, file_binario);

In questo esempio, fread() legge fino a 5 interi dal file binario specificato da file_binario e li memorizza nell'array array. La funzione restituisce il numero di elementi effettivamente letti, che potrebbe essere inferiore a 5 se si raggiunge la fine del file prima di leggere tutti gli elementi richiesti.

In quest'ultimo caso, è importante verificare il valore restituito da fread() per assicurarsi che l'operazione di lettura sia andata a buon fine e che il numero di elementi letti sia quello previsto. In caso contrario, possiamo adoperare la funzione feof() per verificare se è stata raggiunta la fine del file, o ferror() per controllare se si è verificato un errore di lettura.

Osservazioni sull'uso di fwrite e fread

Una prima osservazione importante riguarda i due parametri size e count. Questi due parametri non vanno confusi, specialmente quando si vuole leggere da un file.

Infatti, consideriamo un attimo la seguente chiamata a fread():

fread(array, 1, 100, file);

In questo caso, stiamo chiedendo a fread di leggere 100 elementi di dimensione 1 byte ciascuno, ovvero di leggere 100 byte dal file. Per tal motivo, in caso di successo, la funzione restituirà il numero 100, che rappresenta il numero di elementi letti (ognuno di 1 byte). In ogni caso, essa restituirà un numero compreso tra 0 e 100, a seconda di quanti elementi sono stati effettivamente letti.

Se, invece, invertiamo i due parametri, come nel seguente esempio:

fread(array, 100, 1, file);

Stiamo chiedendo a fread di leggere 1 elemento di dimensione 100 byte. Pertanto, in caso di successo, la funzione restituirà il numero 1, che rappresenta il numero di elementi letti.

In entrambe i casi la funzione legge 100 byte dal file, ma il controllo che dobbiamo effettuare sul valore restituito dalla funzione cambia a seconda di come abbiamo impostato i parametri size e count.

Salvataggio e Lettura di Strutture Dati

Sopra, abbiamo affermato che fwrite e fread servono per leggere e scrivere blocchi di dati binari, o array, in modo efficiente.

In realtà, queste funzioni possono essere utilizzate anche per salvare e leggere strutture dati complesse definite dall'utente tramite la parola chiave struct. Possiamo, infatti, usare fwrite per scrivere un'intera struttura su un file binario, e fread per leggere la struttura dal file e memorizzarla in una variabile di tipo struct. In questo modo, possiamo salvare e recuperare facilmente dati strutturati senza doverli convertire in formato testuale.

Ad esempio, supponiamo di avere la seguente struttura dati che rappresenta una persona:

struct Persona {
    char nome[50];
    int eta;
};

Possiamo scrivere un'istanza di questa struttura su un file binario utilizzando fwrite in questo modo:

struct Persona p = {"Mario Rossi", 30};
fwrite(&p, sizeof(struct Persona), 1, file_binario);

Allo stesso modo, possiamo leggere la struttura dal file binario utilizzando fread:

struct Persona p;
fread(&p, sizeof(struct Persona), 1, file_binario);

C'è solo una cosa a cui dobbiamo fare attenzione quando salviamo e leggiamo strutture dati con fwrite e fread: la presenza di campi di tipo puntatore all'interno della struttura. Nella struttura di esempio di sopra, non ci sono puntatori, quindi possiamo salvare e leggere la struttura senza problemi.

Tuttavia, supponiamo di modificare la struttura Persona in modo che il nome non sia più una stringa statica (un array statico di caratteri), ma un puntatore a carattere:

struct Persona {
    char *nome;
    int eta;
};

Per creare una struttura di questo tipo, dobbiamo allocare dinamicamente la memoria per il campo nome:

struct Persona p;
p.nome = malloc(50 * sizeof(char));
strcpy(p.nome, "Mario Rossi");
p.eta = 30;

Ma a questo punto, se proviamo a salvare la struttura p su un file binario utilizzando fwrite, come nel seguente esempio:

fwrite(&p, sizeof(struct Persona), 1, file_binario);

Non stiamo salvando il contenuto della stringa "Mario Rossi", ma solo il valore del puntatore nome, che rappresenta un indirizzo di memoria. Quando leggeremo la struttura dal file binario, il campo nome conterrà un indirizzo di memoria che non è più valido, poiché la memoria a cui puntava potrebbe essere stata liberata o sovrascritta.

Quindi, in questi casi, dobbiamo adottare un approccio diverso per salvare e leggere i dati. Nelle prossime lezioni, vedremo come gestire correttamente la lettura e la scrittura di strutture dati complesse che contengono puntatori, utilizzando tecniche come la serializzazione e la deserializzazione dei dati.

Esempio: Rubrica Telefonica

Proviamo a mettere insieme quanto visto finora con un esempio completo di programma che gestisce una rubrica telefonica salvando e leggendo i contatti da un file binario.

Questo programma permette di effettuare le seguenti operazioni:

  1. Aggiungere un nuovo contatto alla rubrica.
  2. Visualizzare tutti i contatti presenti nella rubrica.
  3. Salvare i contatti su un file binario.
  4. Caricare i contatti da un file binario.

Per prima cosa, definiamo la struttura dati per rappresentare un contatto della rubrica:

struct Contatto {
    char nome[50];
    char numero_telefono[15];
};

Il programma memorizzerà i contatti in memoria utilizzando un array statico di strutture Contatto. Questo array potrà contenere un massimo di 100 contatti. In una variabile separata, terremo traccia del numero di contatti attualmente presenti nella rubrica.

Quando il programma salva i contatti su un file binario, utilizzerà la funzione fwrite per scrivere due cose:

  1. Il numero di contatti presenti nella rubrica.
  2. L'array di contatti.

Analogamente, quando il programma carica i contatti da un file binario, utilizzerà la funzione fread per leggere prima il numero di contatti e poi l'array di contatti.

Realizziamo, prima, la funzione per salvare i contatti su un file binario:

/*
 * Funzione per salvare la rubrica su un file binario
 * Parametri:
 * - nome_file: il nome del file in cui salvare la rubrica
 * - rubrica: l'array di contatti da salvare
 * - num_contatti: il numero di contatti presenti nella rubrica
 * Ritorna:
 * - 0 in caso di successo, -1 in caso di errore
 */
int salva_rubrica(const char *nome_file,
                  struct Contatto rubrica[],
                  size_t num_contatti) {
    FILE *file = fopen(nome_file, "wb");
    if (file == NULL) {
        return -1; // Errore nell'apertura del file
    }

    // Scrive prima il numero di contatti
    fwrite(&num_contatti, sizeof(size_t), 1, file);

    // Scrive l'array di contatti
    fwrite(rubrica, sizeof(struct Contatto), num_contatti, file);

    fclose(file);

    // Successo
    return 0;
}

Adesso, implementiamo la funzione per caricare i contatti da un file binario. Questa funzione leggerà prima il numero di contatti e poi l'array di contatti. Per evitare problemi di overflow, la funzione controllerà che il numero di contatti letto non superi la dimensione massima dell'array di contatti.

/*
 * Funzione per caricare la rubrica da un file binario
 * Parametri:
 * - nome_file: il nome del file da cui caricare la rubrica
 * - rubrica: l'array in cui memorizzare i contatti caricati
 * - max_contatti: la dimensione massima dell'array di contatti
 * Ritorna:
 * - il numero di contatti caricati in caso di successo, -1 in caso di errore
 */
int carica_rubrica(const char *nome_file,
                   struct Contatto rubrica[],
                   size_t max_contatti) {
    FILE *file = fopen(nome_file, "rb");
    if (file == NULL) {
        return -1; // Errore nell'apertura del file
    }

    size_t num_contatti;
    // Legge il numero di contatti
    fread(&num_contatti, sizeof(size_t), 1, file);

    // Controlla che il numero di contatti non superi la dimensione massima
    if (num_contatti > max_contatti) {
        fclose(file);
        return -1; // Errore: numero di contatti troppo grande
    }

    // Legge l'array di contatti
    fread(rubrica, sizeof(struct Contatto), num_contatti, file);

    fclose(file);

    // Ritorna il numero di contatti caricati
    return num_contatti;
}

Con queste due funzioni, possiamo ora implementare il resto del programma per gestire la rubrica telefonica, permettendo all'utente di aggiungere contatti, visualizzarli, salvarli su un file binario e caricarli da un file binario.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_CONTATTI 100

struct Contatto {
    char nome[50];
    char numero_telefono[15];
};

int salva_rubrica(const char *nome_file,
                  struct Contatto rubrica[],
                  size_t num_contatti) {
    FILE *file = fopen(nome_file, "wb");
    if (file == NULL) {
        return -1; // Errore nell'apertura del file
    }

    fwrite(&num_contatti, sizeof(size_t), 1, file);
    fwrite(rubrica, sizeof(struct Contatto), num_contatti, file);

    fclose(file);
    return 0;
}

int carica_rubrica(const char *nome_file,
                   struct Contatto rubrica[],
                   size_t max_contatti) {
    FILE *file = fopen(nome_file, "rb");
    if (file == NULL) {
        return -1; // Errore nell'apertura del file
    }

    size_t num_contatti;
    fread(&num_contatti, sizeof(size_t), 1, file);

    if (num_contatti > max_contatti) {
        fclose(file);
        return -1; // Errore: numero di contatti troppo grande
    }

    fread(rubrica, sizeof(struct Contatto), num_contatti, file);

    fclose(file);
    return num_contatti;
}

int main() {
    struct Contatto rubrica[MAX_CONTATTI];
    size_t num_contatti = 0;
    int scelta;

    do {
        printf("Rubrica Telefonica\n");
        printf("1. Aggiungi Contatto\n");
        printf("2. Visualizza Contatti\n");
        printf("3. Salva Rubrica su File\n");
        printf("4. Carica Rubrica da File\n");
        printf("5. Esci\n");
        printf("Scelta: ");
        scanf("%d", &scelta);
        // Consuma il carattere di nuova linea
        getchar();

        if (scelta == 1) {
            if (num_contatti < MAX_CONTATTI) {
                printf("Nome: ");
                fgets(rubrica[num_contatti].nome,
                      sizeof(rubrica[num_contatti].nome),
                      stdin);
                // Rimuove nuova linea
                rubrica[num_contatti].nome[strcspn(rubrica[num_contatti].nome, "\n")] = 0;

                printf("Numero di Telefono: ");
                fgets(rubrica[num_contatti].numero_telefono,
                      sizeof(rubrica[num_contatti].numero_telefono),
                      stdin);
                // Rimuove nuova linea
                rubrica[num_contatti].numero_telefono[strcspn(rubrica[num_contatti].numero_telefono, "\n")] = 0;

                num_contatti++;
            } else {
                printf("Rubrica piena!\n");
            }
        } else if (scelta == 2) {
            for (size_t i = 0; i < num_contatti; i++) {
                printf("%s - %s\n", rubrica[i].nome, rubrica[i].numero_telefono);
            }
        } else if (scelta == 3) {
            if (salva_rubrica("rubrica.bin", rubrica, num_contatti) == 0) {
                printf("Rubrica salvata con successo.\n");
            } else {
                printf("Errore nel salvataggio della rubrica.\n");
            }
        } else if (scelta == 4) {
            int contatti_caricati = carica_rubrica("rubrica.bin", rubrica, MAX_CONTATTI);
            if (contatti_caricati >= 0) {
                num_contatti = contatti_caricati;
                printf("Rubrica caricata con successo.\n");
            } else {
                printf("Errore nel caricamento della rubrica.\n");
            }
        }
        printf("\n");
    } while (scelta != 5);

    return 0;
}

Se proviamo a compilare ed eseguire questo programma, possiamo aggiungere contatti alla rubrica, visualizzarli, salvarli su un file binario chiamato rubrica.bin e caricarli nuovamente dal file.

Ad esempio, se aggiungiamo due contatti:

Rubrica Telefonica
1. Aggiungi Contatto
2. Visualizza Contatti
3. Salva Rubrica su File
4. Carica Rubrica da File
5. Esci
Scelta: 1
Nome: Mario Rossi
Numero di Telefono: 0123456789

Rubrica Telefonica
1. Aggiungi Contatto
2. Visualizza Contatti
3. Salva Rubrica su File
4. Carica Rubrica da File
5. Esci
Scelta: 1
Nome: Luigi Bianchi
Numero di Telefono: 0987654321

Rubrica Telefonica
1. Aggiungi Contatto
2. Visualizza Contatti
3. Salva Rubrica su File
4. Carica Rubrica da File
5. Esci
Scelta: 2
Mario Rossi - 0123456789
Luigi Bianchi - 0987654321

Rubrica Telefonica
1. Aggiungi Contatto
2. Visualizza Contatti
3. Salva Rubrica su File
4. Carica Rubrica da File
5. Esci
Scelta: 3
Rubrica salvata con successo.

Rubrica Telefonica
1. Aggiungi Contatto
2. Visualizza Contatti
3. Salva Rubrica su File
4. Carica Rubrica da File
5. Esci
Scelta: 5

In questo modo, il programma creerà un file binario rubrica.bin contenente i contatti salvati. Possiamo poi eseguire nuovamente il programma, scegliere l'opzione per caricare la rubrica dal file binario e visualizzare i contatti precedentemente salvati:

Rubrica Telefonica
1. Aggiungi Contatto
2. Visualizza Contatti
3. Salva Rubrica su File
4. Carica Rubrica da File
5. Esci
Scelta: 4
Rubrica caricata con successo.

Rubrica Telefonica
1. Aggiungi Contatto
2. Visualizza Contatti
3. Salva Rubrica su File
4. Carica Rubrica da File
5. Esci
Scelta: 2
Mario Rossi - 0123456789
Luigi Bianchi - 0987654321