Input e Output Formattato in Linguaggio C

In questa lezione studieremo le funzioni di libreria del linguaggio C che permettono di leggere e scrivere file e che utilizzano le cosiddette stringhe di formato.

Tali funzioni, di cui in una certa misura fanno parte anche le funzioni di input e output standard printf() e scanf(), permettono di leggere e scrivere dati strutturati specificando il formato con cui i dati devono essere interpretati o scritti, utilizzando appunto delle stringhe di formato.

Esse funzionano convertendo dati numerici in dati testuali (nel caso della scrittura) o convertendo dati testuali in dati numerici (nel caso della lettura), secondo le specifiche indicate nella stringa di formato. Le altre funzioni di input e output che vedremo nelle prossime lezioni non hanno, invece, tale capacità.

Concetti Chiave
  • Le funzioni fprintf e fscanf permettono di scrivere e leggere dati formattati su file utilizzando stringhe di formato.
  • La funzione fprintf scrive dati su un file secondo il formato specificato.
  • La funzione fscanf legge dati da un file secondo il formato specificato.
  • Entrambe le funzioni accettano un numero variabile di argomenti e restituiscono il numero di elementi scritti o letti con successo.
  • Le stringhe di formato utilizzano specificatori di formato per rappresentare diversi tipi di dati, come interi, numeri in virgola mobile, caratteri e stringhe.

La funzione fprintf

La funzione fprintf() è molto simile alla funzione printf(), ma invece di scrivere l'output sullo schermo, lo scrive su un file.

Entrambe le funzioni prendono in ingresso una stringa di formato per controllare come i dati devono essere scritti. La firma delle due funzioni è la seguente:

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);

Notiamo una prima cosa: entrambe le funzioni presentano una lista di parametri che termina con i tre puntini ..., il che indica che esse accettano un numero variabile di argomenti.

Inoltre, il valore da esse restituito è un intero che rappresenta il numero di caratteri scritti (escluso il carattere di terminazione nullo \0), oppure un valore negativo in caso di errore.

La principale differenza tra le due funzioni è che fprintf() richiede come primo parametro un puntatore a un oggetto di tipo FILE, che rappresenta il file su cui scrivere l'output, mentre printf() non richiede tale parametro perché scrive direttamente sullo standard output (lo schermo).

In realtà, lo standard output è rappresentato da un oggetto di tipo FILE chiamato stdout, che è definito nella libreria standard del linguaggio C. Quindi, possiamo considerare che la chiamata a printf() sia equivalente a:

// Chiamate equivalenti
printf("Hello, World!\n");
fprintf(stdout, "Hello, World!\n");

Le due chiamate di sopra producono lo stesso risultato, scrivendo la stringa "Hello, World!" seguita da un carattere di nuova linea sullo schermo.

La flessibilità della funzione fprintf() sta nel fatto che non si tratta di una semplice funzione di scrittura su file. Essa lavora, infatti, con qualsiasi stream di output, che può essere un file, lo standard output, o anche uno stream di errore standard (stderr). Infatti, quest'ultimo è il suo utilizzo più comune quando si desidera scrivere messaggi di errore.

Ad esempio, per scrivere un messaggio di errore sullo standard error, possiamo utilizzare fprintf() in questo modo:

fprintf(stderr, "Errore: Impossibile aprire il file.\n");

Scrivere un messaggio di errore sullo standard error è utile perché permette di separare i messaggi di errore dall'output normale del programma. Così facendo, se l'utente ha reindirizzato l'output del programma su un file, i messaggi di errore continueranno a essere visualizzati sullo schermo, rendendo più facile individuare e risolvere eventuali problemi.

Sia printf() che fprintf() supportano una vasta gamma di specificatori di formato per rappresentare diversi tipi di dati, come interi, numeri in virgola mobile, caratteri e stringhe. Alcuni di essi li abbiamo già esaminati nelle lezioni precedenti: ad esempio abbiamo visto come scrivere numeri interi con printf e scrivere numeri in virgola mobile con printf. Gli stessi specificatori di formato possono essere utilizzati con fprintf().

In ogni caso, esamineremo in dettaglio i vari specificatori di formato nelle prossime lezioni.

Dal momento che il funzionamento di fprintf() è molto simile a quello di printf(), si parla di famiglia di funzioni printf, che include anche altre funzioni più o meno oscure come la funzione vprintf() e vfprintf() che però studieremo in seguito.

Esempio di utilizzo di fprintf

Adesso, vediamo un esempio di utilizzo della funzione fprintf() per scrivere dati su un file. Supponiamo di voler creare un file di testo chiamato dati.txt e scrivere al suo interno alcune informazioni formattate.

Ad esempio, vogliamo scrivere un programma che salvi il quadrato e il cubo dei primi 20 numeri interi positivi in un file di testo. Vogliamo che la struttura del file sia la seguente:

    Numero   Quadrato       Cubo
--------------------------------
         1          1          1
         2          4          8
         3          9         27
...
        20        400       8000

Quindi, i numeri devono essere allineati a destra in colonne di larghezza fissa di 10 caratteri. Inoltre, il file deve possedere un'intestazione che descriva il contenuto delle colonne.

Iniziamo a realizzare lo scheletro del programma:

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

int main() {
    FILE *file;
    int i;

    // Apre il file in modalità scrittura
    file = fopen("dati.txt", "w");
    if (file == NULL) {
        printf("Errore nell'apertura del file\n");
        return EXIT_FAILURE;
    }

    // Scrive l'intestazione del file
    // ...

    // Scrive i dati nel file
    for (i = 1; i <= 20; i++) {
        // Calcola il quadrato e il cubo
        int quadrato = i * i;
        int cubo = quadrato * i;

        // Scrive i dati formattati nel file
        // ...
    }

    // Chiude il file
    fclose(file);

    return EXIT_SUCCESS;
}

Questo scheletro contiene le parti principali del programma. Mancano ancora le parti in cui scriviamo l'intestazione e i dati formattati nel file.

Per scrivere l'intestazione del file, possiamo specificare una stringa di formato che allinei i titoli delle colonne. Usiamo la funzione fprintf() per scrivere l'intestazione:

// Scrive l'intestazione del file
int lunghezza_intestazione =
    fprintf(file, "%10s %10s %10s\n", "Numero", "Quadrato", "Cubo");

In questa riga, usiamo il specificatore di formato %10s per allineare a destra le stringhe "Numero", "Quadrato" e "Cubo" in colonne di larghezza 10 caratteri.

Inoltre, memorizziamo la lunghezza dell'intestazione per poter scrivere una linea di separazione della stessa lunghezza. Infatti, fprintf() restituisce il numero di caratteri scritti, che utilizziamo per creare la linea di separazione:

// Scrive la linea di separazione
for (i = 0; i < lunghezza_intestazione - 1; i++) {
    fprintf(file, "-");
}
fprintf(file, "\n");

Adesso, per scrivere i dati formattati nel file, usiamo un altro specificatore di formato per allineare i numeri interi. Usiamo %10d per allineare a destra i numeri in colonne di larghezza 10 caratteri:

// Scrive i dati nel file
for (i = 1; i <= 20; i++) {
    // Calcola il quadrato e il cubo
    int quadrato = i * i;
    int cubo = quadrato * i;
    // Scrive i dati formattati nel file
    fprintf(file, "%10d %10d %10d\n", i, quadrato, cubo);
}

Combinando tutte le parti, otteniamo il seguente programma completo:

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

int main() {
    FILE *file;
    int i;

    // Apre il file in modalità scrittura
    file = fopen("dati.txt", "w");
    if (file == NULL) {
        printf("Errore nell'apertura del file\n");
        return EXIT_FAILURE;
    }

    // Scrive l'intestazione del file
    int lunghezza_intestazione =
            fprintf(file, "%10s %10s %10s\n", "Numero", "Quadrato", "Cubo");

    // Scrive la linea di separazione
    for (i = 0; i < lunghezza_intestazione - 1; i++) {
        fprintf(file, "-");
    }
    fprintf(file, "\n");

    // Scrive i dati nel file
    for (i = 1; i <= 20; i++) {
        // Calcola il quadrato e il cubo
        int quadrato = i * i;
        int cubo = quadrato * i;
        // Scrive i dati formattati nel file
        fprintf(file, "%10d %10d %10d\n", i, quadrato, cubo);
    }

    // Chiude il file
    fclose(file);

    return EXIT_SUCCESS;
}

Se compiliamo ed eseguiamo questo programma, otterremo un file di testo chiamato dati.txt con il seguente contenuto:

    Numero   Quadrato       Cubo
--------------------------------
         1          1          1
         2          4          8
         3          9         27
         4         16         64
         5         25        125
         6         36        216
         7         49        343
         8         64        512
         9         81        729
        10        100       1000
        11        121       1331
        12        144       1728
        13        169       2197
        14        196       2744
        15        225       3375
        16        256       4096
        17        289       4913
        18        324       5832
        19        361       6859
        20        400       8000

La funzione fscanf

Analogamente a fprintf(), la funzione fscanf() è simile alla funzione scanf(), ma invece di leggere l'input dalla tastiera, lo legge da un file.

Anche fscanf() utilizza una stringa di formato per specificare come i dati devono essere letti e convertiti. La firma delle due funzioni è la seguente:

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);

Come fprintf(), fscanf() richiede come primo parametro un puntatore a un oggetto di tipo FILE, che rappresenta il file da cui leggere l'input, mentre scanf() non richiede tale parametro perché legge direttamente dallo standard input (la tastiera).

Inoltre, anche in questo caso, entrambe le funzioni accettano un numero variabile di argomenti e restituiscono un intero che rappresenta il numero di elementi letti con successo, oppure EOF in caso di errore o fine del file.

Dopo il parametro che specifica la stringa di formato, entrambe le funzioni richiedono una serie di puntatori alle variabili in cui memorizzare i dati letti.

Ad esempio, se vogliamo leggere un intero e un numero in virgola mobile dalla tastiera, possiamo usare scanf() in questo modo:

int numero;
float reale;
scanf("%d %f", &numero, &reale);

Tale chiamata è equivalente a:

int numero;
float reale;
fscanf(stdin, "%d %f", &numero, &reale);

Le due funzioni, fscanf e scanf, ritornano prematuramente se si verifica uno degli eventi seguenti:

  1. Si raggiunge la fine del file (EOF). Nel caso di scanf(), ciò può accadere se l'utente invia un segnale di EOF (ad esempio, premendo Ctrl+D su Unix o Ctrl+Z seguito da Invio su Windows).
  2. Si presenta un errore di corrispondenza tra l'input e la stringa di formato. Ad esempio, se si tenta di leggere un intero ma l'input fornito non è un numero valido, la funzione si fermerà e restituirà il numero di elementi letti con successo fino a quel punto.

Quindi il valore di ritorno di fscanf() e scanf() può essere utilizzato per verificare se la lettura è avvenuta con successo e quanti elementi sono stati letti correttamente. Il valore EOF indica che si è raggiunta la fine del file oppure nessun elemento è stato letto a causa di un errore.

Nei programmi C sono spesso adoperati dei cicli while che continuano a leggere dati da un file finché non si raggiunge la fine del file e che testano il valore di ritorno per verificare se la lettura è avvenuta con successo.

Ad esempio, volendo leggere una serie di numeri interi da un file di testo fino alla fine del file, possiamo utilizzare il seguente schema di programma:

while (fscanf(file, "%d", &numero) == 1) {
    // Elabora il numero letto
}

Esempio di utilizzo di fscanf

Vediamo un esempio di utilizzo della funzione fscanf() per leggere dati da un file.

Supponiamo di avere un file di testo chiamato dati.csv che contiene una serie di numeri interi in formato CSV (Comma-Separated Values), come mostrato di seguito:

10,20,30
40,50,60
70,80,90

Un file CSV è un formato comune per memorizzare dati tabulari, in cui i valori sono separati da virgole e ogni riga rappresenta un record. Esso può essere facilmente creato e letto da programmi di fogli di calcolo come Microsoft Excel o Google Sheets.

Supponiamo che il nostro file dati.csv contenga una serie di numeri interi disposti a triplette su ogni riga, separati da virgole. Vogliamo leggere questi numeri e calcolare la somma di tutte le colonne del file.

Iniziamo a realizzare lo scheletro del programma:

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

int main() {
    FILE *file;
    int num1, num2, num3;
    int somma1 = 0, somma2 = 0, somma3 = 0;

    // Apre il file in modalità lettura
    file = fopen("dati.csv", "r");
    if (file == NULL) {
        printf("Errore nell'apertura del file\n");
        return EXIT_FAILURE;
    }

    // Legge i dati dal file
    while (/* Condizione di lettura */) {
        // Elabora i numeri letti
        // ...
    }

    // Stampa le somme delle colonne
    printf("Somma colonna 1: %d\n", somma1);
    printf("Somma colonna 2: %d\n", somma2);
    printf("Somma colonna 3: %d\n", somma3);

    // Chiude il file
    fclose(file);

    return EXIT_SUCCESS;
}

Per leggere i dati dal file, utilizziamo la funzione fscanf() all'interno di un ciclo while. La condizione di lettura deve verificare se sono stati letti correttamente tre numeri interi separati da virgole. La stringa di formato per fscanf() sarà quindi "%d,%d,%d".

// Legge i dati dal file
while (fscanf(file, "%d,%d,%d", &num1, &num2, &num3) == 3) {
    // Elabora i numeri letti
    somma1 += num1;
    somma2 += num2;
    somma3 += num3;
}

In questo modo, il ciclo continuerà a leggere triplette di numeri finché non si raggiunge la fine del file o si verifica un errore di lettura.

Combinando tutte le parti, otteniamo il seguente programma completo:

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

int main() {
    FILE *file;
    int num1, num2, num3;
    int somma1 = 0, somma2 = 0, somma3 = 0;

    // Apre il file in modalità lettura
    file = fopen("dati.csv", "r");
    if (file == NULL) {
        printf("Errore nell'apertura del file\n");
        return EXIT_FAILURE;
    }

    // Legge i dati dal file
    while (fscanf(file, "%d,%d,%d", &num1, &num2, &num3) == 3) {
        // Elabora i numeri letti
        somma1 += num1;
        somma2 += num2;
        somma3 += num3;
    }

    // Stampa le somme delle colonne
    printf("Somma colonna 1: %d\n", somma1);
    printf("Somma colonna 2: %d\n", somma2);
    printf("Somma colonna 3: %d\n", somma3);

    // Chiude il file
    fclose(file);

    return EXIT_SUCCESS;
}

Se compiliamo ed eseguiamo questo programma con il file dati.csv mostrato sopra, otterremo il seguente output:

Somma colonna 1: 120
Somma colonna 2: 150
Somma colonna 3: 180