In questo brevissimo tutorial vediamo come si possa, usando il C standard, leggere e scrivere file di testo. È richiesta una conoscenza minimale del C.
1. Quando già si sa quali file leggere e scrivere
In questo primo codice di esempio assumiamo di conoscere già quali file vorremo leggere e scrivere; il loro nome (ed eventualmente il percorso) verranno pertanto inclusi direttamente nel codice sorgente del programma. Si tratta del caso più semplice, e pertanto lo esaminiamo per primo.
Aprite il vostro editor di testo preferito e digitate il seguente programma:
#include <stdio.h>
#include <stdlib.h>
#define kFileDaLeggere "input.txt"
#define kFileDaScrivere "output.txt"
#define kLunghezzaMaxStringa 100
#define kErroreIO -1
#define kErroreMemoria -2
#define kNessunErrore 0
double numero1,numero2,numero3;
char *stringa;
int main (void);
int LeggiDaFile(void);
int ScriviSuFile(void);
int main (void)
{
int errore;
errore=LeggiDaFile();
if(errore)
return errore;
errore=ScriviSuFile();
if(errore)
return errore;
return kNessunErrore;
}
int LeggiDaFile(void)
{
FILE *ingresso;
printf("Lettura dal file %s\n",kFileDaLeggere);
ingresso=fopen(kFileDaLeggere,"r");
if(ingresso==NULL)
return kErroreIO;
stringa=calloc(kLunghezzaMaxStringa,sizeof(char));
if(stringa==NULL)
return kErroreMemoria;
fscanf(ingresso,"%lg %lg %lg %s",&numero1,&numero2,&numero3,stringa);
printf("Numero 1: %g\nNumero 2: %g\nNumero 3: %g\nStringa: %s\n",numero1,numero2,numero3,stringa);
printf("\n");
fclose(ingresso);
return kNessunErrore;
}
int ScriviSuFile(void)
{
FILE *uscita;
printf("Scrittura sul file %s\n",kFileDaScrivere);
uscita=fopen(kFileDaScrivere,"w");
if(uscita==NULL)
return kErroreIO;
fprintf(uscita,"%g %g %g %s\n",numero1,numero2,numero3,stringa);
printf("Ora puoi aprire il file %s e verificare che contenga una copia di %s\n",kFileDaScrivere,kFileDaLeggere);
printf("\n");
return kNessunErrore;
}
Salvatelo come demoIO.c. Sempre col vostro editor di testo preferito, create anche un nuovo file, che salverete come input.txt nella stessa cartella in cui avete salvato demoIO.c, che abbia il seguente contenuto:
25.14 33.75 -3.14e24 Teodorico
Aprite il Terminale e recatevi nella cartella in cui avete salvato i due file. Compilate il programma col comando
gcc -o demoIO demoIO.c
Se non ci sono errori, eseguitelo col comando
./demoIO
Osservate che l'output dovrebbe essere simile a questo:
Lettura dal file input.txt
Numero 1: 25.14
Numero 2: 33.75
Numero 3: -3.14e+24
Stringa: Teodorico
Scrittura sul file output.txt
Ora puoi aprire il file output.txt e verificare che contenga una copia di input.txt
Dal Finder, aprite il file output.txt che è stato creato nella stessa cartella in cui avete salvato gli altri due, e verificate che contiene esattamente quello che conteneva input.txt.
Come è avvenuto tutto questo? Esaminiamo il codice nel dettaglio.
I nomi dei file di ingresso e di uscita sono memorizzati a livello di codice nelle due costanti kFileDaLeggere e kFileDaScrivere. Notate che i file vanno specificati col loro percorso; in assenza di questo, il programma li cerca o li colloca nella stessa cartella in cui si trova l'eseguibile; in alternativa, dovete specificare il nome del file con un percorso relativo alla cartella in cui si trova l'eseguibile o assoluto.
Quattro variabili globali (numero1, numero2, numero3 e stringa) saranno deputate a conservare il contenuto del file di input così da poterlo riscrivere nel file di output.
Le funzioni che si occupano di leggere e scrivere i file sono ovviamente LeggiDaFile() e ScriviSuFile(). La prima apre il file di ingresso mediante la funzione fopen(). Essa vuole come primo argomento il nome del file da aprire (con il suo percorso, relativo o assoluto), e come secondo argomento un parametro che specifica come vada aperto il file. Qui specifichiamo "r", che indica che il file va aperto in lettura (reading). Discuteremo più avanti degli altri parametri disponibili. Se il file viene correttamente trovato ed aperto, la variabile ingresso di tipo puntatore a FILE contiene un riferimento al file specificato; se no contiene il valore NULL, che può essere controllato per verificare se ci sono stati errori.
La lettura dal file si fa con una funzione assolutamente analoga a scanf() di nome fscanf(); l'unica differenza è che fscanf() anziché leggere dallo standard input (ovvero dalla tastiera) legge dal file specificato come primo argomento; il secondo argomento individua il formato di ciò che si va a leggere, e a seguire ci sono i puntatori alle variabili in cui verranno inseriti i contenuti del file. A parte il primo argomento (il puntatore al file), l'uso di fscanf() è assolutamente identico all'uso di scanf(), e alla documentazione su quest'ultima si rimanda il lettore.
Il file viene quindi chiuso con una chiamata a fclose().
La scrittura sul file si effettua un modo assolutamente analogo, le uniche differenze essendo il parametro da passare a fopen() (ora gli passiamo "w" per indicare la scrittura su file, o writing), e la funzione usata per la scrittura dei contenuti dei file: fprintf(), identica a printf() nell'uso (e ancora una volta rimandiamo il lettore alla documentazione su printf() per il dettaglio della sintassi) con la sola differenza che richiede, come primo argomento, il puntatore al file di uscita.
2. Quando ancora non si sa quali file leggere e scrivere
È possibile che in sede di scrittura del programma ancora non si sappia quali file verranno letti o scritti; tale scelta sarà infatti rinviata in fase di runtime. In questo caso non è possibile includere il nome del file e il suo percorso direttamente nel codice del programma, ma è meglio lasciare che sia l'utente a specificare su quali file intende agire. È naturalmente possibile prevedere una funzione del programma che chiede all'utente di immettere nome e percorso dei file di ingresso e di uscita, ma è possibile anche un'altra strada che esaminiamo qui brevemente: specificare i nomi dei file di interesse direttamente quando si lancia il programma tramite il Terminale.
Aprite il vostro editor di testo preferito e digitate il seguente codice, che è una variante di quello precedente:
#include <stdio.h>
#include <stdlib.h>
#define kLunghezzaMaxStringa 100
#define kErroreIO -1
#define kErroreMemoria -2
#define kErroreArgomenti -3
#define kNessunErrore 0
double numero1,numero2,numero3;
char *stringa;
int main (int argc, char *argv[]);
int LeggiDaFile(char *fin);
int ScriviSuFile(char *fout);
int main (int argc, char *argv[])
{
int errore;
if(argc != 3)
{
printf("Devi specificare un file di ingresso e uno di uscita!\n");
return kErroreArgomenti;
}
errore=LeggiDaFile(argv[1]);
if(errore)
return errore;
errore=ScriviSuFile(argv[2]);
if(errore)
return errore;
return kNessunErrore;
}
int LeggiDaFile(char *fin)
{
FILE *ingresso;
printf("Lettura dal file %s\n",fin);
ingresso=fopen(fin,"r");
if(ingresso==NULL)
return kErroreIO;
stringa=calloc(kLunghezzaMaxStringa,sizeof(char));
if(stringa==NULL)
return kErroreMemoria;
fscanf(ingresso,"%lg %lg %lg %s",&numero1,&numero2,&numero3,stringa);
printf("Numero 1: %g\nNumero 2: %g\nNumero 3: %g\nStringa: %s\n",numero1,numero2,numero3,stringa);
printf("\n");
fclose(ingresso);
return kNessunErrore;
}
int ScriviSuFile(char *fout)
{
FILE *uscita;
printf("Scrittura sul file %s\n",fout);
uscita=fopen(fout,"w");
if(uscita==NULL)
return kErroreIO;
fprintf(uscita,"%g %g %g %s\n",numero1,numero2,numero3,stringa);
printf("Ora puoi aprire il file %s.\n",fout);
printf("\n");
return kNessunErrore;
}
Salvatelo come demoIOargomenti.c. Nella stessa cartella accertatevi che ci sia il file di testo input.txt già creato in precedenza, e abbiate cura di cancellare il file output.txt. Compilate il programma col comando
gcc -o demoIOargomenti demoIOargomenti.c
ed eseguitelo col comando
./demoIOargomenti input.txt output.txt
Osservate che nella cartella in cui si trova il programma viene creato il file output.txt, che come prima contiene una copia del file input.txt.
Le funzioni di lettura da e di scrittura su file sono sempre le stesse, semplicemente abbiamo sfruttato la possibilità offerta dal C di ricevere degli argomenti nella funzione main(), argomenti che vengono specificati dall'utente all'atto di eseguire il programma. La funzione main() infatti ha ora due argomenti. Il primo, argc di tipo int, contiene il numero di argomenti specificati dall'utente quando ha eseguito il programma; essa vale sempre almeno 1, in quanto il primo argomento è sempre il nome del programma stesso. Il secondo, argv[], è un'array di puntatori a char (ovvero un'array di stringhe), e contiene (sotto forma di stringhe per l'appunto) i vari argomenti passati dall'utente al programma. Nel nostro caso, il programma richiede che l'utente specifichi esattamente e in quest'ordine un solo file di ingresso e un solo file di uscita, quindi argc deve valere 3 (nome del programma, file di ingresso, file di uscita) e l'array argv[] conterrà, in corrispondenza degli indici 0, 1 e 2 rispettivamente il nome del programma, il nome del file di ingresso e il nome del file di uscita. Questi ultimi due vengono passati ad argomento a LeggiDaFile() e a ScriviSuFile().
3. Osservazioni
- L'uso della funzione fscanf() per la lettura del contenuto di un file assume che già si sappia il formato con cui sono memorizzate le informazioni nel file stesso. Se esso differisce da quanto specificato nella stringa di formato ad argomento di fscanf() il programma restituirà un errore o si comporterà in maniera imprevedibile.
- È naturalmente possibile acquisire il contenuto di un file privo di un formato specifico memorizzandolo interamente all'interno di una stringa (sempre da acquisire con fscanf()) e rinviando ad un secondo momento l'esame del contenuto della stringa al fine di estrarne le informazioni contenute (parsing).
- In luogo di fscanf() è possibile usare le funzioni
- int fgetc(FILE *ingresso) che legge da ingresso un solo carattere e lo restituisce
- char *fgets(char *s,int n,FILE *ingresso) che legge n caratteri da ingresso e li copia in s (e li restituisce)
- Per la lettura da file sono inoltre disponibili altre funzioni, come fread(), fgetpos(), fsetpos(), feof() che vanno al di là dello scopo di questo tutorial.
- Similmente, la scrittura su file può essere fatta mediante funzioni alternative a fprintf(), quali ad esempio:
- int fputc(int c,FILE *uscita) che scrive il carattere contenuto nella variabile c nel file uscita
- int fputs(char *s,FILE *uscita) che scrive la stringa contenuta in s nel file uscita
- Anche per la scrittura su file, funzioni più complesse come fwrite() sono disponibili ma esulano dallo scopo di questo tutorial.
4. I parametri di fopen()
Qui di seguito sono indicati i parametri che, racchiusi tra virgolette, possono essere passati a fopen() per specificare la modalità con cui andrà aperto il file:
- r - il file viene aperto in sola lettura
- w - il file viene aperto in scrittura; se un file con lo stesso nome esisteva già, viene cancellato
- a - il file viene aperto in scrittura; se un file con lo stesso nome esisteva già, il nuovo contenuto viene aggiunto al fondo del contenuto vecchio, che non viene cancellato
- rb - come r, ma il file è binario
- wb - come w, ma il file è binario
- ab - come a, ma il file è binario
- r+ - il file viene aperto in lettura e scrittura; se un file con lo stesso nome esisteva già, il contenuto precedente non viene cancellato
- w+ - il file viene aperto in lettura e scrittura; se un file con lo stesso nome esisteva già, il contenuto precedente viene cancellato
- a+ - il file viene aperto in lettura e scrittura; se un file con lo stesso nome esisteva già, il contenuto precedente non viene cancellato, ma tutte le operazioni di lettura e scrittura avvengono al termine del file, in coda al contenuto precedente
- r+b oppure rb+ - come r+, ma il file è binario
- w+b oppure wb+ - come w+, ma il file è binario
- a+b oppure ab+ - come a+, ma il file è binario
Buon divertimento! : )