Per leggere un file di testo in linguaggio C abbiamo a disposizione principalmente due funzioni: la fscanf() e la fgets().

La scelta di utilizzare l'una o l'altra è spesso una questione di religione; in questo articolo analizziamo pregi e difetti di ciascuna delle due funzioni affrontando alcune semplici prove sul campo.

goats 173940 420

Presentazione

Prima di iniziare il nostro test, diamo un'occhiata ai prototipi delle due funzioni:

int fscanf ( FILE * stream, const char * format, ... );
char * fgets ( char * str, int num, FILE * stream );

Sostanzialmente:
- fscanf() lavora a "campi", ovvero stringhe separate fra loro da spazio o newline, e interpreta la configurazione specificata in format
- fgets() è più semplice e legge una riga di testo fino al newline

Una buona descrizione delle funzioni la possiamo trovare qui:
- fscanf() http://www.cplusplus.com/reference/cstdio/fscanf/
- fgets() http://www.cplusplus.com/reference/cstdio/fgets/

Ora, però, non preoccupiamoci troppo dei dettagli; durante le prove avremo modo di evidenziare le caratteristiche di ciascuna funzione, in modo poi da poterne sfruttare al meglio le potenzialità.

Le prove che effettueremo sono:
a) lettura di una lista di stringhe
b) lettura di una lista di interi
c) lettura con formattazione

Iniziamo quindi la sfida!

A) Lettura di una lista di stringhe

Supponiamo di voler leggere riga per riga un file di testo, stampando a video le righe, e di contare il numero di linee lette.
Il nostro file in input contiene:

The quick brown fox
jumps over the lazy dog

Leggere una stringa con fscanf() è pericoloso in quanto non vi sono controlli sulla dimensione del buffer e si rischia pertanto un buffer overflow (ovvero uno sforamento del limite). Esiste però la possibilità di definire la dimensione massima del campo, inserendolo nello specificatore "%s" (es. "%100s"): certo non è comodo ma è possibile, con l'accortezza di considerare che il terminatore non è conteggiato, quindi dovremo inserire la dimensione massima della stringa diminuita di 1 (es. se la dimensione è 100 dobbiamo scrivere "%99s").

Vediamo le due implementazioni:

// con fscanf()

#define MAXSTR        100

int main(void)
{
    char tmpstr[MAXSTR];
    int cntlines = 0;
    FILE *fp;

    fp = fopen("inputA.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fscanf(fp, "%99s", tmpstr) == 1)
            {
                printf(tmpstr);
                cntlines++;
            }
            else
            {
                printf("errore!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("righe lette: %d\n", cntlines);
    getch();

    return 0;
}

// con fgets()

#define MAXSTR        100

int main(void)
{
    char tmpstr[MAXSTR];
    int cntlines = 0;
    FILE *fp;

    fp = fopen("inputA.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fgets(tmpstr, MAXSTR, fp) != NULL)
            {
                printf(tmpstr);
                cntlines++;
            }
            else
            {
                printf("errore!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("righe lette: %d\n", cntlines);
    getch();

    return 0;
}

Il risultato del test con fscanf() è il seguente:

Thequickbrownfoxjumpsoverthelazydog
righe lette: 9

fscanf() legge un campo alla volta, fermandosi allo spazio o al carattere di newline; in questo modo è difficile poter contare le righe.

Molto più semplice con fgets() che legge tutto fino al newline o alla fine del file:

The quick brown fox
jumps over the lazy dog
righe lette: 2

Il conteggio delle righe è preciso e gli spazi sono presenti: un punto a favore della fgets()!

In questo caso però bisogna fare un appunto anche alla fgets(): essa non rimuove il newline alla fine della riga. Rimuovere il newline, però, non è un lavoro troppo complesso; nella prossima prova ho implementato una funzione che esegue proprio questo compito.

Ora vi suggerisco di provare anche con altri testi e soprattutto con righe vuote: vi accorgerete che fgets() è davvero meglio!

B) Lettura di una lista di interi

Supponiamo ora di dover leggere una lista di interi da file di testo, come il seguente

5
1
3
24
0
45

Queste le due implementazioni:

// con fscanf()

#define MAXSTR        100
#define MAXITEMS    10

int main(void)
{
    int i = 0;
    int vectint[MAXITEMS];
    FILE *fp;

    fp = fopen("inputB.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fscanf(fp, "%d", &(vectint[i])) == 1)
            {
                printf("%d\n", vectint[i]);
                i++;
                if (i > MAXITEMS)
                    break;
            }
            else
            {
                printf("errore!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("numeri letti: %d\n", i);
    getch();

    return 0;
}

// con fgets()

#define MAXSTR        100
#define MAXITEMS    10

int main(void)
{
    int i = 0;
    char tmpstr[MAXSTR];
    int vectint[MAXITEMS];
    FILE *fp;

    fp = fopen("inputB.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fgets(tmpstr, MAXSTR, fp) != NULL)
            {
                vectint[i] = atoi(tmpstr);
                printf("%d\n", vectint[i]);
                i++;
                if (i > MAXITEMS)
                    break;
            }
            else
            {
                printf("errore!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("numeri letti: %d\n", i);
    getch();

    return 0;
}

Entrambe danno lo stesso risultato, corretto, e pertanto consideriamo un bel pareggio: è vero che per la fgets() abbiamo dovuto appoggiarci ad una funzione aggiuntiva, la atoi(), ma davvero non è poi una gran fatica!

Avendo ottenuto un pareggio, proviamo ad andare più a fondo nel confronto: simuliamo una situazione di errore, inserendo una stringa non numerica e una riga vuota.

5
1
3
24
0
PROVA


45

Ecco cosa succede con fscanf()

5
1
3
24
0
errore!
numeri letti: 5

Si potrebbe pensare che basta togliere il break in corrispondenza del riconoscimento dell'errore, per proseguire e andare a leggere anche l'ultimo valore.
Se togliamo il break, però, con questo file di input entriamo in un loop infinito!!! 

In questo modo scopriamo che in pratica fscanf() non avanza se non trova i dati attesi; senza break si rimane lì! Un bel problema!!!
Trovate una discussione al riguardo a questo link https://stackoverflow.com/questions/17921514/infinite-loop-after-reading-a-file-using-fscanf
La morale è: controllate sempre bene il valore di ritorno della fscanf() perchè in caso di errore si entra inevitabilmente in un loop infinito.

Ecco cosa succede invece con fgets()

5
1
3
24
0
0
0
45
numeri letti: 8

fgets() non ha il problema del loop infinito; lei si occupa semplicemente di leggere una riga alla volta.

Comunque, in un modo o nell'altro, entrambe hanno sbagliato! Con fgets(), tuttavia, la situazione è facilmente risolvibile testando il contenuto della stringa mentre con fscanf() diventa veramente dura.
Questa situazione evidenzia come la fgets() permetta di scrivere codice più "robusto"; qui sotto l'aggiunta dei controlli che permettono ad fgets() di leggere ed interpretare correttamente il file in input:

#define MAXSTR        100
#define MAXITEMS    10

// elimina \n \r e spazi alla fine della stringa
void fixEndString(char *str)
{
    char *p;
    size_t len;
    len = strlen(str);
    if (len == 0)
        return;
    p = str + len - 1;
    while (1)
    {
        if (*p == '\n')
            *p = '\0';
        else if (*p == '\r')
            *p = '\0';
        else if (*p == ' ')
            *p = '\0';
        else
            break;

        if (p == str)
            break;

        p--;
    }
}

int isNumeric(char *str)
{
    char *p;
    p = str;
    while (*p)
    {
        if ((*p < '0') || (*p > '9'))
            return 0;
        p++;
    }
    return 1;
}

int main(void)
{
    int i = 0;
    char tmpstr[MAXSTR];
    int vectint[MAXITEMS];
    FILE *fp;

    fp = fopen("inputB.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fgets(tmpstr, MAXSTR, fp) != NULL)
            {
                fixEndString(tmpstr);
                if (strlen(tmpstr) == 0)
                    printf("vuoto\n");
                else if (!isNumeric(tmpstr))
                    printf("non numerico\n");
                else
                {
                    vectint[i] = atoi(tmpstr);
                    printf("%d\n", vectint[i]);
                    i++;
                    if (i > MAXITEMS)
                        break;
                }
            }
            else
            {
                printf("errore!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("numeri letti: %d\n", i);
    getch();

    return 0;
}

Con questi controlli otteniamo questo risultato:

5
1
3
24
0
non numerico
vuoto
45
numeri letti: 6

Davvero ottimo: quello che pare impossibile con fscanf() diventa un gioco da ragazzi con fgets().

Consideriamo comunque un pareggio; attenzione però a controllare il valore di ritorno della fscanf() altrimenti si rischia il loop infinito!!!

C) Lettura con formattazione

Ora passiamo a qualcosa di più complicato: proviamo ad interpretare un file di configurazione

PAR1 = 900
PAR2 = 800
PAR3 = 850

Queste le due implementazioni:

// con fscanf()

#define MAXSTR        100
#define MAXITEMS    10

int main(void)
{
    int i = 0;
    int vectint[MAXITEMS];
    char par[MAXSTR];
    FILE *fp;

    fp = fopen("inputC.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fscanf(fp, "%99s = %d", par, &(vectint[i])) == 2)
            {
                printf("parametro %s = %d\n", par, vectint[i]);
                i++;
                if (i > MAXITEMS)
                    break;
            }
            else
            {
                printf("errore!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("parametri letti: %d\n", i);
    getch();

    return 0;
}

// con fgets()

#define MAXSTR        100
#define MAXITEMS    10

int main(void)
{
    int i = 0;
    char tmpstr[MAXSTR];
    int vectint[MAXITEMS];
    char par[MAXSTR];
    FILE *fp;

    fp = fopen("inputC.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fgets(tmpstr, MAXSTR, fp) != NULL)
            {
                if (sscanf(tmpstr, "%99s = %d", par, &(vectint[i])) == 2)
                {
                    printf("parametro %s = %d\n", par, vectint[i]);
                    i++;
                    if (i > MAXITEMS)
                        break;
                }
                else
                    printf("errore di formato!\n");
            }
            else
            {
                printf("errore di lettura!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("parametri letti: %d\n", i);
    getch();

    return 0;
}

Ecco il risultato per entrambi corretto:

parametro PAR1 = 900
parametro PAR2 = 800
parametro PAR3 = 850
parametri letti: 3

Attenzione: per la fgets() ho dovuto introdurre la funzione sscanf() il cui prototipo è il seguente

int sscanf ( const char * s, const char * format, ...);

Al solito consiglio di dare un'occhiata al funzionamento http://www.cplusplus.com/reference/cstdio/sscanf/

In pratica si tratta di una funzione simile ad fscanf() che lavora sulle stringhe invece di lavorare sui file.

Dato il "costo" di inserire questa chiamata dopo fgets() si potrebbe pensare di dare il punto a favore di fscanf().

Come per la prova precedente, si impone la necessità di una prova aggiuntiva: proviamo a caricare questo contenuto:

PAR1 = 900
PAR2 = 800

 

;il parametro PAR3 bla bla bla
PAR3 = 850

con fscanf()

parametro PAR1 = 900
parametro PAR2 = 800
errore!
parametri letti: 2

con fgets()

parametro PAR1 = 900
parametro PAR2 = 800
errore di formato!
errore di formato!
parametro PAR3 = 850
parametri letti: 3

Questo evidenzia una caratteristica: con fgets() possiamo distinguere fra errori di lettura da file ed errori di formato mentre con fscanf() dobbiamo arrestarci al primo errore (per evitare l'inloopamento).

Un altro punto a favore di fgets()!

Per sottolineare la praticità di fgets() aggiungiamo la possibilità di gestire situazioni con righe di diversi formati.
Restando sempre nel nostro esempio, supponiamo di voler aggiungere anche i nomi di sezione:

[MIAAPP]
PAR1 = 900
PAR2 = 800
[MIOEXE]
;il parametro PAR3 bla bla bla
PAR3 = 850

Questa l'implementazione con fgets()+sscanf():

#define MAXSTR        100
#define MAXITEMS    10

int main(void)
{
    int i = 0;
    char tmpstr[MAXSTR];
    int vectint[MAXITEMS];
    char par[MAXSTR];
    char sez[MAXSTR];
    FILE *fp;

    fp = fopen("inputC1.txt", "r");
    if (fp)
    {
        while (!feof(fp))
        {
            if (fgets(tmpstr, MAXSTR, fp) != NULL)
            {
                if (sscanf(tmpstr, "%99s = %d", par, &(vectint[i])) == 2)
                {
                    printf("parametro %s = %d\n", par, vectint[i]);
                    i++;
                    if (i > MAXITEMS)
                        break;
                }
                else if (sscanf(tmpstr, "[%[^]]", sez) == 1)
                    printf("sezione %s\n", sez);
                else
                    printf("errore di formato!\n");
            }
            else
            {
                printf("errore di lettura!");
                break;
            }
        }
        fclose(fp);
    }
    printf("\n");
    printf("parametri letti: %d\n", i);
    getch();

    return 0;
}

Ecco il risultato:

sezione MIAAPP
parametro PAR1 = 900
parametro PAR2 = 800
errore di formato!
sezione MIOEXE
errore di formato!
parametro PAR3 = 850
parametri letti: 3

Come si può notare, fgets() è davvero pratica: è vero che bisogna scrivere un po' di codice ma il risultato che si ottiene è davvero affidabile.

Il problema principale di fscanf() è che integra in un'unica chiamata sia la lettura da disco che l'interpretazione dei dati: questo può sembrare una semplificazione ma in realtà, come abbiamo visto, rende la gestione degli errori davvero un'incubo!

And the winner is... fgets() !!!

fgets() ha vinto inequivocabilmente i test A) e C) mentre nel test B) abbiamo decretato un pareggio.

Conclusioni

Al di là del risultato, comunque sempre opinabile, possiamo dire:
- se vogliamo un codice robusto dobbiamo utilizzare fgets(); questa ci permette di distinguere gli errori di I/O dagli errori di formattazione;
- se vogliamo gestire diversi formati all'interno dello stesso file, di nuovo dobbiamo utilizzare fgets() con l'aiuto eventualmente di sscanf();
- se vogliamo scrivere poco codice usiamo fscanf() ma preoccupiamoci almeno di controllare il suo valore di ritorno (per evitare il problema dell'inloopamento).