Input/Output su disco In C, la gestione dei dispositivi di lettura (tastiera, file su disco,...) e scrittura (monitor, file su disco, stampante,...) viene effettuata mediante canali di comunicazione. Tali canali possono essere sia di input o di output (a seconda che il flusso di informazioni avvenga dal canale verso il codice o dal codice verso il canale) e rappresentano una sorta di interfaccia tra il codice ed il dispositivo fisico cui si vuole accedere dall'interno del codice stesso. In questo modo, le operazioni di accesso ai singoli dispositivi diventano indipendenti dalle caratteristiche tecniche di tali dispositivi; ad esempio, la scrittura su monitor e quella su file vengono effettuate utilizzando lo stesso set di funzioni, permettendo al programmatore di non curarsi di come il processo di scrittura venga effettivamente portato a termine. In generale, prima di poter comunicare con un dispositivo è necessario attivare un canale di comunicazione; nel caso in cui il programma debba accedere alla tastiera o al monitor, non è necessario effettuare tale apertura, in quanto tastiera e monitor rappresentano i canali di input ed output standard, stdin e stdout, e risultano automaticamente aperti all'avvio del programma. L'apertura del canale potrà avvenire in modalità di scrittura o lettura ed il flusso di informazioni potrà essere gestito in modalità testo o binaria.
Scrittura su file La scrittura di informazioni all'interno di un documento memorizzato sull'hard-disk del computer è possibile dopo aver attivato il canale di comunicazione con tale documento in modalità scrittura. Nel caso il flusso di informazioni venga gestito in modalità testo, l'invio di informazioni al file avviene per caratteri (non nel senso che si può scrivere solo un carattere per volta, ma nel senso che, una volta inviata un'informazione al file, essa viene salvata sotto forma di un insieme di caratteri). Ad esempio, il valore 123.45, una volta salvato su file, diventa una sequenza di caratteri: '1', '2', '3', '.', '4' e '5'. Le funzioni di gestione dell'output permettono di inviare su file dei dati: 1. carattere per carattere, usando la funzione int putc(int c,file*output) che permette di inviare il carattere c al dispositivo cui è collegato il canale output 2. in modalità formattata, usando la funzione in fprintf(file*output, "stringa di formato",...) che permette di inviare l'output sfruttando le medesime potenzialità della funzione printf() 3. una stringa alla volta, usando la funzione char *fputs(char*stringa,file* output) Vediamo tre esempi in cui l'output venga inviato su disco in questi tre diversi modi. /* inizio codice */ #include <stdio.h> #include <stdlib.h> int main() {int i; char *stringa_di_prova="prova di invio dati carattere per carattere"; FILE *output; /* questa è la variabile da utilizzare per l'invio di dati al canale di comunicazione con il file. prima di iniziare la comunicazione con il documento su disco è necessario attivare il canale di comunicazione, specificando il nome del documento e la modalità di accesso al documento stesso */ output = fopen("prova_char_a_char.txt","w"); /* accediamo al dispositivo in scrittura
*/ if(output ==NULL) /* verifico che il canale sia attivo */ {printf("\n apertura fallita"); exit(0); /* ora è possibile accedere al documento */ i=0; while( *(stringa_di_prova+i) ) /* eseguo il ciclo fino al raggiungimento dell'ultimo char */ {putc( *(stringa_di_prova+i), output ); i++; /* ora inserisco un carattere "a capo" */ putc('\n', output ); /* ora inserisco la stringa al contrario */ i--; while( i >= 0 ) /* eseguo il ciclo fino al raggiungimento del primo char */ {putc( *(stringa_di_prova+i), output ); i--; /* Ora iniziamo le operazioni di scrittura usando la funzione fprintf() */ fprintf(output,"\n iniziano le operazioni di scrittura mediante fprintf()\n"); fprintf(output,"\n valore casuale compreso tra 0 e 999: %d", rand() % 1000); fprintf(output,"\n una stringa: %s\n",stringa_di_prova); /* Ora iniziamo le operazioni di scrittura usando la funzione fputs() */ fputs("\n ora scrivo su file, una stringa per volta\n",output); fputs(stringa_di_prova,output); /* Ora chiudiamo il canale di comunicazione. Se ci dimenticassimo di farlo, esso verrebbe automaticamente chiuso al termine del programma */ fclose(output); return 0; /* fine codice */ Qual è ora il contenuto del file prova_char_a_char.txt? Proviamo ad aprirlo con un qualsiasi editor di testo... Prova di invio dati carattere per carattere erettarac rep erettarac itad oivni id avorp iniziano le operazioni di scrittura mediante fprintf() valore casuale compreso tra 0 e 999: 41 una stringa: Prova di invio dati carattere per carattere ora scrivo su file, una stringa per volta Prova di invio dati carattere per carattere
Notate che nel testo non si vedono esplicitamente i caratteri '\n', a capo; se ne nota però l'effetto. Questo indica che, quando un file viene letto in modalità testo, alcuni dei caratteri, i cosiddetti caratteri di controllo, possono essere interpretati e non visualizzati. Lettura da file Le operazioni di input da file possono essere effettuate con le stesse modalità cui cui esse venivano effettuate a partire dal buffer di input standard, ovvero carattere per carattere, a gruppi di caratteri (il separatore tra gruppo e gruppo è rappresentato dal carattere ' ') e stringa per stringa. Per capire quando si sia raggiunta la fine del file, identificata dal carattere EOF, è utile considerare i valori restituiti dalle funzioni di lettura: int fgetc(file*input) restituisce il carattere letto dal canale input o EOF, se incontra la fine del file o se si verifica un errore int fscanf(file*input,"stringa di formato") restituisce un numero pari al numero di caratteri letti o EOF se durante la lettura viene incontrata la fine del file char *fgets(char*stringa,int numero,file* input) la funzione preleva dal canale input un numero di caratteri che al massimo è costituito da numero-1 elementi e li copia nell'area di memoria cui fa riferimento il puntatore stringa. La funzione restituisce il puntatore stringa, se l'operazione di lettura ha successo, ovvero dopo aver copiato numero-1 caratteri, o dopo aver incontrato un carattere "nuova linea" o se durante la lettura incontra EOF. Essa restituisce NULL se si verifica un errore o se tenta di leggere oltre la fine del file. Utilizziamo tali modalità per leggere il contenuto di questo file: prova_di_lettura.txt prima riga seconda riga ora alcuni numeri interi 34 678 98323 ora alcuni decimali 23.2391 432.12323
/* inizio codice */ #include <stdio.h> #include <stdlib.h> int main() {int valore; char c; char lettura[100]; float f; FILE *input; /* questa è la variabile da utilizzare per la lettura di dati dal canale di comunicazione con il file. prima di iniziare la comunicazione con il documento su disco è necessario attivare il canale di comunicazione, specificando il nome del documento da leggere e la modalità di accesso al documento stesso */ input = fopen("prova_di_lettura.txt","r"); /* accediamo al dispositivo in lettura*/ if(input ==NULL) /* verifico che il canale sia attivo */ {printf("\n apertura fallita"); exit(0); /* leggiamo il testo del file, carattere per carattere, e visualizziamone il contenuto */ while(1) {c = fgetc(input); if(c==eof) break; /* interrompo il ciclo di lettura */ if(c<31) printf("%d",c); /* se è un carattere di controllo, ne visualizzo il codice ASCII */ else printf("%c",c); /* riavvolgiamo il file e utilizziamo la funzione fscanf() */ rewind(input); while(1) {valore = fscanf(input,"%s",lettura); if(valore == EOF) break; printf("%s",lettura); /* Notate che i caratteri "a capo" non sono stati inseriti nelle stringhe lette. Riavvolgiamo il file e utilizziamo la funzione fscanf() per leggere i numeri e la funzione fgets() per leggere le righe contenenti solo testo */ /* le prime tre righe sono formate da testo */ fgets(lettura,100,input); /* leggo la prima riga */ puts(lettura); fgets(lettura,100,input); /* leggo la seconda riga */ puts(lettura); fgets(lettura,100,input); /* leggo la terza riga */ puts(lettura); /* poi ci sono tre numeri interi */
fscanf(input,"%d",&valore); printf(" %d ",valore); fscanf(input,"%d",&valore); printf(" %d ",valore); fscanf(input,"%d",&valore); printf(" %d ",valore); /* poi una riga contenente solo testo */ fgets(lettura,100,input); puts(lettura); /* ed infine due decimali */ fscanf(input,"%f",& f); printf(" %f ",f); fscanf(input,"%f",& f); printf(" %f ",f); fclose(input); return 0; /* fine codice */ Oltre alla possibilità di gestire la comunicazione attraverso il canale in modalità testo, è possibile utilizzare la gestione in modalità binaria, ovvero mediante un flusso di Byte, sia in input che in output. In pratica, usando questa modalità, vengono lette dal disco o scritte su disco le sequenze di Byte che servono per rappresentare il valore di una data variabile. Tale flusso non subisce le interpretazioni cui può andare soggetto un flusso di dati gestito in modalità testo. Un vantaggio derivante dall'utilizzare la gestione binaria del flusso di dati è rappresentato dal risparmio di memoria che essa può comportare. consideriamo le seguenti variabili int a = 4000, b = 1234567890;
Entrambe sono variabili di tipo intero, quindi occupano 4 Byte in memoria; se copio tali byte su un file, entrambe occuperanno 4 Byte su disco. Se, viceversa, scrivo il valore delle variabili in modalità testo, la prima occuperà 4 Byte sul disco mentre la seconda ne occuperà 9. Come scriviamo / leggiamo Byte per Byte? int fwrite(void *puntatore,int dim,int num,file*output) La funzione invia al canale cui fa riferimento la variabile output la sequenza di Byte relativa ad un numero num di oggetti, ciascuno di dimensione pari a dim; tali byte sono vengono cercati a partire dalla locazione di memoria cui fa riferimento puntatore. Esempio: salviamo i valori di un array su disco. /* inizio codice */ #include <stdio.h> #include <stdlib.h> #define DIM 50 int main() {int *array; int i; FILE *output = fopen("testo.dat","w"); if(output == NULL) exit(0); FILE *bin_output = fopen("binario.dat","wb"); if(bin_output == NULL) exit(0); if((array = (int *) malloc( DIM * sizeof(int))) == NULL) exit(0); /* inizializzo gli elementi */ array[i] = rand() ; /* scrivo i valori in modalità testo usando il canale di testo */ fprintf(output," %d \n",array[i]); fclose(output); /* chiudo il canale di testo */ /* scrivo i valori in modalità binaria, uno dopo l'altro */
fwrite(array+i,sizeof(int),1,bin_output); /* scrivo i valori in modalità binaria, tutti in una sola volta */ fwrite(array,sizeof(int),dim,bin_output); /* scrivo l'intero array, in un colpo solo */ fwrite(array,dim*sizeof(int),1,bin_output); fclose(bin_output); return 0; /* fine codice */ Ora nel file binario ci sono tre copie del medesimo array. Leggiamo tali valori in modalità binaria usando la funzione fread(). Se tentiamo di leggere il file binario.dat usando un editor di testo vediamo qualcosa che non ha nulla a che fare con i valori salvati nel file testo.dat int fread(void *puntatore,int dim,int num,file*input) La funzione legge dal canale cui fa riferimento la variabile input la sequenza di Byte relativa ad un insieme di al più num oggetti, ciascuno di dimensione pari a dim; tali byte sono vengono cercati a partire dalla locazione di memoria cui fa riferimento puntatore. /* inizio codice */ #include <stdio.h> #include <stdlib.h> #define DIM 50 int main() {int *array; int i; FILE *bin_input = fopen("binario.dat","rb"); if(bin_input == NULL) exit(0); /* riservo l'area di memoria in cui scrivere i valori che verranno letti in seguito */ if((array = (int *) malloc( DIM * sizeof(int))) == NULL) exit(0); /* leggo i valori, tutti in una volta */
fread(array,sizeof(int)*dim,1,bin_input); /* visualizzo i valori */ fprintf(stdout," %d \n",array[i]); /* ora li leggo uno dopo l'altro */ fread(array+i,sizeof(int),1,bin_input); fprintf(stdout," %d \n",array[i]); fclose(bin_input); return 0; /* fine codice */