Lezione 10 – Gestione dei File



By admin on febbraio 8, 2010


Nelle precedenti lezioni, tutti gli input o output avvenivano tramite la tastiera e lo schermo. In questa lezione impareremo ad effettuare letture e scritture su file presenti sulla memoria di massa (hard disk). Senza addentrarci troppo su come è fatto un calcolatore, i diversi tipi di dispositivi di memorizzazione e velocità di accesso, descriviamo subito quali sono le operazioni di base che bisogna fare per poter utilizzare il file.
La gestione di un file, di qualunque tipo esso sia, è fatta in tre passi:

  1. Apertura: Questa operazione crea una associazione tra il file su disco, individuato tramite il suo percorso e nome, ed una variabile locale al programma chiamata in gergo file descriptor o Handle. In questa operazione viene anche fatta la scelta della modalità di apertura: lettura, scrittura, append (scrittura che inizia alla fine del file), binario o ASCII.
  2. Lettura/Scrittura: Queste operazioni permettono l'effettiva scrittura e lettura dei dati su e da file. Il contenuto da scrivere/leggere ovviamente è memorizzato in una variabile locale al programma. In base al tipo di apertura del file (binario o ASCII) sarà possibile leggere/scrivere dati direttamente in formato testo o binario.
  3. Chiusura: Quando non è più necessario l'utilizzo del file è consigliabile chiudere il file. Al contrario dell'apertura, questa operazione distrugge il collegamento che si era creato prima. Questa operazione non è obbligatoria ma siccome il numero di file che un programma può mantenere aperti contemporaneamente è limitato, per evitare la generazione di un errore di apertura file è consigliabile chiudere un file dopo il suo utilizzo.

 

Handle – Apertura e chiusura di un file
Per capire cos'è un handle osserviamo il disegno sottostante:

Dal disegno si evince che un handle non è altro che un puntatore ad una struttura dati interna al sistema operativo, alla quale ovviamente il programma ha accesso, contenente delle informazioni sul file aperto: dimensione, attributi, posizione attuale nel file per le operazioni di lettura, data di creazione ed ultima modifica ed infine un puntatore alla posizione fisica del file nell'HDD che un programmatore non userà mai.
Questa struttura potrebbe essere nascosta al programmatore è visibile solo parzialmente tramite una struttura intermedia fornita dal linguaggio di programmazione. Ad esempio in Visual Basic l'handle di un file è un numero intero a cominciare da zero.
Il modo di aprire e chiudere un file in C e C++ è diverso, esaminiamoli separatamente a partire dal linguaggio C (come al solito in un programma C++ è possibile utilizzare le chiamate fatte in C).
L'esempio che segue include anche operazioni lettura e scrittura che vedremo in seguito, questo esempio apre 2 file, uno in lettura e l'altro in scrittura. Il secondo file viene riempito con lo stesso contenuto del primo ed infine chiusi. Notare la gestione dell'errore di apertura file.

#include <stdio.h>

int main(void)
{
    FILE *in, *out;
    
    if((in = fopen("TESTFILE.DAT", "rt")) == NULL)
    {
        fprintf(stderr, "Cannot open input file.\n");
        return 1;
    }
    
    if((out = fopen("TESTFILE.BAK", "wt")) == NULL)
    {
        fprintf(stderr, "Cannot open output file.\n");
        return 1;
    }
    
    while(!feof(in))
    fputc(fgetc(in), out);
    fclose(in); 
    fclose(out);
    return 0;
}

La prima riga di codice all'interno del main dichiara 2 variabili puntatore alla struttura FILE. Viene subito aperto il file di input tramite l'operazione in = fopen("TESTFILE.DAT", "rt"), nella quale viene specificato il nome del file e la modalità lettura (r) testo (t). Se l'operazione fallisce la variabile 'in' conterrà NULL. La chiusura dei file è fatta nelle 2 istruzioni precedenti il return: fclose(in); e fclose(out);.
Una funzione interessante è la feof() prensente nella condizione di test del ciclo while. Questa funzione controlla che il puntatore al file, nella struttura descritta all'inizio del paragrafo, non sia arrivato alla fine del file stesso. In altre parole questa funzione ci consente di determinare quando il file termina.
La lettura di un carattere dal file di input si esegue con la funzione fgetc(in) che ritorna il carattere letto, mentre la scrittura di un carattere viene fatta con fputc() la quale ha come parametri di input il carattere da scrivere e l'handle del file di output.

Passiamo al C++. Un semplice esempio di gestione file è il seguente:

#include <fstream.h>

main()
{
    int loop = 0;
    int x;

    char filename[12] = "a:xtest.dat";
    int mode = (ios::in | ios::binary);
    fstream fin( filename, mode );   
    if (!fin) cerr << "Unable to open file";
    while (fin >> x)
    {
        cout << x << endl;
        loop++;
    }
    fin.close();
    return(0);
}

Ricordiamo che il C++ è un linguaggio ad oggetti, quindi quale migliore esempio di utilizzo degli oggetti quello di incapsulare in una classe tutta la gestione dei file! Infatti la classe che permette di gestire i file è chiamata fstream e nell'esempio l'oggetto di questo tipo e fin.
L'inizializzazione avviene passando al costruttore dell'oggetto i parametri filename e mode. In questo esempio il file è aperto in lettura (ios::in) binaria (ios::binary). La chiusura del file è fatta tramite l'ultima istruzione: fin.close(), mentre la lettura con l'istruzione fin >> x. Se quest'ultima istruzione non legge alcun intero dal file ha come risultato zero e quindi si esce dal ciclo while.

 

File di testo e file binari
La distinzione tra un file di testo o binario è utile solo al programmatore, per il calcolatore non esiste alcuna differenza. Un file di testo è un file che contiene solo ed esclusivamente lettere, segni di punteggiatura e numeri. Un file di testo se aperto con un qualunque editor di testo sarà possibile leggere chiaramente il suo contenuto. Attenzione: Se nel nostro programma abbiamo una variabile di tipo intero e dobbiamo scriverla in un file di testo, è necessario convertire il valore intero nella sua rappresentazione stringa. Lo stesso discorso vale per i valori in virgola mobile. Se invece il file è binario, ovvero un file contenente un serie di byte qualunque, l'operazione di conversione non è necessaria.
In C le operazioni utilizzate sono:

fprintf
Il prototipo è

int fprintf(FILE *stream, const char *format[, argument, ...]);.

 ed un altro esempio di utilizzo è: fprintf(stream, "%d %c %f", i, c, f);
Dopo il segno "%" è possibile mettere una stringa composta nel seguente modo:

% [flags] [width] [.prec] [F|N|h|l|L] type_char

Dove:
[flags] : Segno dei valori numerici e può assumere il carattere '-', '+' o la stringa "blank".

[width]: Numero minimo di caratteri da utilizzare si può scrivere "%0n" ad indicare che se il numero di caratteri è minore di n allora la saranno stampati degli zeri.

[.prec] : Precisione, ovvero il numero di caratteri da stampare dopo la virgola ad es. "%.3f".

[F|N|h|l|L] : Modificatore del tipo di caratteri che segue.

type_char : Vedi figura seguente


fscanf
Il prototipo è

int fscanf(FILE *stream, const char *format[, address, ...]);

I parametri utilizzati sono gli stessi della fprintf. L'unica differenza alla quale bisogna fare attenzione è la modalità di passaggio delle variabili che conterranno i valori letti. Questa regola vale non solo per la fscanf ma anche per la scanf.

scanf("%i", &val);

nella quale è evidente la & davanti al nome della variabile val. Questa notazione indica che bisogna passare alla funzione l'indirizzo della variabile.

Per i file binari in C è possibile utilizzare la funzione open il cui prototipo è:

int open(const char *path, int access [, unsigned mode]);

nella quale la variabile access permette di impostare oltre che la modalità testo anche quella binaria. Vediamo un esempio applicativo:

#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>

int main(void)
{
    int handle;
    char msg[] = "Hello world";
    if ((handle = open("TEST.$$$", O_CREAT | O_TEXT)) == -1)
    {
        perror("Error:");
        return 1;
    }
    write(handle, msg, strlen(msg));
    close(handle);
    return 0;
}

I possibili valori che il parametro access può assumere sono divisi in 2 gruppi:

Read/Write Flags

O_RDONLY     Apertura in sola lettura.
O_WRONLY     Apertura in sola scrittura.
O_RDWR         Apertura in lettura e scrittura.

Other Access Flags

O_NDELAY       Non è usato; per compatibilità con UNIX.
O_APPEND       Se impostato, il puntatore al file punta alla fine del file.
O_CREAT         Se il file esiste, questo flag non ha effetto, se invece non esiste il file viene creato.
O_TRUNC         Se il file esiste, la sua lunghezza viene impostata a zero.
O_EXCL           Usato solo con O_CREAT. Se il file esiste, viene generato un errore.
O_BINARY       Apre il file in modalità binaria.
O_TEXT           Apre il file in modalità testo.

Le operazioni di lettura e scrittura avvengono tramite le funzioni read e write.

int read(int handle, void *buf, unsigned len);

int write(int handle, void *buf, unsigned len);

In C++ invece è possibile utilizzare un unico oggetto che gestisce sia la lettura/scrittura binaria che quella testuale. Per la lettura/scrittura binaria è sufficiente specificare la costante ios::binary vista nell'esempio sul C++. Nel caso in cui sia necessaria la lettura/scrittura testuale basta omettere tale costante. Le operazioni di lettura e scrittura avvengono gli usuali operatori >> e <<.





Comments are closed.