File di testo

Ogni riga di un file di testo è un record di lunghezza variabile.

Ogni byte del file rappresenta il codice ASCII di un carattere.

Una riga è terminata con un vai a capo che è costituito dalla coppia di caratteri ASCII CR (Carriage Return = Ritorno Carrello) e LF (Line Feed = Avanzamento di una linea). L'origine del nome attribuito a questi due caratteri risale all'uso della telescrivente. Il carrello si riferisce alla testina di stampa della telescrivente. La coppia di caratteri aveva lo scopo di riportare il carrello all'inizio della riga di stampa (Carriage Return) e quindi di far avanzare il foglio alla riga successiva (Line Feed), al fine di stampare una nuova riga.

In C un File è una sequenza di byte, quindi durante le operazioni di lettura o di scrittura si deve immaginare che le informazioni vengano acquisite o scritte su un nastro.

La posizione di Lettura/scrittura dell'ipotetica Testina è rappresentata da un puntatore a file. Ad esempio, se si scrive la riga "Testo", verrà registrato un carattere per ogni byte e alla fine verrà accodata la coppia di caratteri CR + LF:

TestoCRLF(vuota)

Dopo la scrittura il puntatore al file si trova posizionato sul successivo byte del file, da cui si può iniziare a scrivere una nuova riga.

I caratteri CR e LF, che hanno i codici ASCII 13 e 10 rispettivamente, non possono mai comparire all'interno del file. Anche il codice 26 ha un significato speciale, esso indica la fine del file.


Accesso al file

Il gestore di archiviazione del sistema operativo riceve tutte le richieste di accesso al file provenienti dai programmi utente. Il gestore impiega una tabella dei descrittori di file aperti dai processi utente in cui memorizza le informazioni utili per individuare la posizione su disco dei dati di un file.

Inoltre, un file su disco occupa Blocchi di settori (cluster), non necessariamente adiacenti. Il sistema operativo riesce a leggere o scrivere solo un blocco intero, non è in grado di accedere ai byte all'interno di un blocco su disco.

Per questo motivo il gestore del sistema di archiviazione effettua le operazioni di lettura e di scrittura in un'area di transito che si trova in memoria centrale, ed è detta buffer del disco. In fase di scrittura, i caratteri del file vengono registrati nel buffer e solo quando questo si riempie avviene la vera registrazione in un blocco del disco.

Analogamente, in fase di lettura, nel buffer viene trasferito un blocco intero e da qui il programma utente ne legge i singoli caratteri. Quando si giunge alla fine del buffer, si procede con la lettura del blocco successivo.

Altre informazioni contenute nel descrittore sono: il nome fisico del file, la modalità di accesso (in lettura, in scrittura, ecc...)


Puntatore al file

Per creare il puntatore ad un file in C, e per riservare l'area per il buffer di transito, si usa la dichiarazione:

 

FILE *pf;

 

dove FILE è una struttura predefinita nella libreria stdio.h. La variabile pf è una locazione di memoria grande quanto basta per mantenere un indirizzo di memoria. La dichiarazione riserva solo lo spazio per pf, per assegnare a pf l'indirizzo del descrittore bisogna prima crearlo con l'istruzione:

 

pf = fopen("testo.txt", "w");

 

Il primo parametro della funzione fopen è il nome del file, il secondo parametro è la modalità di accesso in scrittura.

Le modalità di accesso che si possono specificare sono:

Con le modalità w, a, r+, w+ e a+ il file può essere modificato.

Dopo l'apertura bisogna verificare se l'operazione è riuscita. Infatti, se non è stato possibile aprire il file la segnalazione di errore non è automatica, ma è compito del programmatore gestire la situazione anomala. Altrimenti il programma, continuando, non accede al file ma l'utente non se ne accorge. Quindi dopo l'apertura si deve verificare il valore del puntatore:

 

if (pf==NULL) {
  printf("Errore");
  return;
}

 

Per scrivere nel file si usa la funzione fprintf:

 

fprintf(pf, "%s", s);

 

in cui:

Per leggere da un file si usa la funzione fscanf:

 

fscanf(pf, "%s", s);

 

in cui il significato dei parametri è lo stesso di fprintf.

Quando si è terminato di usare il file bisogna chiuderlo. L'operazione di chiusura corrisponde al rilascio delle aree riservate per il buffer e a quella per il descrittore.

 

fclose(pf);

 

Le due funzioni: fgets() e fputs() 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 fine riga.

La funzione fgets() permette di leggere una riga di testo di una data dimensione massima. Ad esempio:

 

fgets (s, 100, fp);

 

legge una riga di testo di dimensione massima 99 caratteri, dal file rappresentato dal puntatore fp. Questa riga viene trasferita all'interno dell'array s, con l'aggiunta del carattere \0 finale (il 100-mo carattere) di fine stringa.

La funzione fputs() richiede solo la stringa e il puntatore del file da scrivere. Il carattere di fine stringa è già contenuto nella stringa.

 

fputs (s, fp);

 

Notare che tutte le funzioni di accesso al file richiedono che si specifichi l'indirizzo della locazione di memoria coinvolta nel trasferimento sul file.

Negli esempi precedenti si è usato il nome della stringa che è il puntatore alla stringa, ma nel caso di una variabile semplice bisogna specificare l'indirizzo della variabile, tramite l'operatore &.


Operazioni sui file di testo.

Per usare un file di testo si deve dichiarare il puntatore ad una struttura FILE predefinita nella libreria stdio.h:

 

FILE *pf;

 

poi bisogna assegnare il valore al puntatore, aprendo il file:

 

pf = fopen("input.txt", modo);

 

dove il primo parametro è il nome del file, mentre il secondo parametro (modo) può essere uno dei seguenti valori:

"r"apre un file in lettura. Il file deve esistere.
"w"apre in scrittura un file, creandolo se non esiste, ne cancella il contenuto se invece esiste. Il contenuto del file non si può leggere: bisogna chiudere il file e riaprirlo in lettura.
"r+"apre un file esistente sia per la lettura che per la scrittura,
"w+"apre un file in scrittura e lettura, creandolo se non esiste.
"a"Le operazioni di scrittura avvengono alla fine del file. Il file viene creato se non esiste.
"a+"Il file viene aperto per la lettura, ma il suo contenuto viene protetto dalle operazioni di scrittura, perchè avvengono alla fine del file. Il puntatore al file può essere riposizionato, ad esempio con fseek, solo per leggere, le operazioni di scrittura avverranno sempre alla fine del file. Il file viene creato se non esiste.

Dopo aver aperto il file è opportuno assicurarsi che l'operazione sia riuscita:

 

if (pf==NULL) {
  printf("errore apertura");
  return;
}

 

Se pf è il puntatore al file,

Lettura:

Per controllare la presenza di dati da leggere in un file ad accesso sequenziale:

In tutti i casi, si può controllare la presenza di dati da leggere con la funzione feof, che ritorna true se non ci sono più dati da leggere.:

if (feof(f)) ...

oppure

while (!feof(f)) ...

Ad esempio, per leggere un qualsiasi file di testo, carattere per carattere e mostrarne il contenuto sullo schermo:

FILE *f;

f = fopen("nomefile.txt", "r");

while (!feof(f)) {

    putchar(fgetc(f));

}

fclose(f);


Esempio: Contare le parole in un testo.

Si suppone che le parole siano delimitate da spazi o da segni di punteggiatura.

Nella stessa cartella in cui si salverà il progetto dev-cpp, creare un file di testo denominato "ContaParole.txt".

Nel documento di testo scrivere un breve brano, Ad esempio:
Nel mezzo del cammin di nostra vita mi ritrovai per una selva oscura che la dritta via era smarrita.
Salvare il file.

Creare un nuovo progetto dev-cpp.
Includere il file header:

#include <string.h>

Nel main, invece, dichiarare le seguenti variabili:

Commenti:

  1. pf è il puntatore al file

  2. file è un array di caratteri inizializzato con il valore ContaParole.txt

  3. Punti è una stringa contenente tutti i caratteri di punteggiatura usati per separare le parole.

  4. n è un intero che conta le parole, ch è il carattere letto dal file.

Apertura del file di testo:

Commenti:

  1. si apre il file in lettura e si restituisce il puntatore al file.

  2. se si verifica un errore la funzione fopen restituisce NULL

  3. si segnala un generico errore di apertura (generalmente perchè il file non esiste oppure non si trova nella cartella corrente, o il nome è errato)

  4. si esce dal programma con un codice 0, per indicare, ad esempio, l'errore. L'errore viene ricevuto dal sistema operativo che, non avendo istruzioni su come interpretarlo, lo scarta.

Il ciclo di lettura dal file:

Commenti:

  1. Si inizia a contare: 0 parole

  2. se ci sono caratteri da leggere dal file

  3. si legge un carattere dal file, trasferendolo nella variabile ch

  4. la funzione strchr(Stringa, carattere) cerca il carattere nella stringa e restituisce la posizione del carattere, se ce n'è più di uno, restituisce la posizione del primo

  5.  
  6. se si tratta di uno dei caratteri di punteggiatura, si ammette che sia terminata una parola e ne inizi un'altra, quindi si conta una parola.

  7.  
  8.  
  9. Si chiude il file

  10. si stampa il numero di parole contate.

Modificando il testo, o prendendo un brano diverso, per collaudare la correttezza del procedimento, si intuisce che, se vengono usati due caratteri di separazione consecutivi, ad esempio una virgola ed uno spazio,il programma conta una parola che non c'è.

Quindi per correggere questo difetto, nella linea 14 si inserisca il seguente ciclo:

while
  (!feof(pf)
    && strchr(Punti,ch)
    !=NULL)
       ch=fgetc(pf);

Cioè si legge dal file fintantochè ci sono caratteri di punteggiatura consecutivi.


File binari.

In un file di testo alcuni caratteri sono utilizzati per marcare la fine di una riga (record), la fine del file, ecc.

Quando questa interpretazione di alcuni caratteri non può essere usata, ad esempio perchè si memorizzano numeri, allora il file deve essere usato in modalità binaria.

Uno stesso file può essere aperto come file di testo o come file binario, l'uso diverso lo deve gestire il programmatore.

FILE *pfo;

int n=1234;

pfo = fopen("Numeri", "wb");

fwrite(&n, sizeof(n), 1, pfo);

fclose(pfo);

Questo semplice programma apre un file per scrittura in modalità binaria ("wb"), poi usa la funzione

fwrite(&, sizeof(n), 1, pfo);

per scrivere su file. La funzione fwrite richiede 4 parametri:

La funzione fwrite può scrivere i valori degli elementi di un array:

fwrite(V, sizeof(V[0]), N, pf)

dove V è il puntatore all'array, sizeof(V[0]) è la dimensione in byte del primo elemento, N è il numero di elementi da registrare e pf è il puntatore al file.

Un file scritto in modalità binaria non può essere letto con blocco note, perchè i valori numerici sono codificati in binario.

Per leggere da file bisogna aprire il file in modalità binaria, e usare l'istruzione fread.

FILE *pfi;

int n;

pfi = fopen("Numeri", "rb");

fread(&n, sizeof(n), 1, pfi);

fclose(pfi);

Nota:

Per aprire un file di testo in modalità binaria si deve inserire anche il carattere "b" nella stringa modo (il secondo parametro della funzione fopen).

Questo carattere "b" può essere specificato alla fine della stringa ("rb", "wb", "ab", "r+b", "w+b", "a+b") o inserito tra la lettera e il segno "+" ("rb+", "wb+", "ab+").

Esempio: Rubrica telefonica

Premessa
L'esempio seguente viene proposto allo scopo di apprendere le operazioni di accesso ai file. In esso sono presenti alcuni difetti che verranno corretti dopo averne compreso la causa.

Nel programma che si propone si vogliono compiere poche semplici operazioni tipicamente svolte sui file: Inserimento, Lettura, Modifica, Eliminazione record.

Per la scelta dell'operazione si introduce un carattere da tastiera, il programma lo legge e decide quale operazione si richiede. Quindi includere la libreria conio.h per poter usare la funzione getch().

Si dichiari poi il puntatore al file e una stringa contenente il nome del file.

Si supponga di voler registrare nel file un elenco di nomi e numeri di telefono. Definire il formato di un record contenente 2 campi e dichiarare una variabile: Amico

Si definisca una funzione per creare un file il cui nome è specificato nella stringa nomeFile.
Dopo la creazione il file viene chiuso.

La funzione apri() apre il file in lettura/scrittura. Se il file non esiste viene restituito il valore logico false per avvertire il programma chiamante dell'errore.

La funzione Inserisci() riceve l'indirizzo della variabile che contiene i dati e li scrive nel file (dalla posizione corrente)

Nel main si dichiarano due variabili locali e si assegna il valore NULL al puntatore al file. Poi inizia un ciclo di lettura e di esecuzione dei comandi forniti al programma.

Dopo aver mostrato l'elenco dei comandi, il programma si ferma in attesa che venga premuto un tasto.

Dopo aver scelto il comando, tramite la struttura a diramazione switch si eseguono le azioni corrispondenti.

Si crea, si apre il file ...

Per aggiungere un record si acquisiscono le due stringhe da tastiera (gets) e le si memorizzano nel record allo scopo di trasferirle nel file..

La lettura di tutta la rubrica riposiziona il puntatore all'inizio del file (rewind), poi avanza di un record alla volta: lo legge e lo stampa.

Infine prima di terminare il programma si chiude il file, se era aperto.


Collaudo

Lanciando il programma in esecuzione, dopo aver creato il file si può inserire qualche valore.

Si inserisca uno o più record. Quando si chiede la lettura di tutta la rubrica si nota che l'ultimo record viene stampato due volte.
L'errore è dovuto al fatto che la segnalazione di end of file viene fornita dopo aver tentato di leggere oltre la fine del file.

Si modifichi il ciclo di lettura:

Si provi adesso a chiudere il programma e a riavviarlo. Prima di leggere tutta la rubrica si scelga l'opzione: "Aggiungi nuovo contatto".

Il nuovo record viene sovrascritto sul primo. Infatti l'operazione di scrittura avviene a partire dalla posizione corrente. Per correggere questo difetto inserire l'istruzione:

fseek(pf, 0, SEEK_END);

prima della chiamata della funzione Inserisci.

Inserire altre funzionalità. Ad esempio: la ricerca di un record, la modifica e la cancellazione di un record.

Ricerca.
Inserire l'opzione 5: Cerca un record.

Nella sezione switch inserire l'operazione di ricerca:

Inserire la definizione della funzione cerca:

Affinchè la funzione cerca possa confrontare la stringa cercata con quella letta occorre includere il file header:

#include <string.h>