[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 584.   C: utilizzo comune dei file

Nel linguaggio C, i file aperti sono flussi di file e l'apertura coincide con la predisposizione automatica di una variabile strutturata di tipo FILE, a cui, di conseguenza, si fa riferimento attraverso un puntatore (di tipo FILE *). Di solito, questo puntatore viene chiamato discorsivamente «puntatore al file», ovvero file pointer.

Quando si vuole accedere a un file, così come per poter usare le funzioni che consentono l'input e l'output elementare, è necessario includere il file stdio.h, dove, tra l'altro, è dichiarato il tipo FILE.

584.1   Apertura e chiusura

L'apertura dei file viene ottenuta normalmente con la funzione fopen() che restituisce il puntatore al file, oppure il puntatore nullo, NULL, in caso di fallimento dell'operazione. L'esempio seguente mostra l'apertura del file mio_file contenuto nella directory corrente, con una modalità di accesso in sola lettura.

#include <stdio.h>
...
int main (void)
  {
    FILE *fp_mio_file;
    ...
    fp_mio_file = fopen ("mio_file", "r");
    ...
  }

Come si vede dall'esempio, è normale assegnare il puntatore ottenuto a una variabile adatta, che da quel momento identifica il file, finché questo resta aperto.

La chiusura del file avviene in modo analogo, attraverso la funzione fclose(), che restituisce zero se l'operazione è stata conclusa con successo, oppure il valore rappresentato da EOF. L'esempio seguente ne mostra l'utilizzo.

...
    fclose (fp_mio_file);
...

La chiusura del file conclude l'attività con questo, dopo avere scritto tutti i dati eventualmente ancora rimasti in sospeso (se il file è stato aperto in scrittura).

Normalmente, un file aperto viene definito come flusso di file, o stream; così, nello stesso modo viene identificata la variabile puntatore che vi si riferisce. In effetti, lo stesso file potrebbe anche essere aperto più volte con puntatori differenti, quindi è corretto distinguere tra file fisici su disco e file aperti, o flussi.

Seguono gli schemi sintattici di fopen() e fclose(), in forma di prototipo di funzione:

FILE *fopen (char *file, char *modalità);
int fclose (FILE *flusso_di_file);

La funzione fopen() richiede come secondo argomento una stringa contenente l'informazione della modalità di accesso. Questa può essere composta utilizzando i simboli seguenti, dove la lettera b richiede espressamente un accesso binario, mentre la mancanza di tale lettera indica un accesso con le convenzioni dei file di testo:

Stringa Descrizione
r
rb
apre il file in sola lettura, posizionandosi all'inizio del file;
r+
rb+|r+b
apre il file in lettura e scrittura, posizionandosi all'inizio del file;
w
wb
apre il file in sola scrittura, creandolo se necessario, o troncandone a zero il suo contenuto se questo esiste già;
w+
wb+|w+b
apre il file in scrittura e lettura, creandolo se necessario, o troncandone a zero il suo contenuto se questo esiste già;
a
ab
apre il file in scrittura in aggiunta (append), creandolo se necessario, o aggiungendovi dati a partire dalla fine e, di conseguenza, posizionandosi alla fine dello stesso;
a+
ab+|a+b
apre il file in scrittura in aggiunta e in lettura, creandolo se necessario, o aggiungendovi dati a partire dalla fine e, di conseguenza, posizionandosi alla fine dello stesso.

La funzione fclose() restituisce zero in caso di successo, oppure il valore corrispondente alla macro-variabile EOF (annotando anche un valore appropriato nella variabile errno).

584.2   Lettura e scrittura

L'accesso al contenuto dei file avviene generalmente a livello di byte e le operazioni di lettura e scrittura dipendono da un indicatore riferito a una posizione, espressa in byte, del contenuto del file stesso. Naturalmente, tale indicatore fa parte delle informazioni che si conservano nella variabile strutturata di tipo FILE, a cui si fa riferimento per identificare il flusso di file.

A seconda di come viene aperto il file, questo indicatore viene posizionato nel modo più logico, come descritto a proposito della funzione fopen(). Questo indicatore viene spostato automaticamente a seconda delle operazioni di lettura e scrittura che si compiono, tuttavia, quando si passa da una modalità di accesso all'altra, è necessario spostare l'indicatore attraverso le istruzioni opportune, in modo da non creare ambiguità.

Per la lettura generica di un file in modo binario (nel senso di una lettura tale e quale del file) si può usare la funzione fread() che legge una quantità di byte trattandoli come un array. Per la precisione, si tratta di definire la dimensione di ogni elemento, espressa in byte, quindi la quantità di tali elementi. Il risultato della lettura viene inserito in un array, i cui elementi hanno la stessa dimensione. Si osservi l'esempio seguente:

...
    char ca[100];
    FILE *fp;
    int i;
    ...
    i = fread (ca, 1, 100, fp);
    ...

In questo modo si intende leggere 100 elementi della dimensione di un solo byte, collocandoli nell'array ca, organizzato nello stesso modo. Naturalmente, non è detto che la lettura abbia successo, o quantomeno non è detto che si riesca a leggere la quantità di elementi richiesta. Il valore restituito dalla funzione rappresenta la quantità di elementi letti effettivamente. Se si verifica un qualsiasi tipo di errore che impedisce la lettura, la funzione si limita a restituire zero.

Quando il file viene aperto in lettura, l'indicatore interno viene posizionato all'inizio del file; quindi, ogni operazione di lettura sposta in avanti il puntatore, in modo che la lettura successiva avvenga a partire dalla posizione immediatamente seguente:

...
    char ca[100];
    FILE *fp;
    int i;
    ...
    fp = fopen ("mio_file", "rb");
    ...
    while (1)        /* Ciclo senza fine */
      {
        i = fread (ca, 1, 100, fp);
        if (i == 0)
          {
            break;      /* Termina il ciclo */
          } 
        ...
      }
    ...

In questo modo, come mostra l'esempio, viene letto tutto il file a colpi di 100 byte alla volta, tranne l'ultima in cui si ottiene solo quello che resta da leggere.

Analogamente, la scrittura può essere eseguita con la funzione fwrite() che scrive una quantità di byte trattandoli come un array, nello stesso modo già visto con la funzione fread(). La scrittura procede a partire dalla posizione corrente riferita al file.

...
    char ca[100];
    FILE *fp;
    int i;
    ...
    i = fwrite (ca, 1, 100, fp);
    ...

L'esempio, come nel caso di fread(), mostra la scrittura di 100 elementi di un solo byte, prelevati da un array. Il valore restituito dalla funzione è la quantità di elementi che sono stati scritti con successo. Se si verifica un qualsiasi tipo di errore che impedisce la scrittura, la funzione si limita a restituire zero.

Anche in scrittura è importante l'indicatore della posizione interna del file. Di solito, quando si crea un file o lo si estende, l'indicatore si trova sempre alla fine. L'esempio seguente mostra lo scheletro di un programma che crea un file, copiando il contenuto di un altro (non viene utilizzato alcun tipo di controllo degli errori).

#include <stdio.h>
...
int main (void)
{
    char ca[1024];
    FILE *fp_in;
    FILE *fp_out;
    int i;
    ...
    fp_in = fopen ("file_in", "r");
    ...
    fp_out = fopen ("file_out", "w");
    ...
    while (1)                   // Ciclo senza fine.
      {
        i = fread (ca, 1, 1024, fp_in);
        if (i == 0)
          {
            break;              // Termina il ciclo.
          }
        ...
        fwrite (ca, 1, i, fp_out);
        ...
      } 
    ...
    fclose (fp_in);
    fclose (fp_out);
    ...
    return 0;
}

Seguono i modelli sintattici di fread() e fwrite(), espressi in forma di prototipi di funzione:

size_t fread (void *restrict ptr,
              size_t dimensione,
              size_t quantità,
              FILE *restrict stream);
size_t fwrite (const void *restrict ptr,
               size_t dimensione,
               size_t quantità,
               FILE *stream);

Il tipo di dati size_t serve a garantire la compatibilità con qualunque tipo intero, mentre il tipo void per l'array permette l'utilizzo di qualunque tipo per i suoi elementi, anche se negli esempi è sempre stato visto il trattamento di sole sequenze di byte.

584.3   Indicatore interno al file

Lo spostamento diretto dell'indicatore interno della posizione di un file aperto è un'operazione necessaria quando il file è stato aperto simultaneamente in lettura e in scrittura, e da un tipo di operazione si vuole passare all'altro. Per questo si utilizza la funzione fseek() ed eventualmente anche ftell() per conoscere la posizione attuale. La posizione e gli spostamenti sono espressi in byte.

La funzione fseek() esegue lo spostamento a partire dall'inizio del file, oppure dalla posizione attuale, oppure dalla posizione finale. Per questo utilizza un parametro che può avere tre valori identificati rispettivamente da tre macro-variabili: SEEK_SET, SEEK_CUR e SEEK_END. l'esempio seguente mostra lo spostamento del puntatore, riferito al flusso di file fp, in avanti di 10 byte, a partire dalla posizione attuale.

...
i = fseek (fp, 10, SEEK_CUR);
...

La funzione fseek() restituisce zero se lo spostamento avviene con successo, altrimenti si ottiene un valore negativo.

L'esempio seguente mostra lo scheletro di un programma, senza controlli sugli errori, che, dopo aver aperto un file in lettura e scrittura, lo legge a blocchi di dimensioni uguali, modifica questi blocchi e li riscrive nel file.

#include <stdio.h>

static const int dim = 100;     // Dimensione del record logico.

int main (void)
{
    char ca[dim];
    FILE *fp;
    int qta;
    int posizione_1;
    int posizione_2;

    fp = fopen ("mio_file", "r+b");     // Lettura e scrittura.

    while (1)                           // Ciclo senza fine.
      {
        //
        // Salva la posizione del puntatore interno al file
        // prima di eseguire la lettura.
        //
        posizione_1 = ftell (fp);
        qta = fread (ca, 1, dim, fp);

        if (qta == 0)
          {
            break;                      // Termina il ciclo.
          }
        //
        // Salva la posizione del puntatore interno al file
        // dopo la lettura.
        //
        posizione_2 = ftell (fp);
        //
        // Sposta il puntatore alla posizione precedente alla lettura.
        //
        fseek (fp, posizione_1, SEEK_SET);
        //
        // Esegue qualche modifica nei dati, per esempio mette un
        // punto esclamativo all'inizio.
        //
        ca[0] = '!';
        //
        // Riscrive il record modificato.
        //
        fwrite (ca, 1, qta, fp);
        //
        // Riporta il puntatore interno al file alla posizione
        // corretta per eseguire la lettura successiva
        //
        fseek (fp, posizione_2, SEEK_SET);
      } 

    fclose (fp);
    return 0;
}

Segue il modello sintattico per l'uso della funzione fseek(), espresso attraverso il suo prototipo:

int fseek (FILE *stream, long int spostamento, int punto_di_partenza);

Il valore dello spostamento, fornito come secondo parametro, rappresenta una quantità di byte che può essere anche negativa, indicando in tal caso un arretramento dal punto di partenza. Il valore restituito da fseek() è zero se l'operazione viene completata con successo, altrimenti viene restituito un valore diverso.

Segue il modello sintattico per l'uso della funzione ftell(), espresso attraverso il suo prototipo:

long int ftell (FILE *stream)

La funzione ftell() permette di conoscere la posizione dell'indicatore interno al file a cui fa riferimento il flusso di file fornito come parametro. Se si tratta di un file per il quale si esegue un accesso binario, la posizione ottenuta è assoluta, ovvero riferita all'inizio del file.

Il valore restituito in caso di successo è positivo, a indicare appunto la posizione dell'indicatore. Se si verifica un errore viene restituito un valore negativo: -1.

584.4   File di testo

I file di testo possono essere gestiti in modo più semplice attraverso due funzioni: fgets() e fputs(). Queste permettono rispettivamente di leggere e scrivere un file una riga alla volta, intendendo come riga una porzione di testo che termina con il codice di interruzione di riga, secondo l'astrazione usata dal linguaggio.

La funzione fgets() permette di leggere una riga di testo di una data dimensione massima. Si osservi l'esempio seguente:

...
fgets (ca, 100, fp);
...

In questo caso, viene letta una riga di testo di una dimensione massima di 99 caratteri, dal file rappresentato dal puntatore fp. Questa riga viene posta all'interno dell'array ca, con l'aggiunta di un carattere \0 finale. Questo fatto spiega il motivo per il quale il secondo parametro corrisponde a 100, mentre la dimensione massima della riga letta è di 99 caratteri. In pratica, l'array di destinazione è sempre una stringa, terminata correttamente.

Nello stesso modo funziona fputs(), che però richiede solo la stringa e il puntatore del file da scrivere. Dal momento che una stringa contiene già l'informazione della sua lunghezza perché possiede un carattere di conclusione, non è prevista l'indicazione della quantità di elementi da scrivere.

...
fputs (ca, fp);
...

Seguono i modelli sintattici delle funzioni fputs() e fgets(), in forma di prototipi di funzione:

char *fgets (char *stringa, int dimensione_max, FILE *stream);
int fputs (const char *stringa, FILE *stream)

Se l'operazione di lettura riesce, fgets() restituisce un puntatore corrispondente alla stessa stringa (cioè l'array di caratteri di destinazione), altrimenti restituisce il puntatore nullo, NULL, per esempio quando è già stata raggiunta la fine del file.

La funzione fputs() permette di scrivere una stringa in un file di testo. La stringa viene scritta senza il codice di terminazione finale, \0, ma anche senza aggiungere il codice di interruzione di riga. Il valore restituito è un valore positivo in caso si successo, altrimenti EOF.

In alternativa a fgets() e a fputs() si possono considerare anche le funzioni gets() e puts(), le quali però utilizzano rispettivamente lo standard input e lo standard output. Ma la funzione gets() legge tutto quello che trova fino alla fine della riga o, in mancanza di questo, fino alla fine del file, mentre puts() aggiungere automaticamente il codice di interruzione di riga alla fine della stringa che viene scritta nel file.

char *gets (char *stringa);
int puts (const char *stringa)

584.5   I/O standard

Ci sono tre flussi di file che risultano aperti in modo predefinito, all'avvio del programma:

Spesso si utilizzano questi flussi di file attraverso funzioni apposite (come nel caso di gets() e puts()) che vi fanno riferimento in modo implicito, ma si potrebbe accedere anche attraverso funzioni generalizzate, utilizzando come puntatori i nomi: stdio, stdout e stderr.

584.6   Ridirezione

È possibile associare un flusso di file già in essere, a un file differente, attraverso la funzione freopen(), oppure è possibile modificarne la modalità di accesso. Evidentemente questo tipo di operazione richiede la chiusura del flusso di file, prima di associarvi un file differente o di cambiare la modalità, cosa che comunque tenta di eseguire automaticamente la stessa funzione freopen():

FILE *freopen (const char *restrict nome_file_nuovo,
               const char *restrict modalità_di_accesso,
               FILE *restrict flusso di file);

La funzione, se riesce a eseguire il proprio compito, restituisce il puntatore allo stesso flusso di file indicato come terzo argomento, ovvero quello a cui viene applicata la ridirezione o la modifica dei permessi (o entrambe le cose). Per limitare l'effetto alla sola modifica della modalità di accesso, è sufficiente indicare il puntatore nullo al posto del nome del file. Viene mostrato un esempio che ridirige lo standard output:

#include <stdio.h>
int main (void)
{
    printf ("ciao 1\n");
    freopen ("mio", "w", stdout);
    printf ("ciao 2\n");
    freopen ("/dev/tty", "w", stdout);
    printf ("ciao 3\n");
    return 0;
}

In questo caso, dal momento che la funzione printf() scrive automaticamente attraverso lo standard output, quando il flusso di file stdout viene ridiretto nel file mio, il testo ciao 2 viene scritto in tale file. Ipotizzando di operare in un sistema Unix o in un sistema equivalente, il file di dispositivo /dev/tty dovrebbe corrispondere allo schermo del terminale utilizzato in quel momento (anche se fosse un terminale grafico); pertanto, il messaggio ciao 3 dovrebbe apparire nuovamente sullo schermo.

Logicamente, quando si riapre un file e si cambia la modalità, da binaria a testo o viceversa, può essere appropriato un riposizionamento, con l'aiuto di fseek().

584.7   Controllo degli errori

Molte funzioni, quando si verifica un errore, annotano quanto accaduto, in forma di numero intero, in una variabile globale nota con il nome errno. In generale, il nome errno è un'espressione che si traduce nell'accesso a un'area di memoria condiviso dal programma, ed eventualmente distinto in base al thread. Il significato del valore attribuito alla variabile errno è descritto da macro-variabili definite nel file errno.h, nel quale viene anche dichiarata la variabile errno, o l'espressione che la rappresenta.

La lettura della variabile errno porta alla conoscenza dell'ultimo errore che si è presentato e non è previsto il suo azzeramento automatico.

La variabile strutturata che si utilizza per fare riferimento a un flusso di file prevede anche l'annotazione di uno stato di errore. In pratica, le funzioni che accedono ai file, oltre che aggiornare la variabile globale errno, gestiscono l'indicazione di questo stato, azzerandolo quando non è più significativo. Per verificare la presenza di uno stato di errore ancora valido, a proposito di un flusso di file, si usa la funzione ferror() che restituisce un valore diverso da zero se questo stato esiste effettivamente:

int ferror (FILE *flusso_di_file);

Per interpretare l'errore annotato nella variabile errno e visualizzare direttamente un messaggio attraverso lo standard error, si può usare la funzione perror():

void perror (const char *s);

La funzione perror() mostra un messaggio in modo autonomo, aggiungendo davanti la stringa che può essere fornita come primo argomento (diversamente si può indicare il puntatore nullo o una stringa nulla, in quanto contenente solo il carattere di terminazione).

L'esempio seguente mostra un programma completo e molto semplice, in cui si crea un errore, tentando di scrivere un messaggio attraverso lo standard input, cosa che produce un errore. Se effettivamente si rileva un errore associato a quel flusso di file, attraverso la funzione ferror(), allora si passa alla sua interpretazione con la funzione perror():

#include <stdio.h>
#include <errno.h>
int main (void)
{
    fprintf (stdin, "Ciao amore!\n");
    if (ferror (stdin))
      {
        perror ("Attenzione");
      }
    return 0;
}

Come si vede, è necessario includere anche il file errno.h, senza il quale la variabile errno non risulterebbe accessibile. Avviando questo programma in un sistema GNU/Linux si potrebbe ottenere il messaggio seguente:

Attenzione: Bad file descriptor

In alternativa alla funzione perror() si può usare anche strerror() (dal file string.h), con la quale si ottiene la stringa contenente il messaggio di errore:

char *strerror (int n_errore);

Si può modificare leggermente l'esempio già apparso, in modo da usare la funzione strerror() per produrre lo stesso risultato:

#include <stdio.h>
#include <errno.h>
#include <string.h>
int main (void)
{
    char *cp;
    fprintf (stdin, "Ciao amore!\n");
    if (ferror (stdin))
      {
        cp = strerror (errno);
        fprintf (stderr, "Attenzione: %s\n", cp);
      }
    return 0;
}

Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome c_utilizzo_comune_dei_file.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory