FILE DATI 16 lunedi 26 luglio giovedi ore 9 Come sappiamo, un file dati è un gruppo di informazioni tra loro correlate, memorizzate in un dispositivo di memoria secondaria e utilizzabili da un programma. Dichiarazione (FILE). Ogni file dati è caratterizzato da un nome di file, in genere non più lungo di 8 caratteri, seguito opzionalmente da un punto e da una estensione di un massimo di 3 caratteri. In un programma C, un file è sempre riferito da un nome di variabile che deve essere dichiarata nel programma. Tale variabile punta a una speciale struttura di file, e quindi deve essere dichiarata come un puntatore a un file. Esempi di dichiarazioni: FILE *file_in; FILE *prezzi; Il nome del puntatore è il nome con il quale il programma farà riferimento al file dati, e non deve essere necessariamente uguale al nome esterno usato dal computer per memorizzare il file. Il termine FILE è il nome tag di una speciale struttura dati usata dal C per memorizzare le informazioni relative al file, tra le quali: se il file sia disponibile per lettura o scrittura; il prossimo carattere disponibile nel file: dove tale carattere sia memorizzato. L’effettiva dichiarazione della struttura di un file e la sua equivalenza al nome simbolico FILE sono contenute nel file d’intestazione standard stdio.h, che deve essere incluso all’inizio di ogni programma che usi un file dati. Apertura (fopen()). L’apertura di un file è una procedura che raggiunge due scopi, uno solo dei quali interessa direttamente il programmatore: 1. Stabilisce un canale fisico di comunicazione tra il programma e il file dati: dato che i dettagli di questo collegamento sono gestiti dal sistema operativo e sono trasparenti al programma, il programmatore di solito non li considera. 2. Uguaglia il nome del file esterno al computer al nome puntatore usato internamente dal programma. La funzione per l’apertura di un file è chiamata fopen(); dato che la sua dichiarazione è inserita nel file d’intestazione stdio.h, esso va incluso in ogni programma che usi fopen(). Vediamo come vada chiamata fopen() per collegare il nome esterno di un file a quello interno. Per usare fopen() occorre passarle due argomenti, racchiusi tra doppi apici: il nome del file per il computer; il modo in cui il file va aperto. I modi principali sono w, a, r, quelli secondari sono w+, a+, r+, che hanno i seguenti significati: L’apertura di un file per scrittura crea un nuovo file e lo rende disponibile per l’uscita della funzione che lo ha aperto. Ad es., l’istruzione: out_file = fopen(“prezzi.dat”, “w”); crea un file di nome esterno prezzi.dat in cui è possibile scrivere, e permette al programma di accedervi usando il puntatore interno di nome out_file. #include <stdio.h> . out_file . prezzi.dat se esiste già un file con il nome di uno aperto per scrittura, esso viene cancellato. L’apertura per aggiunta di un file esistente consente di aggiungervi dati alla fine. Se il file aperto per aggiunta non esiste, ne viene creato uno con il nome indicato, in grado di ricevere l’uscita dal programma. Ad es., l’istruzione: out_file = fopen(“prezzi.dat”, “a”); apre il file prezzi.dat e lo rende disponibile per l’aggiunta di dati alla sua fine. L’unica differenza tra l’apertura un file in modo scrittura e in modo aggiunta è il posto in cui i dati sono fisicamente messi nel file: • nel modo scrittura sono scritti a partire dall’inizio del file, • nel modo aggiunta sono scritti a partire dalla fine. Per un file nuovo, i due modi sono identici. Funzioni di scrittura (fputc(), fputs(), fprintf()). Per scrivere i dati in un file aperto in modo scrittura o aggiunta si usano le funzioni di biblioteca indicate in tabella, che sono simili a quelle usate per visualizzare i dati sullo schermo (putchar(), puts(), printf()). In ciascuna di esse il nome-di-file è il nome del puntatore interno specificato quando il file è stato aperto. Ad es., se out_file è il nome di puntatore interno di un file aperto in modo scrittura o aggiunta, sono valide le seguenti istruzioni di uscita: fputc(‘a’, out_file); fputs(“Salve tutti”, out_file); fprintf(out_file, “%s %d”, descriz, prezzo); Osservazione. Le funzioni fputc() fputs() fprintf() sono usate nello stesso modo delle loro equivalenti putchar() puts() printf() con l’aggiunta di un nome-di-file come argomento. Esso dirige l’uscita a un file specifico, anziché al dispositivo di uscita standard che è lo schermo. I prototipi di fputc(), fputs(), fprintf() sono contenuti nel file stdio.h. Chiusura (fclose()). Dopo essere stato aperto, un file va chiuso con la funzione fclose(), che ha come argomento il nome del puntatore usato quando il file è stato aperto. Essa rompe il legame tra i nomi esterno e interno del file rilasciando il nome del puntatore al file interno, che quindi può essere usato per un altro file. Ad es., l’istruzione fclose(out_file); chiude il file puntato da out_file. Dato che il numero di file che possono rimanere aperti contemporaneamente è limitato, è bene chiudere i file non più necessari (tutti i file aperti sono chiusi automaticamente dal sistema operativo alla fine della normale esecuzione del programma). Scrittura da programma. Il programma seguente illustra l‘uso della funzione di scrittura file fprintf() per scrivere in un file un elenco di descrizioni e prezzi contenuti nel programma stesso. #include <stdio.h> void main(void) { int i; FILE *out_file; /* dichiarazione di FILE */ float prezzo[] = {39.95, 3.22, 1.03}; char *descriz[] = {"Batterie", "Bulbi", "Fusibili"}; out_file = fopen("prezzi.dat","w"); /* apre il file */ if(out_file == NULL) { printf("\nErrore nell’apertura del file.\n"); exit(1); } for(i = 0; i < 3; ++i) fprintf(out_file,"%-9s %5.2f\n", descriz[i], prezzo[i]); fclose(out_file); } Se per qualche ragione non è possibile aprire il file, il puntatore out_file assume il valore NULL (già visto nel caso delle liste concatenate), viene visualizzato un messaggio di errore e la funzione exit() passa il suo argomento al sistema operativo, determinando la fine della esecuzione del programma. Se invece il file viene aperto, viene creato e salvato il file prezzi.dat consistente nelle tre linee seguenti: Batterie 39.95 Bulbi 3.22 Fusibili 1.03 La sequenza di controllo di conversione %-9s nella chiamata alla funzione printf() forza la giustificazione a sinistra delle descrizioni in un campo di 9 caratteri. Analogamente, i prezzi sono giustificati a destra in un campo di 5 caratteri, a partire da uno spazio dopo la fine del campo descrizioni. Sebbene sembri che nel file siano memorizzati 42 caratteri, corrispondenti alle descrizioni, ai caratteri vuoti e ai prezzi scritti nel file, esso in effetti ne contiene 46. I caratteri aggiuntivi consistono in sequenze di escape di linea nuova alla fine di ogni linea, e in uno speciale marcatore di fine-file posto come ultimo carattere nel file quando esso viene chiuso. La memorizzazione effettiva dei caratteri nel file prezzi.dat è costituita dai loro codici ASCII, come indicato in figura. Il codice per il marcatore di fine-file ^Z dipende dal sistema usato: uno molto comune è il codice esadecimale 26. 42 B 61 a 74 t 74 t 65 e 72 r 69 i 65 e 20 33 3 39 9 2E . 32 2 35 5 0A \n 42 B 75 u 6c l 62 b 69 i 20 20 20 20 20 33 3 2E . 32 2 32 2 0A \n 46 F 75 u 73 s 69 i 62 b 69 i 6c l 69 i 20 20 31 1 2E . 30 0 33 3 0A \n 26 ^Z Osservazione. Le due istruzioni out_file = fopen("prezzi.dat","w"); if(out_file == NULL) possono essere sostituite dall’unica istruzione if((out_file = fopen("prezzi.dat","w")) == NULL) L’esecuzione del programma non produce alcuna uscita sullo schermo, e infatti la lettura del file andrà eseguita con un programma specifico. Lettura. L’apertura di un file per lettura richiama un file esistente e ne rende i dati disponibili come ingresso per il programma. Ad es., l’istruzione di apertura: in_file = fopen(“prezzi.dat”, “r”); apre il file prezzi.dat e rende i suoi dati disponibili per l’ingresso. All’interno della funzione che apre il file, esso è letto usando il nome di puntatore in_file. Se un file aperto per lettura non esiste, fopen() restituisce il valore di indirizzo NULL. Esso può essere usato per controllare se sia stato aperto un file esistente. Osservazione. Nelle precedenti istruzioni di apertura sia il nome di file esterno, sia gli argomenti relativi al modo passati a fopen() erano stringhe racchiuse tra doppi apici. Se il nome di file esterno è dapprima memorizzato in un vettore di caratteri o come una stringa, si può usare il nome di vettore o la stringa, senza apici, come primo argomento per fopen(). Apertura. Il programma seguente illustra: • le istruzioni necessarie per aprire un file in modo lettura: chiede all’utente il nome di un file dati esterno e lo memorizza nel vettore di caratteri nome_f; passa quindi questo nome di vettore alla funzione fopen(). • l’uso del valore restituito da fopen() per controllare la corretta apertura del file: l’utente viene avvertito se il nome file esterno non esiste. #include <stdio.h> void main(void) { FILE *in_file; char nomefile[12]; printf("\nScrivi il nome del file da leggere: "); gets(nomefile); /* oppure: scanf(“%s”, nomefile); */ in_file = fopen(nomefile, "r"); /* apre il file */ if (in_file == NULL) { printf("\nIl file non può essere aperto."); printf("\nControlla che il file esista."); exit(1); } printf("\nIl file è stato aperto per la lettura"); } Ecco un esempio della sua esecuzione: Scrivi un nome di file: pippo Il file non può essere aperto. Controlla che il file esista. Osservazione. Anche adesso le istruzioni per aprire il file e assegnare l’indirizzo restituito possono essere inserite direttamente nell’istruzione if, sostituendo le due linee in_file = fopen(nomefile, "r"); if (in_file == NULL) con l’unica espressione if ((in_file = fopen(nomefile, “r”)) == NULL) Il programma precedente non contiene ancora le istruzioni necessarie per leggere i dati nel file e per chiuderlo. Funzioni di lettura (fgetc(), fgets(), fscanf()). La lettura dei dati da un file è quasi identica a quella da una tastiera standard, con l’aggiunta del nome-di-file per indicare la loro provenienza. Le funzioni disponibili per leggere i dati da un file sono simili alle funzioni getchar(), gets(), scanf() usate per l’ingresso dei dati da tastiera, e sono indicate in tabella. Ad es., se in_file è il nome interno di un puntatore a un file aperto in modo lettura, per leggere dati dal file si possono usare le seguenti espressioni: fgetc(in_file); /* legge il prossimo carattere nel file */ fscanf(in_file, “%f”, &prezzo); /* legge un numero in virgola mobile */ fgets(messaggio, 10, in_file); /*legge i 9 caratteri successivi dal file in messaggio*/ Tutte le funzioni di ingresso rilevano correttamente il marcatore di finefile; tuttavia, quando esso viene rilevato, le funzioni fgetc() e fscanf() restituiscono la costante di nome EOF; la funzione fgets() restituisce il carattere NULL (\0). Entrambe le costanti EOF e NULL sono sentinelle utili per rilevare la fine di un file che viene letto, a seconda della funzione usata. Uso di fgetc(). Come esempio di uso di fgetc(), scriviamo un programma che visualizzi i codici ASCII dei caratteri memorizzati nel file prezzi.dat. Per visualizzare i codici in formato esadecimale usiamo la sequenza di controllo di conversione %x. #include <stdio.h> void main(void) { char var; FILE *in_file; if((in_file = fopen("prezzi.dat", "r")) == NULL) { printf("\nErrore nell'apertura del file.\n"); exit(1); } do { var = fgetc(in_file); printf("%x %c", var, ' '); } while (var != EOF); fclose(in_file); } Esercizio. Modificare il programma precedente, in modo che chieda il nome del file da leggere e ne visualizzi i caratteri. Ecco una possibile risposta. #include <stdio.h> void main(void) { char var, nomefile[12]; FILE *in_file; printf(“Scrivi il nome del file da leggere”); scanf(“%s”, nomefile); /* oppure: gets(nomefile); */ if((in_file = fopen(nomefile, "r")) == NULL) { printf("\nErrore nell'apertura del file.\n"); exit(1); } do { var = fgetc(in_file); printf("%c", var); } while (var != EOF); fclose(in_file); } Se invece si vogliono leggere tutti i caratteri presenti in un file, compresi quelli privi di “significato”, occorre leggere il file in modalità binaria. Il programma seguente legge in modalità binaria il file salve.doc, contenente la frase “Salve a tutti” e creato con Microsoft Word, di dimensione 24 kbyte. #include <stdio.h> void main(void) { char var; int i; FILE *in_file; if((in_file = fopen("salve.doc","rb")) == NULL) { printf("\nErrore nell'apertura del file.\n"); exit(1); } for(i = 0; i < 24000; i++) { var = fgetc(in_file); printf("%c", var); } fclose(in_file); } Uso di fscanf(). La lettura dei dati tramite fscanf() richiede che si conosca il formato in cui essi sono memorizzati nel file (esattamente come succedeva con scanf()). Ciò per una corretta “strisciatura” dei dati dal file in variabili adatte per la memorizzazione. Tutti i file sono letti in modo sequenziale, cosicché, una volta letta una voce, la successiva nel file diventa disponibile per la lettura. Il programma seguente illustra la lettura del file prezzi.dat tramite fscanf(): si osservi l’uso del marcatore EOF, che viene restituito da fscanf() quando s’incontra la fine del file. Lettura con fscanf() #include <stdio.h> void main(void) { char descriz[10]; float prezzi; FILE *in_file; if((in_file = fopen("prezzi.dat","r")) == NULL) { printf("\nErrore nell’apertura del file.\n"); exit(1); } while (fscanf(in_file,"%s %f",descriz, &prezzi) != EOF) printf("%-9s %5.2f\n",descriz, prezzi); fclose(in_file); } Il programma continua a leggere il file fino a che s’incontra il marcatore EOF. Ogni volta che si legge il file, sono immessi nel programma una stringa e un numero in virgola mobile. Ecco la visualizzazione prodotta: Batterie 39.95 Bulbi 3.22 Fusibili 1.03 Uso di fgets(). In luogo della funzione fscanf() si può usare la funzione fgets(), che richiede tre argomenti: l’indirizzo dove sarà memorizzato il primo carattere letto; il numero massimo di caratteri da leggere; il nome del file d’ingresso. Ad es., la chiamata di funzione: fgets(linea, 81, in_file); determina la lettura di un massimo di 80 caratteri (1 in meno del numero indicato) dal file in_file e la loro memorizzazione a partire dall’indirizzo contenuto nel puntatore di nome linea. fgets() continua a leggere i caratteri fino a che • ne sono stati letti 80, oppure • s’incontra un carattere di linea nuova (\n). Nel secondo caso, il carattere viene incluso con gli altri caratteri immessi prima che la stringa sia terminata con il marcatore di finestringa \0. Anche fgets() rileva il marcatore di fine-file, ma quando lo incontra restituisce il carattere NULL. Il programma seguente illustra l’uso di fgets() in un programma effettivo. Lettura con fgets() #include <stdio.h> void main(void) { char linea[81]; FILE *in_file; if((in_file = fopen("prezzi.dat", "r")) == NULL) { printf("\nImpossibile aprire il file.\n"); exit(1); } while (fgets(linea, 81, in_file) != NULL) printf("%s", linea); fclose(in_file); } Questo programma di lettura è in realtà un programma di copia di un testo linea per linea, che legge una linea di testo dal file e la visualizza sullo schermo. Perciò la sua uscita è identica a quella del programma di lettura con fscanf(). Però, a differenza di esso, non usa le variabili individuali descrizione e prezzo per memorizzare i valori. Se ciò fosse necessario, si dovrebbe elaborare ulteriormente la stringa restituita da fgets() tramite la funzione di scansione di stringa, scanf(). Per estrarre la descrizione e il prezzo dalla stringa memorizzata nel vettore di caratteri linea si potrebbe, ad es., aggiungere l’istruzione scanf(linea, “%s %f”, descriz, &prezzo); Ciò, naturalmente, dopo la dichiarazione delle variabili descriz e prezzo: char descriz[10]; float prezzo; File standard di dispositivo (stdin, stdout). I puntatori a file di dati che abbiamo usato erano tutti puntatori a file logici. Un puntatore a un file logico fa riferimento a un file o a un dato collegato che è stato salvato sotto un nome comune; cioè “punta a” un file di dati. In aggiunta ai puntatori a file logici, i C supporta anche puntatori a file fisici. Un puntatore a un file fisico “punta a” un dispositivo hardware quale la tastiera, lo schermo o una stampante. L’effettivo dispositivo fisico assegnato a un programma per l’ingresso dei dati è detto formalmente file d’ingresso standard, ed è di solito la tastiera. Quando in un programma C s’incontra una chiamata alla funzione scanf(), il computer va automaticamente a questo file d’ingresso standard per l’ingresso atteso. Analogamente, quando s’incontra una chiamata alla funzione printf(), l’uscita è automaticamente visualizzata o “scritta su” un dispositivo che è stato assegnato come file standard di uscita, in genere lo schermo (ma talvolta una stampante). Quando si esegue un programma, la tastiera usata per l’ingresso dei dati è automaticamente aperta e assegnata al puntatore di file interno di nome stdin. Analogamente, il dispositivo di uscita usato per visualizzare è assegnato al puntatore di file di nome stdout. Questi puntatori di file sono sempre disponibili per l’uso del programmatore. Le somiglianze tra printf() e fprintf(), e tra scanf() e fscanf() non sono accidentali: • printf() è un caso speciale di fprintf() che ha come default il file di uscita standard, mentre • scanf() è un caso speciale di fscanf() che ha come default il file d’ingresso standard. Così, si ottiene la stessa visualizzazione con le due espressioni: fprintf(stdout, “Salve a tutti”); e printf(“Salve a tuti”); come pure sono tra loro equivalenti le espressioni: fscanf(stdin, “%d”, &num); e scanf(“%d”, &num); Puntatore di file (stderr). Oltre ai puntatori di file stdin e stdout, esiste un terzo puntatore, di nome stderr, assegnato al dispositivo di uscita usato per i messaggi di sistema. Sebbene stderr e stdout si riferiscano frequentemente allo stesso dispositivo, l’uso di stderr fornisce un mezzo per reindirizzare qualsiasi messaggio di errore dal file usato per la normale uscita del programma. Proprio come printf() e scanf() sono casi speciali rispettivamente di fprintf() e fscanf(), anche le funzioni getchar() e gets(), putchar() e puts() sono casi speciali delle funzioni più generali elencate in tabella. Le coppie di funzioni di carattere sulla stessa riga (evidenziate in verde) possono essere usate in modo intercambiabile le une con le altre. Ciò non è vero per le funzioni che manipolano le stringhe (evidenziate in rosa), per le seguenti ragioni. Confronto tra fgets()/gets(). . . In fase d’ingresso, come osservato, la funzione fgets() legge i dati da un file fino a una sequenza di escape di linea nuova (\n), o fino a che sia stato letto uno specificato numero di caratteri. Come sappiamo, la funzione gets()non memorizza nella stringa finale la sequenza di escape di linea nuova (\n) (dato che la traduce in un carattere NULL (\0)). Invece, come abbiamo visto nel programma di lettura con fgets(), se fgets() incontra una sequenza di escape di linea nuova (\n) la memorizza insieme agli altri caratteri immessi. Entrambe le funzioni terminano i caratteri immessi con un carattere NULL di fine-stringa. . . . e puts()/fputs() In fase di uscita, sia puts() sia fputs() scrivono tutti i caratteri nella stringa, tranne il carattere terminale di fine-stringa NULL. Come sappiamo, la funzione puts() aggiunge automaticamente una sequenza di escape di linea nuova (\n) alla fine dei caratteri trasmessi. Invece fputs() non aggiunge la sequenza \n alla fine dei caratteri trasmessi. Scrittura di un file da tastiera (feof()). Mentre il precedente programma di scrittura scriveva in un file i dati contenuti nel programma stesso, di solito i dati vengono forniti in modo interattivo da tastiera. Il programma seguente crea il file ad accesso sequenziale cliente.dat relativo alla situazione contabile dei clienti di una banca, nel quale memorizza i dati relativi a numero di conto, nome e saldo di ciascun cliente, immessi da tastiera. Per terminare l’immissione dei dati si deve immettere il carattere di finefile (^Z in ambiente Windows). I dati di ogni cliente costituiscono il suo “record”, mentre il numero di conto costituisce la “chiave” del record, ossia il campo rispetto al quale il file viene ordinato. #include <stdio.h> int main() { int conto; char nome[30]; float saldo; FILE *out_file; if((out_file = fopen("cliente.dat","w")) == NULL) printf("Impossibile aprire il file\n"); else { printf("Scrivi conto, nome, saldo (^Z per teminare).\n"); scanf("%d %s %f", &conto, nome, &saldo); while(!feof(stdin)) { fprintf(out_file, "%d %s %f\n", conto, nome, saldo); scanf("%d %s %f", &conto, nome, &saldo); } fclose(out_file); } return(0); } Il programma contiene l’espressione while(!feof(stdin)) che determina l’elaborazione di un nuovo record se in precedenza non è stato immesso da tastiera il carattere di fine-file. Per determinare la presenza del carattere di fine-file nel file al quale punta stdin (ossia la tastiera), si è usata la funzione feof(). Essa ha come argomento il puntatore al file nel quale si vuole esaminare la presenza dell’indicatore di fine-file (come detto, il file è stdin, ossia la tastiera). La funzione feof() restituisce un valore diverso da 0 (vero) quando rileva nel file la presenza dell’indicatore di fine-file, altrimenti restituisce 0. Lettura del file immesso da tastiera. Se eseguiamo il programma precedente, possiamo verificare che i record siano stati memorizzati correttamente nel file eseguendo il programma seguente, che legge il file cliente.dat e ne visualizza il contenuto. #include <stdio.h> int main() { int conto; char nome[30]; float saldo; FILE *out_file; if((out_file = fopen("cliente.dat", "r")) == NULL) printf("Il file non può essere letto\n"); else { printf("%-10s %-16s %-12s\n", "Conto", "Nome", "Saldo"); fscanf(out_file, "%d %s %f", &conto, nome, &saldo); while(!feof(out_file)) { printf("%-10d %-13s %7.2f\n", conto, nome, saldo); fscanf(out_file, "%d %s %f", &conto, nome, &saldo); } fclose(out_file); } return(0); } File offset. La struttura FILE, associata a ogni file, ha tra i suoi membri il puntatore di posizione del file, detto anche file offset. Esso non è un puntatore vero e proprio, ma un numero intero che specifica la posizione del byte in corrispondenza del quale sarà eseguita la prossima lettura o scrittura. Il file offset viene utilizzato dalle funzioni di biblioteca standard rewind(), fseek() e ftell(). Funzione rewind(). Di solito, per recuperare in modo sequenziale i dati di un file, un programma incomincia dal suo inizio e legge tutti i dati in modo consecutivo, finché non siano stati trovati quelli desiderati. Durante l’esecuzione di un programma potrebbe essere necessario rielaborare più volte in modo sequenziale i dati di un file ripartendo dal suo inizio. A tale scopo si usa una istruzione del tipo rewind(in_file); che riposiziona all’inizio (ossia al byte 0) del file puntato da in_file il puntatore di posizione del file. In tal modo il prossimo carattere a cui si avrà accesso sarà il primo nel file. Quando si apre un file in modo lettura, viene eseguita automaticamente una rewind(). Il programma seguente usa rewind() per trovare nel file cliente.dat e visualizzare sullo schermo i record nei quali il saldo sia zero o positivo, a seconda della scelta (1 o 2) dell’utente. #include <stdio.h> int main() { int scelta, conto; float saldo; char nome[30]; FILE *in_file; if((in_file=fopen("cliente.dat", "r")) == NULL) printf("Il file non può essere letto\n"); else { printf("Scegli una richiesta\n1 - Elenca i conti con saldo 0\n2 - Elenca i conti con saldo positivo\n3 - Fine del programma\n? "); scanf(“%d", &scelta); while(scelta != 3) { fscanf(in_file, "%d %s %f", &conto, nome, &saldo); switch(scelta) { case 1: printf("Conti con saldo zero:\n"); while(!feof(in_file)) { if(saldo == 0) printf("%-10d %-13s %7.2f\n", conto, nome, saldo); fscanf(in_file, "%d %s %f", &conto, nome, &saldo); } break; case 2: printf("Conti con saldo positivo:\n"); while(!feof(in_file)) { if(saldo > 0) printf("%-10d %-13s %7.2f\n", conto, nome, saldo); fscanf(in_file, "%d %s %f", &conto, nome, &saldo); } break; } rewind(in_file); printf("\n? "); scanf("%d", &scelta); } printf("Fine del programma.\n"); fclose(in_file); } return(0); } Funzione fseek(). La funzione fseek() consente di muoversi in qualsiasi posizione nel file. Per comprenderne il funzionamento, ricordiamo come avviene il riferimento ai dati in un file. Ogni carattere in un file dati è localizzato tramite la sua posizione nel file: il primo nella posizione 0, il secondo nella posizione 1 e così via. La posizione di un carattere è detta anche il suo offset dall’inizio del file. Così, il primo carattere ha offset 0, il secondo offset 1 e così via. La funzione fseek() richiede 3 argomenti: il nome puntatore del file; l’offset, in forma di intero lungo; la posizione a partire dalla quale va calcolato l’offset. La forma generale di fseek() è la seguente: fseek(nome-di-file, offset, origine) I valori dell’argomento origine possono essere 0, 1 o 2, che sono definiti in stdio.h come le costanti di nomi rispettivamente SEEK_SET, SEEK_CUR e SEEK_END. Un offset positivo fa muovere in avanti nel file; uno negativo fa muovere indietro. Ecco alcuni esempi di fseek() (nei quali in_file è il nome del puntatore di file usato quando il file dati è stato aperto): Funzione ftell(). La funzione ftell() restituisce il valore di offset del prossimo carattere che sarà letto o scritto. Ad es., se sono stati già letti 10 caratteri dal file in_file, la chiamata di funzione ftell(in_file); restituisce l’intero lungo 10. Ciò significa che il prossimo carattere che sarà letto è alla posizione 10 byte di offset dall’inizio del file, ossia è l’11° carattere del file. Anche il prototipo di ftell() è contenuto in stdio.h. Il programma seguente mostra l’uso congiunto di fseek() e ftell() per leggere un file in ordine inverso dall’ultimo carattere al primo, e visualizza ogni carattere letto. #include <stdio.h> void main(void) { int ch, n; long int offset, ult, ftell(); FILE *in_file; if((in_file = fopen(“prezzi.dat","r")) == NULL) { printf("\nErrore nell’aprire il file prezzi.dat.\n"); exit(1); } fseek(in_file, 0L, SEEK_END); ult = ftell(in_file); /* salva l’offset dell’ultimo caratt. */ for(offset = 0; offset <= ult; ++offset) { fseek(in_file, -offset, SEEK_END); /* va al caratt. preced. */ ch = getc(in_file); /* riceve il carattere */ switch(ch) { case '\n': printf("LF : "); break; case EOF : printf("EOF: "); break; default : printf("%c : ",ch); break; } } fclose(in_file); } Questo programma va inizialmente all’ultimo carattere nel file, che è quello di fine-file, e salva il suo offset nella variabile ult (dato che ftell() restituisce un intero lungo, ult è stata dichiarata come intero lungo). A partire dalla fine del file, si usa fseek() per posizionare il prossimo crattere da leggere, con riferimento alla fine del file. Via via che ogni carattere è letto, esso è visualizzato e l’offset modificato in modo da accedere al prosimo carattere. File ad accesso casuale Con organizzazione di un file s’intende il modo con cui i dati vi sono memorizzati. Tutti i file usati finora avevano una organizzazione sequenziale, nel senso che i caratteri erano memorizzati sequenzialmente, uno dopo l’altro. Inoltre, abbiamo anche letto il file in modo sequenziale. Il modo in cui si accede ai dati di un file è detto accesso al file. Tuttavia, il fatto che i dati in un file siano memorizzati in modo sequenziale non ci costringe anche ad accedere al file in modo sequenziale. Nell’accesso casuale ogni carattere nel file può essere letto immediatamente, senza che sia necessario leggere prima tutti i caratteri precedenti. I record di un file creati con l’uscita formattata della funzione fprintf() non hanno tutti necessariamente la stessa lunghezza. Invece tutti i record di un file ad accesso casuale hanno la stessa lunghezza, cosicché la posizione ciascuno di essi, relativamente all’inizio del file, può essere calcolata con una funzione della chiave del record. Ciò consente di accedere a ogni record in modo diretto (e quindi più velocemente) senza passare attraverso i precedenti. Per questa ragione i file ad accesso casuale sono particolarmente appropriati per i sistemi per l’elaborazione di transazioni che richiedano un accesso rapido a dati specifici. La figura seguente illustra un modo di implementare un file ad accesso casuale: un file di questo tipo è come un treno merci con molti vagoni, alcuni vuoti e altri con un carico, ognuno dei quali ha la stessa lunghezza. In un file ad accesso casuale i nuovi dati possono essere inseriti senza distruggere quelli già immagazzinati; questi ultimi possono anche essere aggiornati o eliminati, senza che si debba riscrivere l’intero file. Creazione di un file ad accesso casuale (fwrite()) La funzione fwrite() scrive in un file un numero specificato di byte, a partire da quello indicato dal puntatore di posizione del file. La funzione fread() trasferisce un numero specificato di byte dalla posizione indicata dal file offset a un’area di memoria che comincia a un indirizzo specificato. A questo punto, per scrivere un intero, invece di usare fprintf(in_file, “%d”, numero); che può stampare da un minimo di 1 a un massimo di 11 caratteri (10 cifre più il segno), ognuno dei quali richiede un byte di memoria, per un intero di 4 byte possiamo usare: fwrite(&numero, sizeof(int), 1, in_file); che scrive in ogni caso 4 byte* dalla variabile numero al file puntato da in_file. In seguito useremo fread() per leggere 4 di quei byte nella variabile intera numero. *o 2, su sistemi con interi di 2 byte Se, da una parte, fread() e fwrite() leggono e scrivono dei dati come gli interi in un formato con dimensione fissa, anziché variabile, dall’altro i dati che esse gestiscono sono elaborati nel formato “dati grezzi”, ossia in byte di dati, anziché nel formato umanamente intelligibile di printf() e scanf(). Le funzioni fwrite() e fread() sono in grado di leggere e scrivere su disco interi vettori di dati; in effetti il loro terzo argomento è appunto il numero di elementi del vettore che dovranno essere letti dal disco, o che vi dovranno essere scritti. La precedente chiamata della funzione fwrite() scrive su disco un singolo intero, dato che il terzo argomento è 1 (come per scrivere un solo elemento di un vettore). Di solito i programmi per l’elaborazione dei file non scrivono un solo campo all’interno di un file, ma una intera struttura per volta, come mostrato nell’esempio che segue. Esempio. Vogliamo creare un sistema in grado di memorizzare fino a 100 record di lunghezza fissa; ognuno dovrà consistere in: • un numero (intero) di conto, che sarà usato come chiave del record • un cognome • un nome • un saldo. Dovremo scrivere tre distinti programmi, in grado rispettivamente di: 1.creare il file dove saranno memorizzati i vari record 2.aggungere un record nuovo, aggiornarne o cancellarne uno esistente 3.elencare tutti i record in un file di testo formattato per la visualizzazione. Il programma seguente mostra come: • • • • aprire un file ad accesso casuale definire un formato di record usando una struttura, scrivere i dati sul disco e chiudere il file. Esso inizializza i 100 record del file credito.dat con una struttura vuota usando la funzione fwrite(). Ogni struttura vuota contiene 0 per il numero di conto, una “” (stringa vuota) per il cognome e una per il nome, e 0 per il saldo. Il file viene inizializzato in questo modo per creare sul disco lo spazio nel quale sarà memorizzato, e per consentire di determinare se un record contenga dei dati. #include <stdio.h> struct Dati_cli { int num_conto; char cognome[15]; char nome[10]; int saldo; }; int main() { int i; struct Dati_cli vuoto = {0, "", "", 0.0}; FILE *out_file; if((out_file = fopen("credito.dat", "wb")) == NULL) { printf("Impossibile creare il file.\n"); close(1); } else { for(i=1; i<=100; i++) fwrite(&vuoto, sizeof(struct Dati_cli), 1, out_file); fclose(out_file); } return(0); } Scrittura di un file ad accesso casuale Il seguente programma consente di scrivere e di modificare dei record nel file ad accesso casuale credito.dat appena creato. #include <stdio.h> struct Dati_cli { int num_conto; char cognome[15]; char nome[10]; int saldo; }; int main() { struct Dati_cli cliente = {0, "", "", 0.0}; FILE *out_file; if((out_file = fopen("credito.dat", "rb+")) == NULL) { printf("Impossibile aprire il file.\n"); close(1); } else { printf("Scrivi un n° di conto (da 1 a 100, 0 per finire)\n? "); scanf("%d", &cliente.num_conto); while(cliente.num_conto != 0) { printf("Scrivi Cognome, Nome, saldo\n? "); fscanf(stdin, "%s%s%d", cliente.cognome, cliente.nome, &cliente.saldo); fseek(out_file,(cliente.num_conto-1)*sizeof(struct Dati_cli),SEEK_SET); fwrite(&cliente, sizeof(struct Dati_cli), 1, out_file); printf("Scrivi un n° di conto (0 per finire)\n? "); scanf("%d", &cliente.num_conto); } fclose(out_file); } return(0); } Lettura di un file ad accesso casuale Il seguente programma consente di leggere i record memorizzati nel file ad accesso casuale credito.dat appena creato. #include <stdio.h> struct Dati_cli { int num_conto; char cognome[15]; char nome[10]; int saldo; }; int main() { struct Dati_cli cliente = {0, "", "", 0.0}; FILE *out_file; if((out_file = fopen("credito.dat", "rb")) == NULL) { printf("Impossibile aprire il file.\n"); close(1); } else { printf("%-6s %-16s %-11s %10s \n", "Conto","Cognome","Nome","Saldo"); while(!feof(out_file)) { fread(&cliente, sizeof(struct Dati_cli), 1, out_file); if(cliente.num_conto != 0) { printf("%-6d %-16s %-11s %10d \n",cliente.num_conto,cliente.cognome, cliente.nome, cliente.saldo); } } fclose(out_file); } return(0); }