Rilevare la Fine del File e gli Errori di I/O in Linguaggio C

Concetti Chiave
  • Quando si lavora con file in linguaggio C, è importante essere in grado di rilevare la fine del file e gestire eventuali errori di input/output (I/O).
  • Le funzioni feof(), ferror() e clearerr() sono utilizzate per gestire queste situazioni.
  • La funzione feof() verifica se è stata raggiunta la fine del file.
  • La funzione ferror() verifica se si è verificato un errore di I/O.
  • La funzione clearerr() resetta i flag di stato di un file.

Condizioni di Errore in Fase di Lettura

Nella lezione precedente abbiamo visto come leggere e scrivere dati su file in modo formattato adoperando le funzioni fprintf() e fscanf(). In fase di lettura di un file, la funzione fscanf() restituisce il numero di elementi letti con successo, quindi ci si aspetta che questo numero sia uguale al numero di elementi richiesti.

Il modo corretto per rilevare eventuali errori in fase di lettura è confrontare il valore restituito da fscanf() con il numero di elementi richiesti. Ad esempio, se stiamo leggendo due valori (un intero e un float), ci aspettiamo che fscanf() restituisca 2. Se restituisce un valore inferiore, significa che si è verificato un errore di lettura o che si è raggiunta la fine del file:

int intValue;
float floatValue;
int result = fscanf(file, "%d %f", &intValue, &floatValue);
if (result != 2) {
    // Gestione dell'errore
}

In questo esempio, se fscanf() non riesce a leggere entrambi i valori, significa che qualcosa è andato storto.

Ci sono tre possibili cause:

  1. Fine del File (EOF): Se si raggiunge la fine del file prima di leggere tutti i valori richiesti, fscanf() restituirà un valore inferiore al numero di elementi richiesti.
  2. Errore di Lettura: per motivi come problemi hardware o file corrotti, fscanf() potrebbe non essere in grado di leggere i dati correttamente.
  3. Dati Non Validi: se i dati nel file non corrispondono al formato specificato (ad esempio, se si tenta di leggere un intero ma il file contiene una stringa), fscanf() non riuscirà a convertire i dati e restituirà un valore inferiore.

Il problema è che il semplice confronto del valore restituito da fscanf() con il numero di elementi richiesti non consente di distinguere tra queste tre cause. In molti casi questo non è un problema perché se si è verificato un errore di lettura semplicemente la soluzione migliore è interrompere l'elaborazione del file se non dell'intero programma.

Altre volte, invece, può essere utile o necessario distinguere tra le varie cause di errore. Per questo scopo, il linguaggio C fornisce tre funzioni specifiche: feof(), ferror() e clearerr(). Vediamole in dettaglio.

Flag di Stato di un File e funzione clearerr

Ogni stream di file in linguaggio C mantiene uno stato interno che indica non solo la posizione corrente nel file, ma anche delle condizioni particolari. In dettaglio, ad ogni stream sono associati due flag (indicatori) di stato:

  1. Flag di Fine del File (EOF): Questo flag viene impostato quando si raggiunge la fine del file durante un'operazione di lettura.
  2. Flag di Errore: Questo flag viene impostato quando si verifica un errore durante un'operazione di input/output (I/O) sul file.

Questi due flag sono inizialmente non impostati (cioè sono impostati a false) quando si apre un file con la funzione fopen().

Il primo flag, quello di Fine del File (EOF), viene impostato automaticamente dal sistema quando si tenta di leggere oltre la fine del file. Mentre il secondo flag, quello di Errore, viene impostato quando si verifica un errore durante un'operazione di I/O, come ad esempio un errore hardware o un problema di permessi.

Inoltre, il flag di errore può essere impostato anche in fase di scrittura, ad esempio se si tenta di scrivere su un file di sola lettura.

Se, invece, un errore di dati non validi non imposta il flag di errore, ma semplicemente fa sì che le funzioni di lettura restituiscano un valore inferiore al numero di elementi richiesti.

Una volta che uno di questi flag è stato impostato, rimane tale fino a quando non viene esplicitamente resettato. Per questo motivo, è importante controllare lo stato di questi flag dopo ogni operazione di I/O per gestire correttamente eventuali errori o condizioni di fine file. Per resettare questi flag, si può utilizzare la funzione clearerr().

La sintassi della funzione clearerr() è la seguente:

void clearerr(FILE *stream);

Essa richiede come parametro un puntatore a un oggetto di tipo FILE, che rappresenta lo stream di file su cui si desidera resettare i flag di stato. La funzione non restituisce alcun valore.

Di solito, clearerr() non viene usata frequentemente nei normali programmi, anche perché molte funzioni di I/O resettano automaticamente i flag di stato.

Funzioni feof e ferror

Per verificare lo stato dei flag di Fine del File (EOF) e di Errore, il linguaggio C fornisce due funzioni specifiche: feof() e ferror().

La funzione feof() verifica se il flag di Fine del File è stato impostato per uno stream di file specifico. La sua sintassi è la seguente:

int feof(FILE *stream);

Essa richiede come parametro un puntatore a un oggetto di tipo FILE, che rappresenta lo stream di file da verificare. La funzione restituisce un valore intero: un valore diverso da zero (vero) se il flag di Fine del File è stato impostato, oppure 0 (falso) se non è stato impostato. Quindi se è stata raggiunta la fine del file, feof() restituirà un valore vero.

Allo stesso modo, la funzione ferror() verifica se il flag di Errore è stato impostato per uno stream di file specifico. La sua sintassi è la seguente:

int ferror(FILE *stream);

Anche in questo caso, essa richiede come parametro un puntatore a un oggetto di tipo FILE, che rappresenta lo stream di file da verificare. La funzione restituisce un valore intero: un valore diverso da zero (vero) se il flag di Errore è stato impostato, oppure 0 (falso) se non è stato impostato.

Quando adoperiamo la funzione fscanf o scanf per leggere dati da un file, se otteniamo come risultato un numero di elementi letti inferiore a quello richiesto, possiamo utilizzare le funzioni feof() e ferror() per determinare se la causa è stata la fine del file o un errore di lettura. In tal caso si possono verificare i seguenti casi:

  1. Se feof() restituisce vero, significa che si è raggiunta la fine del file.
  2. Se ferror() restituisce vero, significa che si è verificato un errore di lettura.
  3. Se entrambe le funzioni restituiscono falso, significa che i dati nel file non erano validi per il formato specificato.

Esempio di utilizzo di feof e ferror

Proviamo a mettere in pratica quanto detto con un esempio di funzione che prende in ingresso un nome di file di testo e restituisce la prima riga del file che inizia con un numero intero positivo valido. La funzione restituisce il numero di riga trovata, oppure restituisce:

  • -1 se il file non può essere aperto,
  • -2 se si verifica un errore di lettura,
  • -3 nessuna riga inizia con un numero intero positivo valido.

Lo schema della funzione può essere il seguente:

#include <stdio.h>

int trova_riga(const char *nome_file) {
    FILE *file = fopen(nome_file, "r");
    if (file == NULL) {
        // Errore nell'apertura del file
        return -1;
    }

    // Ciclo di lettura delle righe del file
    int numero_riga = 1;
    int valore;
    while (fscanf(file, "%d", &valore) != 1) {

        // fscanf non è riuscito a leggere un intero
        // Verifica le condizioni
        // ...

        // Scarta la riga corrente
        // ...

        numero_riga++;
    }

    fclose(file);
    return numero_riga;
}

Come si può notare, la funzione apre il file e poi entra in un ciclo di lettura delle righe del file. Se fscanf() riesce a leggere un intero, il ciclo termina e la funzione restituisce il numero di riga corrente.

Viceversa, se fscanf() non riesce a leggere un intero, dobbiamo verificare le condizioni di errore utilizzando feof() e ferror(), come mostrato di seguito:

if (ferror(file)) {
    // Errore di lettura
    fclose(file);
    return -2;
}

if (feof(file)) {
    // Fine del file raggiunta
    fclose(file);
    // Nessuna riga valida trovata
    return -3;
}

Fatto questo, dobbiamo scartare la riga corrente per poter procedere alla lettura della successiva. Per fare ciò, possiamo adoperare gli scanset per ignorare tutto fino alla fine della riga corrente:

// Scarta la riga corrente
fscanf(file, "%*[^\n]");

In questa riga di codice, l'uso di %* indica a fscanf() di leggere ma non memorizzare i caratteri che corrispondono al formato specificato. Il formato [^\n] indica di leggere tutti i caratteri che non sono il carattere di nuova linea (\n). In questo modo, fscanf() leggerà e scarterà tutti i caratteri fino alla fine della riga corrente, permettendoci di procedere alla lettura della riga successiva nel ciclo.

Il codice completo del programma diventa quindi il seguente:

#include <stdio.h>

int trova_riga(const char *nome_file) {
    FILE *file = fopen(nome_file, "r");
    if (file == NULL) {
        // Errore nell'apertura del file
        return -1;
    }

    // Ciclo di lettura delle righe del file
    int numero_riga = 1;
    int valore;
    while (fscanf(file, "%d", &valore) != 1) {

        // fscanf non è riuscito a leggere un intero
        // Verifica le condizioni
        if (ferror(file)) {
            // Errore di lettura
            fclose(file);
            return -2;
        }

        if (feof(file)) {
            // Fine del file raggiunta
            fclose(file);
            // Nessuna riga valida trovata
            return -3;
        }

        // Scarta la riga corrente
        fscanf(file, "%*[^\n]");
        // Scarta il carattere di nuova linea
        fgetc(file);

        numero_riga++;
    }

    fclose(file);
    return numero_riga;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Uso: %s nome_file\n", argv[0]);
        return 1;
    }

    int risultato = trova_riga(argv[1]);
    if (risultato > 0) {
        printf("Prima riga con intero positivo trovata alla riga: %d\n", risultato);
    } else {
        printf("Errore: %d\n", risultato);
    }

    return 0;
}

Compilando ed eseguendo questo programma con un file di testo come argomento, esso restituirà il numero della prima riga che inizia con un intero positivo valido, oppure un codice di errore appropriato in caso di problemi.

Ad esempio, supponiamo di avere un file di testo chiamato dati.txt con il seguente contenuto:

Ciao, come si va?
Tutto bene?
10 esempio
Dati di esempio

Eseguendo il programma con questo file come argomento:

$ ./trova_riga dati.txt

Il programma restituirà:

Prima riga con intero positivo trovata alla riga: 3