Gestione Completa file di testo in C - Per le classi quarte

Fino ad oggi tutti gli esempi di codice che abbiamo scritto hanno richiesto l’input manuale dell’utente oppure i valori necessari per il calcolo sono stati inseriti direttamente all’interno del codice e, cosa più grave, tutti i risultati ottenuti venivano persi immediatamente alla terminazione del programma.
Questo approccio può andar bene per un programma d’esempio ma è chiaro che un programma vero ha quasi sempre la necessità di leggere o scrivere su un file.
La gestione dei file è responsabilità del sistema operativo quindi, analogamente a quello che avviene con le funzioni *alloc, il nostro programma chiederà al sistema operativo di poter scrivere su un determinato file oppure di leggerne il contenuto e questi (il sistema operativo) risponderà positivamente o negativamente a seconda di una serie di informazioni quali ad esempio: i permessi del file, i permessi della partizione etc.
Una piccola digressione sistemistica
Se avete un pò di conoscenza del mondo linux saprete sicuramente che questo sistema operativo permette di gestire i permessi sul file system in modo veramente granulare e sicuro. Cosa succederebbe se un programma, scritto in qualsivoglia linguaggio, riuscisse a bypassare il controllo del sistema operativo per accedere direttamente ai dati grezzi scritti sull’hard disk? Succederebbe che tutta questa architettura di permessi fallirebbe miseramente e risulterebbe quindi inutile.
Ma allora cosa avviene in realtà?
Quando un programma viene avviato eredita i permessi dell’utente che lo ha lanciato e quando ha bisogno di scrivere su un file chiede al sistema operativo qualcosa come “sono l’utente X, posso scrivere su questo file?”, il sistema operativo verifica se l’utente X ha i permessi di scrittura su quella cartella e approva o meno la scrittura.
Per questo motivo è necessario che alcuni programmi vengano lanciati come root oppure tramite il comando “sudo”, perché altrimenti non avrebbero i permessi di scrivere sui file necessari. Un esempio per tutti: passwd.

File Handle

A ben guardare nei nostri esempi abbiamo già utilizzato dei file anche se non esplicitamente. Sto parlando dei due file denominati standard input (stdin) e standard output (stdout). Le funzioni che abbiamo incontrato come printf o scanfsono delle versioni semplificate delle funzioni che vedremo oggi e hanno come limitazione quella di funzionare esclusivamente su questi due file. Ma come si fa a lavorare quindi su qualsiasi altro file? La funzione principale si chiama fopen e il suo prototipo è questo: (link)
Spieghiamola in dettaglio:
Vediamo innanzitutto che la funzione restituisce un puntatore ad un tipo di dato particolare, il tipo FILE. Un puntatore a questo tipo di variabile viene chiamato “file handle” o anche “file descriptor” e viene utilizzato in tutte le funzioni che devono lavorare su questo file.
Il successivo parametro è un puntatore a char (ignoriamo “const” e “restrict” al momento) e non dovreste avere difficoltà, se avete seguito la lezione precedente, a capire che si tratta quindi di una “stringa” che indica il nome del file. Attenzione a questo parametro, se specifichiamo solo il nome del file il programma lo cercherà nella sua working directory (la directory dalla quale il programma è stato lanciato) altrimenti bisognerà specificare tutto il percorso.
Il terzo parametro è anch’esso un puntatore a char e specifica il tipo di accesso al file richiesto, se in lettura/scrittura etc. I valori più usati sono:
  • “r” Apre il file in lettura. Se il file non esiste viene restituito NULL
  • “r+” Apre il file in lettura e scrittura. Se il file non esiste viene restituito NULL
  • “w” Apre il file in scrittura cancellandone il contenuto precedente. Se il file non esiste lo crea.
  • “w+” Apre il file in lettura e scrittura cancellandone il contenuto precedente. se il file non esiste lo crea.
  • “a” Apre il file in scrittura. I dati scritti sul file vengono accodati a quelli già esistenti. Se il file non esiste viene creato
  • “a+” Apre il file in lettura e escrittura. I dati scritti sul file vengono accodati a quelli già esistenti. Se il file non esiste viene creato
Una porzione di codice per accedere in lettura e scrittura sul file “prova.txt” presente nella working directory del programma potrebbe essere:
È importante ricordarsi di chiudere i file che si sono aperti, per indicare al sistema operativo che abbiamo terminato di utilizzare quel file, e lo si fa tramite la funzione fclose il cui prototipo è:
Questa funzione chiude il file il cui handle le viene passato come parametro e restituisce 0 se la chiusura è andata a buon fine, viceversa restituisce EOF (End Of File) ed in quel caso bisognerebbe indagare meglio sull’accaduto.
Ho usato il condizionale perché non vorrei fare la figura di chi predica bene e razzola male… fclose fa parte di quelle funzioni per le quali andrebbe sempre analizzato il valore di ritorno ma che, vista la scarsa probabilità di fallimento, spesso dimentico di verificare. Sapete che anche printf ha un valore di ritorno che andrebbe valutato per ogni chiamata? Potremmo chiamare questo approccio “quite optimistic programming” :)

L’indicatore di posizione

Nella gestione dei file in C è importante tenenere a mente il concetto di indicatore di posizione. Possiamo pensare all’indicatore di posizione come al cursore nei programmi di editing di testo: un segnaposto che indica dove verrà aggiunto il testo in fase di scrittura.
Se il file viene aperto con “r”, “r+”, “w”, “w+” allora l’indicatore di posizione viene posto all’inizio del file, se invece viene aperto con “a” viene posto alla fine.
Quando si effettua una lettura o scrittura su un file l’indicatore di posizione si sposta conseguentemente, quindi se abbiamo aperto il file con “r”, poi abbiamo scritto sul file una stringa di 128 caratteri avremo l’indicatore in posizione 128.
Per gestire l’indicatore di posizione abbiamo inoltre tre funzioni specifiche, che sono:
  • long ftell(FILE *stream);
  • void rewind(FILE *stream);
  • int fseek(FILE *stream, long offset, int whence);
ftell: restituisce un long che indica la posizione corrente dell’indicatore di posizion.
rewind: (complimenti per il nome) riporta l’indicatore di posizione in posizione 0
fseek: sposta l’indicatore di posizione di un valore pari a offset (anche negativo) a partire dalla posizione whence. Per quest’ultimo parametro esistono tre macro che sono SEEK_SET, SEEK_CUR, e SEEK_END e indicano rispettivamente l’inizio del file, la posizione corrente dell’indicatore di posizione e la fine del file.
Vediamone l’utilizzo in un programma completo:

Funzioni per la lettura

Esistono diverse funzioni per la lettura da file, alcune più a basso livello di altre, ma tutte iniziano a leggere dalla posizione attuale dell’indicatore di posizione.
fread()
La funzione più a basso livello è la fread(), il suo utilizzo può non essere immediato per qualcuno ed è una funzione che rientra nella categoria delle funzioni “i know what i’m doing” :) il suo prototipo è :
e forse non c’è modo più chiaro di spiegarla di come fa la sua pagina di man.
The function fread reads nmemb elements of data, each size bytes long, from the stream pointed to by stream, storing them at the location given by ptr.
Per i non anglofoni: La funzione fread legge nmemb elementi, ciascuno di dimensione size bytes, dallo stream puntato da stream e memorizza il tutto nella variabile puntata dal puntatore ptr.
Chissà perché quando vedo questi prototipi così grezzi mi viene in mente il demenziale tormentone “Non c’è problema, tu mi dici quello che devo fare ed io lo faccio”. (e con questa mi sono giocato la reputazione!)
Facciamo subito un esempio per provare la funzione fread(), supponiamo avere un file di testo così formato
Se non volete impazzire per crearlo ecco lo script bash che ho usato io:
Quello che vogliamo fare è scrivere un programma che legga l’intero file inserendo una riga alla volta in un array di caratteri. Poiché conosciamo esattamente il formato del file possiamo usare tranquillamente la funzione fread(), in questo modo:
fgetc()
La funzione fgetc() è più semplice da usare rispetto alla fread() ma ovviamente è più limitata perché non può leggere dati di dimensione arbitraria, ma legge un solo carattere alla volta. L’esempio precedente può essere modificato per usare fgetc in questo modo:
fgets()
Se la necessità è quella di leggere una riga alla volta allora si può usare anche la funzione fgets(), il cui prototipo è:
La funzione fgets() legge dal file stream righe di lunghezza massima n e le memorizza nella variabile puntata da s. Questa funzione ha, a mio avviso, la pecca di mantenere all’interno della stringa letta anche il carattere \n finale, il che costringe spesso ad un’operazione di pulizia. Ha il vantaggio però di inserire autonomamente il carattere \0.
Vediamo come risulterebbe il solito esempio se usassimo questa funzione:

EOF

In questi esempi siamo stati un pò fortunati, perché sapevamo già che il file era composto esattamente da 25 righe e quindi abbiamo sfruttato un semplice FOR per eseguire la lettura. Ma cosa succede se il file può essere arbitrariamente lungo? In quel caso non possiamo far altro che continuare a leggere finché non raggiungiamo la fine del file, quindi ci toccherà utilizzare un WHILE, ma quale sarà il test per verificare se abbiamo o meno raggiunto la fine del file? Dipende dalla funzione che stiamo utilizzando, perché ciascuna funzione ha le sue particolarità:
fread()
Questa funzione restituisce il numero di byte effettivamente letti, quindi può essere usato il suo valore di ritorno per verificare se il file è terminato o meno, in aggiunta può essere usata anche la funzione feof() che indica se si è raggiunta o meno la fine del file. Ecco uno stralcio di esempio:
fgetc()
La funzione fgetc restituisce un carattere alla volta quindi è facile rendersi conto della fine del file, infatti basta inserire la lettura direttamente come condizione del while, come in questo esempio:
fgets()
Anche in questo caso è piuttosto semplice rilevare la fine del file, infatti basterà inserire la lettura come condizione del while:

Le funzioni per la scrittura

Una buona notizia per voi: se avete letto e capito la parte precedente di questo articolo siete praticamente ad un passo da gestire i file in totale libertà e autonomia, perché le funzioni per la scrittura su file sono praticamente identiche a quelle per la lettura. Vediamole seguendo lo stesso approccio precedente, dalle funzioni di basso livello a quelle di alto livello.
fwrite()
Il prototipo della funzione è il seguente:
e come si può notare è praticamente identico al prototipo della funzione fread(), questa funzione scrive nmembelementi composti da size byte ciascuno sullo stream stream, leggendoli dalla variabile puntata da ptr. Vediamone subito un esempio.
fputc()
Il duale della funzione fgetc() è la funzione fputc(). La sua funzione è quella di scrivere un carattere alla volta su un file. Il prototipo è :
Ed ecco un esempio di utilizzo:
fputs()
Come avrete già intuito la funzione fputs() è la duale della fgets() ed il suo utilizzo è molto semplice. La sua funzione è quella di scrivere una stringa (che deve essere terminata con uno \0) sul file. Attenzione che lo \0 non verrà scritto sul file. Il prototipo è il seguente:
ed ecco l’esempio di utilizzo:
fonte: http://www.devapp.it/wordpress/17-la-gestione-dei-file-in-c.html

Commenti