Posizionamento all'interno di un File in Linguaggio C

Concetti Chiave
  • La posizione corrente all'interno di un file viene gestita automaticamente durante le operazioni di lettura e scrittura sequenziali.
  • Le funzioni di posizionamento consentono di spostare la posizione corrente in modo flessibile.
  • Le principali funzioni di posizionamento sono fseek(), ftell(), rewind(), fgetpos() e fsetpos().
  • fseek() permette di spostare la posizione corrente relativamente a una posizione di riferimento specificata.
  • ftell() restituisce la posizione corrente all'interno del file.
  • rewind() riporta la posizione corrente all'inizio del file.
  • fgetpos() e fsetpos() utilizzano il tipo astratto fpos_t per gestire posizioni in file di grandi dimensioni.

Posizione all'interno di un File

Un importante concetto che riguarda la lettura e la scrittura di file in linguaggio C è quello della posizione all'interno di un file.

Ad ogni stream è associata una posizione corrente o file position. Quando si apre un file, la posizione corrente viene inizializzata all'inizio del file. Se invece il file viene aperto in modalità di append, la posizione corrente viene inizializzata alla fine del file.

A questo punto, ogni operazione di lettura o scrittura avanza automaticamente la posizione corrente di un certo numero di byte, a seconda di quanti byte sono stati letti o scritti. Facendo così possiamo accedere in maniera sequenziale ai dati all'interno del file.

Per comprendere meglio, consideriamo l'esempio di un file di testo letto, carattere per carattere, utilizzando la funzione fgetc():

#include <stdio.h>

int main() {
    FILE *file = fopen("esempio.txt", "r");
    if (file == NULL) {
        // Gestione dell'errore
        return 1;
    }

    int ch;
    while ((ch = fgetc(file)) != EOF) {
        putchar(ch); // Stampa il carattere letto
    }

    fclose(file);
    return 0;
}

In questo esempio, per prima cosa apriamo il file in lettura attraverso la funzione fopen(). La posizione corrente viene inizializzata all'inizio del file. Ogni volta che chiamiamo fgetc(), leggiamo un carattere dal file e la posizione corrente avanza di un byte. Questo processo continua fino a quando non raggiungiamo la fine del file, indicata da EOF.

Dal nostro punto di vista, possiamo immaginare la posizione corrente come un cursore che si sposta avanti man mano che leggiamo o scriviamo dati nel file. Non dobbiamo preoccuparci di indicare esplicitamente quale carattere leggere o scrivere, poiché la posizione corrente gestisce automaticamente questo aspetto per noi.

La stessa identica cosa avviene anche in fase di scrittura. Consideriamo l'esempio di un programma che scrive i primi 10 numeri interi in un file di testo, utilizzando la funzione fprintf():

#include <stdio.h>

int main() {
    FILE *file = fopen("esempio.txt", "w");
    if (file == NULL) {
        // Gestione dell'errore
        return 1;
    }

    for (int i = 1; i <= 10; i++) {
        fprintf(file, "%d\n", i);
    }

    fclose(file);
    return 0;
}

Anche in questo caso, la posizione corrente viene inizializzata all'inizio del file quando lo apriamo in modalità di scrittura. Ogni volta che chiamiamo fprintf(), scriviamo un numero intero nel file e la posizione corrente avanza di un certo numero di byte, a seconda di quanti caratteri sono stati scritti per rappresentare il numero e il carattere di nuova linea. Questo processo continua fino a quando non abbiamo scritto tutti i numeri desiderati nel file.

Analogamente alla lettura, possiamo immaginare la posizione corrente come un cursore che si sposta avanti man mano che scriviamo dati nel file, senza doverci preoccupare di specificare esplicitamente dove scrivere ogni volta.

In generale, le funzioni di Input/Output viste sinora, siano esse a caratteri, a linee, formattate o binarie, sono pensate per essere sequenziali, ovvero operano in modo da leggere o scrivere dati partendo dalla posizione corrente e avanzando automaticamente la posizione stessa.

Per molte applicazioni, l'input e output sequenziali sono più che sufficienti.

Tuttavia, per molti programmi sorge spesso l'esigenza di poter spostare la posizione corrente all'interno del file in modo più flessibile, ad esempio per rileggere una parte del file o per sovrascrivere dati in una posizione specifica.

Consideriamo un esempio in cui un file contiene dei dati relativi a un elenco di studenti, con la relativa anagrafica e i voti. Sebbene l'anagrafica rimanga invariata, i voti possono essere soggetti a modifiche. A questo punto si potrebbe agire in due modi:

  1. Si opera in maniera sequenziale:

    Ossia si legge l'intero file, si modificano i voti in memoria e si riscrive l'intero file con i nuovi voti. Questo approccio è semplice ma inefficiente, soprattutto se il file è di grandi dimensioni. Infatti, ogni volta che si modifica un voto, è necessario rileggere e riscrivere l'intero file, anche se solo una piccola parte dei dati è cambiata.

  2. Si utilizza le funzioni di posizionamento:

    In questo caso, si può aprire il file, leggere l'anagrafica degli studenti e, quando si arriva alla sezione dei voti, utilizzare le funzioni di posizionamento per spostare la posizione corrente direttamente alla parte del file che contiene i voti. In questo modo, è possibile modificare solo i voti senza dover rileggere o riscrivere l'intero file, migliorando notevolmente l'efficienza dell'operazione.

Questa seconda modalità di accesso ai file, nota come accesso diretto o random access, è resa possibile dalle funzioni di posizionamento che il linguaggio C mette a disposizione.

Le principali funzioni di posizionamento sono cinque e sono definite nella libreria standard <stdio.h>:

  • fseek()
  • ftell()
  • rewind()
  • fgetpos()
  • fsetpos()

Vediamole in dettaglio.

La funzione fseek

La funzione fseek() consente di spostare la posizione corrente all'interno di un file in modo flessibile. La sua firma è la seguente:

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

Il primo argomento è il puntatore al file (stream) su cui si vuole operare.

Di particolare importanza sono i due argomenti successivi. Infatti, la funzione non sposta la posizione corrente a un punto assoluto del file, ma piuttosto relativamente a una posizione di riferimento specificata dal terzo argomento whence.

Le possibili costanti per whence sono:

  • SEEK_SET: la posizione corrente viene impostata a offset byte dall'inizio del file.
  • SEEK_CUR: la posizione corrente viene spostata di offset byte rispetto alla posizione attuale.
  • SEEK_END: la posizione corrente viene impostata a offset byte dalla fine del file.

Queste tre costanti sono definite come macro nella libreria <stdio.h>.

Quindi offset può essere un numero positivo o negativo, a seconda di dove si vuole spostare la posizione corrente.

Ad esempio, per spostare la posizione corrente all'inizio del file, si può utilizzare:

fseek(file, 0L, SEEK_SET);

Abbiamo specificato un offset di 0 byte dall'inizio del file.

Se, invece, vogliamo spostare la posizione corrente di 10 byte avanti rispetto alla posizione attuale, possiamo utilizzare:

fseek(file, 10L, SEEK_CUR);

Infine, per spostare la posizione corrente di -5 byte dalla fine del file, possiamo utilizzare:

fseek(file, -5L, SEEK_END);

Un'importante osservazione riguarda il tipo del parametro offset, che è di tipo long. Questo significa che l'offset può essere un numero molto grande, permettendo di spostare la posizione corrente anche in file di grandi dimensioni. Infatti, negli esempi di sopra abbiamo utilizzato il suffisso L per indicare che i numeri 0, 10 e -5 sono di tipo long.

In caso di successo la funzione fseek() restituisce 0. Invece, in caso di errore, quando ad esempio la posizione specificata non è valida, la funzione restituisce un valore diverso da 0.

fseek e i file di testo

La funzione fseek, così come le altre funzioni di posizionamento, sono state progettate per funzionare bene con i file binari.

Quando le si usa con i file di testo bisogna fare attenzione, specialmente a seconda del sistema operativo su cui si esegue il programma.

Sotto i sistemi UNIX, e Linux, la funzione fseek non fa differenza alcuna tra file di testo e file binari, per cui può essere utilizzata tranquillamente su entrambe i tipi.

Cosa diversa accade, invece, con sistemi legacy (ad esempio MS-DOS) ed alcune vecchie versioni di Windows. Su tali sistemi, infatti, il carattere di nuova riga non è rappresentato da un singolo carattere, bensì da una sequenza di più caratteri. Ad esempio, su Windows storicamente l'interruzione di riga in un file di testo è rappresentato dai caratteri \r\n, ossia carriage return e line feed.

Questo dettaglio, all'apparenza di poco conto, crea tutta una serie di problemi. Per prima cosa un offset in un file di testo non rappresenterà più un numero preciso di byte, ma piuttosto un numero di caratteri, che può essere differente dal numero di byte effettivamente occupati nel file. Stessa cosa vale quando si scrive in un file.

Questo è il motivo per cui esiste la modalità di apertura dei file in modalità binaria o in modalità testo. Questi sistemi differenziano i due tipi di file proprio per gestire queste differenze. Cosa che invece viene del tutto ignorata nei sistemi UNIX e Linux.

La conseguenza è che, su tali sistemi, quando si usa fseek con i file di testo vi sono delle limitazioni:

  1. Il parametro offset deve essere sempre 0 quando si usa un valore di whence pari a SEEK_CUR o SEEK_END.
  2. Se si usa SEEK_SET, si può usare un offset diverso da 0, ma solo se tale valore è stato restituito in precedenza da una chiamata a ftell() (vedi sotto).

Ovviamente, il problema si pone se vogliamo che il nostro codice compili sia sotto Linux e Unix che sotto altri sistemi. In caso contrario, possiamo tranquillamente ignorare queste limitazioni.

Un'altra limitazione riguarda il fatto che, con i file binari, fseek non deve necessariamente garantire il supporto alla macro SEEK_END. Infatti, in alcuni casi particolari, potrebbe non essere possibile determinare la dimensione del file binario. Anche in questo caso, però, sotto i sistemi UNIX e Linux questa limitazione non si pone.

La funzione ftell

La funzione ftell() consente di ottenere la posizione corrente all'interno di un file. La sua dichiarazione è la seguente:

long ftell(FILE *stream);

In caso di successo, la funzione restituisce la posizione corrente come un valore di tipo long. In caso di errore, invece, restituisce -1L e imposta la variabile globale errno per indicare il tipo di errore (studieremo la variabile errno nelle prossime lezioni).

Bisogna prestare un attimo l'attenzione sul valore restituito dalla funzione:

  1. Se stiamo lavorando con un file binario, il valore restituito rappresenta il numero di byte dall'inizio del file fino alla posizione corrente.
  2. Se stiamo lavorando con un file di testo, il valore restituito rappresenta il numero di caratteri dall'inizio del file fino alla posizione corrente.

Importante è il secondo punto. Infatti, come visto sopra, in un file di testo il numero di caratteri potrebbe non coincidere con il numero di byte effettivamente occupati nel file, specialmente su sistemi legacy come MS-DOS e alcune versioni di Windows.

Pertanto, non sempre è una buona idea utilizzare il valore restituito da ftell() per calcolare un offset in byte all'interno di un file di testo, specialmente se si prevede che il programma possa essere eseguito su diversi sistemi operativi.

Applicazione: Calcolo della dimensione di un file

Un'importante applicazione combinata delle funzioni fseek() e ftell() è il calcolo della dimensione di un file.

Infatti, per ottenere la dimensione di un file, possiamo seguire questi passaggi:

  1. Apriamo il file di cui vogliamo calcolare la dimensione in modalità di lettura binaria.
  2. Usiamo fseek() per spostare la posizione corrente alla fine del file.
  3. Usiamo ftell() per ottenere la posizione corrente, che corrisponde alla dimensione del file in byte.
  4. Infine, chiudiamo il file.

Ecco un esempio di codice che implementa questo procedimento:

#include <stdio.h>

long calcola_dimensione_file(const char *nome_file) {
    FILE *file = fopen(nome_file, "rb");
    if (file == NULL) {
        // Gestione dell'errore
        return -1L;
    }

    // Spostiamo la posizione corrente alla fine del file
    if (fseek(file, 0L, SEEK_END) != 0) {
        // Gestione dell'errore
        fclose(file);
        return -1L;
    }

    // Otteniamo la posizione corrente, che corrisponde alla dimensione del file
    long dimensione = ftell(file);
    if (dimensione == -1L) {
        // Gestione dell'errore
        fclose(file);
        return -1L;
    }

    fclose(file);
    return dimensione;
}

In questo esempio, la funzione calcola_dimensione_file apre un file in modalità di lettura binaria, sposta la posizione corrente alla fine del file utilizzando fseek(), e poi ottiene la dimensione del file con ftell(). Infine, chiude il file e restituisce la dimensione calcolata. In caso di errore, la funzione restituisce -1L.

La funzione rewind

La funzione rewind() è una funzione di utilità che consente di riportare la posizione corrente all'inizio di un file. La sua dichiarazione è la seguente:

void rewind(FILE *stream);

La chiamata a rewind() non restituisce alcun valore ed è quasi completamente equivalente a chiamare fseek() con un offset di 0 e whence pari a SEEK_SET:

fseek(stream, 0L, SEEK_SET);

L'unica differenza è che rewind() ripulisce anche lo stato di errore del file associato allo stream, se presente.

Le funzioni fgetpos e fsetpos

Le funzioni fseek e ftell, sebbene molto utili, presentano un'importante limitazione: l'offset utilizzato in fseek è di tipo long, il che può non essere sufficiente per gestire file di grandi dimensioni su alcune piattaforme.

Per gestire file di dimensioni enormi, tali per cui un semplice long non basta, il linguaggio C fornisce due funzioni aggiuntive: fgetpos() e fsetpos().

Queste funzioni sono in grado di gestire posizioni all'interno di file enormi perché utilizzano un tipo di dato astratto chiamato fpos_t, che è definito appositamente per rappresentare posizioni nei file in modo più flessibile rispetto a un semplice long.

Il tipo fpos_t è definito nella libreria standard <stdio.h>, e la sua implementazione può variare a seconda della piattaforma e del compilatore utilizzato. Infatti, non è detto che fpos_t sia un intero; potrebbe essere una struttura complessa che contiene informazioni aggiuntive necessarie per rappresentare posizioni in file di grandi dimensioni.

Le dichiarazioni delle due funzioni sono le seguenti:

int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);

La funzione fgetpos() salva la posizione corrente del file associato allo stream stream nell'oggetto di tipo fpos_t puntato da pos. In caso di successo, la funzione restituisce 0; in caso di errore, restituisce un valore diverso da 0 e imposta la variabile globale errno per indicare il tipo di errore.

Analogamente, la funzione fsetpos() imposta la posizione corrente del file associato allo stream stream alla posizione specificata dall'oggetto di tipo fpos_t puntato da pos. Anche in questo caso, in caso di successo la funzione restituisce 0, mentre in caso di errore restituisce un valore diverso da 0 e imposta errno.

Di seguito un esempio di utilizzo delle funzioni fgetpos() e fsetpos(), in cui salviamo la posizione corrente, leggiamo alcuni dati, e poi torniamo alla posizione salvata per rileggere gli stessi dati:

fpos_t posizione;

/* ... */

// Salviamo la posizione corrente
if (fgetpos(file, &posizione) != 0) {
    // Gestione dell'errore
}

// Leggiamo alcuni dati dal file
/* ... */

// Torniamo alla posizione salvata
if (fsetpos(file, &posizione) != 0) {
    // Gestione dell'errore
}

// Rileggiamo gli stessi dati
/* ... */