ARCHIVI E FILE
Transcript
ARCHIVI E FILE
Istituto Angioy Informatica ARCHIVI E FILE Prof. Ciaschetti ARCHIVI Un archivio è un insieme di registrazioni (record) memorizzate in modo persistente, su un qualunque tipo di supporto. La caratteristica principale di un archivio è che le diverse registrazioni hanno tutte la stessa struttura. Sono archivi, ad esempio, una rubrica telefonica (ogni nominativo corrisponde a una registrazione, e per ogni registrazione possiamo avere il nome, il numero di telefono di casa, il numero di cellulare, ecc.), il registro delle presenze della classe (ogni record è un giorno che riporta le materie, le firme dei docenti, gli assenti, le giustificazioni, le note), l‟archivio dei clienti di un‟azienda (per ogni cliente, si possono avere il nominativo, la partita iva, l‟indirizzo, ecc.), l‟archivio degli iscritti a un certo sito web (ogni registrazione in questo caso memorizza il nome utente e la password). Il supporto su cui è memorizzato un archivio può essere di diverso tipo: nel caso del registro di classe, è di tipo cartaceo; la rubrica telefonica può essere memorizzata in forma cartacea, oppure in forma elettronica (come sul cellulare), ecc. Un archivio memorizzato in modo elettronico è realizzato tramite file. Una caratteristica importante che distingue gli archivi è la modalità di accesso, che può essere sequenziale o casuale (si parla in questo ultimo caso di archivi ad accesso diretto). Per accesso sequenziale si intende che si accede all‟inizio dell‟archivio, e poi bisogna scorrerlo in avanti per arrivare all‟informazione desiderata, un po‟ come il nastro di una videocassetta. Per accesso casuale (diretto), invece, si intende la possibilità di accedere direttamente all‟informazione desiderata, come succede ad esempio nei lettori cd, dove possiamo scegliere il numero della traccia e sentire direttamente quella, senza dover scorrere prima tutte le tracce precedenti. FILE Un file è una sequenza di bit memorizzata in modo permanente su memoria di massa (dispositivi magnetici come hard disk, floppy disk, pen drive, ecc., oppure dispositivi ottici come cd, dvd, ecc.). Si noti la caratteristica della persistenza in comune con gli archivi. Un file può costituire la memorizzazione di un archivio, ma non è detto in generale. Ad esempio, un file .cpp non è un archivio, ma un file di testo in cui è scritto un programma in C/C++, oppure un file .exe è un file binario eseguibile, un file .doc è un documento word, ecc. In generale, possiamo dire allora che ogni archivio elettronico è anche un file, ma non è vero il viceversa, ci sono file che non sono archivi. In generale, possiamo distinguere due tipi di file: file di testo e file binari. Entrambi sono, come tutti i file, sequenze di bit, ma mentre nei file di testo ogni sottosequenza di 8 bit rappresenta un carattere in codifica ASCII, nei file binari i bit possono rappresentare altri tipi di dato (es. interi, reali, ecc.). I file di testo possono essere visti e compresi dall‟uomo, mentre i file binari sono invece piuttosto incomprensibili all‟occhio umano. Ma come si fa a riconoscere un file di testo da un file binario? Non esiste un modo, dipende da come il file è stato scritto. In generale, i file di testo hanno sempre estensione .txt, ma possono anche avere altre estensioni, come ad esempio .cpp, .pas, e in generale, una qualunque estensione, tranne .exe, riservata ai file eseguibili. Anche i file binari possono avere una qualunque estensione, tranne .txt. Quindi potremmo utilizzare, ad esempio, le estensioni .dat, .ang (angioy), ecc. C‟è una certa differenza nell‟uso dei file binari e dei file di testo. Facciamo un esempio: se andiamo a memorizzare il dato 125 su un file di testo, esso conterrà tre byte, uno per la rappresentazione del carattere 1, uno per la rappresentazione del carattere 2, e uno per la rappresentazione del carattere 5, in codifica ASCII. Se invece memorizziamo il dato in un file binario, quest‟ultimo conterrà tanti byte quanti ne servono per rappresentare il numero intero 125 (in questo caso, il dato potrebbe essere di tipo short int e quindi richiedere solo 2 byte, oppure int e richiedere 4 byte). Prima di parlare più in particolare delle funzioni che il linguaggio C ci mette a disposizione per leggere e scrivere su file, e di come realizzare un archivio su un file, terminiamo la nostra discussione sui file dicendo che per evitare accessi multipli in scrittura su un file, che potrebbero causare effetti indesiderati, occorre sempre aprire il file prima di poterlo usare, e poi chiudere il file alla fine delle nostre operazioni. Si pensi, ad esempio, a due programmi che cerchino di scrivere contemporaneamente la stringa “ciao” e la stringa “pippo” su uno stesso file: si potrebbe avere come effetto, ad esempio, la scrittura della stringa “cpipiaopo” completamente priva di significato L‟apertura e la chiusura di un file creano un meccanismo semaforico: l‟apertura (in scrittura) di un file da parte di un programma rende indisponibile il file (in scrittura) a tutti gli altri programmi/utenti, evitando così gli effetti indesiderati. Solo al momento della chiusura del file, esso è reso nuovamente disponibile dal sistema operativo. L‟apertura e la chiusura di un file devono sempre essere fatte anche se si vuole solo leggere dal file, ma in questo caso non ci sarà nessun meccanismo semaforico a bloccare altri eventuali accessi al file. L‟apertura e la chiusura di un file sono operazioni necessarie sia quando si lavora con file di testo sia quando si lavora con file binari. Per utilizzare i file in linguaggio C occorre dichiarare una variabile di tipo puntatore a file, come ad esempio FILE *fp; /* dichiara una variabile fp di tipo puntatore a file */ Nella variabile fp verrà memorizzato l‟indirizzo del file al momento della sua apertura. Si noti che in questo caso non si tratta di un indirizzo nella memoria RAM, ma su memoria di massa, quindi costituito da numero di cilindro, traccia e settore del dispositivo magnetico o ottico di memorizzazione. Non importa, in questo contesto, sapere esattamente come è costituito l‟indirizzo fisico del file, l‟importante è che memorizziamo questo indirizzo in una variabile di tipo puntatore a file per poterlo poi utilizzare nelle operazioni di lettura/scrittura. Infatti, nel nostro programma, non occorrerà scrivere ogni volta il percorso e il nome del file su cui vogliamo leggere o scrivere, ma ci basterà utilizzare il suo puntatore, come nel seguente esempio: 2 FILE *fp; fp = fopen("pippo.txt", "w"); fprintf(fp, "ciao mondo"); Nella variabile fp di tipo puntatore a file viene memorizzato l‟indirizzo del file di testo pippo.txt aperto in modalità scrittura, e poi viene scritta sul file la stringa “ciao mondo”. Al momento dell‟apertura di un file la testina di lettura/scrittura si posiziona all‟inizio del file, e scorre in avanti ad ogni operazione di un numero di byte pari al numero di byte letti o scritti nell‟operazione eseguita. Quindi, ad esempio, se il file di testo pippo.txt contiene la parola “ciao”, la sequenza di operazioni FILE *fp; char ch; fp = fopen("pippo.txt", "r"); ch = getc(fp); printf("%c", ch); ch = getc(fp); printf("%c", ch); ch = getc(fp); printf("%c", ch); produce in output su video la sequenza “cia”. Infatti, la funzione getc legge un carattere dal file specificato tramite il suo indirizzo (memorizzato nella variabile fp), e la printf lo visualizza sul monitor. Vedremo più avanti, in dettaglio, tutte le funzioni per leggere e scrivere su file. Per ora, si noti che da questo esempio si evince che la testina di lettura/scrittura, posizionata all‟inizio del file al momento della sua apertura, si sposta in avanti dopo ogni operazione di lettura. Di quanto si sposta in avanti? Esattamente del numero di byte letti! La getc legge un carattere, cioè un byte (in codifica ASCII), quindi si sposta in avanti di un byte, cioè al prossimo carattere. ARCHIVI E FILE Quando usiamo i file per memorizzare archivi, possiamo notare che: i file di testo sono sempre ad accesso sequenziale, i file binari permettono sia l‟accesso sequenziale che l‟accesso casuale. La motivazione sta nel fatto che mentre nei file binari possiamo “contare” il numero di byte di ogni registrazione, e quindi dire alla testina di lettura/scrittura di quanti byte spostarsi per accedere all‟informazione desiderata, nei file di testo questo non è possibile. Facciamo un esempio: Supponiamo che ogni registrazione rappresenti un punto del piano cartesiano, con due campi che indicano la coordinata x e la coordinata y del punto. La struttura che definisce il record potrebbe essere la seguente: struct punto { float x; float y; }; typedef struct punto point; Supponiamo ora di voler creare un archivio con i seguenti punti: 3 point punto1, punto2, punto3; punto1.x = 3.534; punto1.y = 2.34; punto2.x = 7.235; punto2.y = 4.1; punto3.x = -1.9836; punto3.y = 3.877; utilizzando un file binario, andremo a rappresentare i dati esattamente nella loro forma binaria, e cioè, essendo il tipo di dato float su 4 byte, 8 byte per il primo punto (4 per la coordinata x, 4 per la coordinata y), 8 byte per il secondo, e 8 byte per il terzo. Complessivamente, il nostro file avrà una dimensione pari a 24 byte. Potremmo, in questo caso, effettuare un accesso diretto all‟archivio, poiché sappiamo esattamente quanti byte dobbiamo saltare per arrivare alla registrazione che ci interessa. Ad esempio, potremmo voler accedere al terzo record, e possiamo farlo dicendo alla testina di spostarsi, dall‟inizio del file, esattamente di 16 byte. Se invece utilizziamo un file di testo per memorizzare il nostro archivio, il primo record richiederà 5 byte per la coordinata x (i caratteri „3‟, „.‟, „5‟, „3‟ e „4‟) e 4 byte per la coordinata y. Analogamente, il secondo record richiederà 5+3=8 byte, e il terzo record 7+5=12 byte. Non conoscendo a priori la dimensione di ogni record, siamo impossibilitati a indicare di quanti byte spostarci per arrivare alla registrazione che ci interessa. Vedremo tra poco che esistono, in C, alcune funzioni per scrivere e leggere dati su/da file di testo, e altre funzioni per scrivere e leggere su/da file binari. Le prime si dicono funzioni di input/output formattato (esattamente come la printf e la scanf che conosciamo già). Significa che se noi diciamo di scrivere su file un dato, ad esempio di tipo float fprintf(fp, "%f", punto1.x); la funzione fprintf eseguirà una conversione in stringa del dato di tipo float, in modo che noi umani la possiamo comprendere, e trasformerà la sequenza di 32 bit che rappresenta il numero 3.534 in binario, ossia 00100000100000000000110111001110 nella stringa “3.534”. Questa operazione è del tutto trasparente all‟utente, che vedrà apparire su file di testo il numero credendo che anche dentro al computer è rappresentato così, ma non è vero. In RAM, il dato punto1.x occupa 4 byte, mentre sul file ne occupa 5, come abbiamo visto. Ci si potrebbe, a questo punto, domandare se è possibile realizzare un archivio ad accesso diretto con un file di testo. La risposta, sebbene sembri negativa, è positiva. Si, è possibile. Bisogna però stare attenti a usare sempre lo stesso numero di caratteri per ogni campo del record, in modo da conoscere a priori la dimensione dell‟intero record. Nell‟esempio precedente dei punti sul piano, potremmo ad esempio stabilire che usiamo sempre 3 caratteri per la parte intera e tre caratteri per la parte decimale, e in questo modo sia la coordinata x che la coordinata y possono essere espresse ognuna su 7 caratteri, per cui un record (un punto) sul nostro file di testo occupa sempre 7+7=14 caratteri. Tuttavia, occorre poi istruire chi userà il nostro programma di procedere sempre con questa convenzione quando inserirà i dati, e dobbiamo prevedere dei meccanismi di controllo in caso di input errati. Se non inseriamo nel nostro programma nessun controllo sull‟input, cioè se permettiamo all‟utente di inserire dati con altri formati (ad esempio, 4 cifre decimali anziché 3), si renderà poi l‟accesso diretto al nostro archivio impossibile. Un'altra domanda che si pone, solitamente, è la seguente: è meglio usare archivi ad accesso sequenziale o archivi ad accesso diretto? La risposta è nel tipo di operazioni che devono essere eseguite sull‟archivio. Se l‟archivio ci serve solo per fare un backup dei dati, che poi difficilmente andremo a rileggere, può andare benissimo un archivio ad accesso sequenziale (ed infatti, per questo tipo di archivi sono spesso usati file di testo). Se invece occorre effettuare spesso operazioni 4 di lettura di dati dall‟archivio, e in modo disordinato (cioè non sequenziale), conviene utilizzare un archivio ad accesso diretto per evitare di aspettare ogni volta che vengano letti tutti i record che precedono quello che ci interessa. ARCHIVI, FILE E TABELLE Ancora una considerazione sugli archivi. Essendo un archivio un insieme di registrazioni, la struttura che meglio si adatta a caricare in RAM un archivio o parti di esso è la tabella, cioè l‟array di record. Se ad esempio, volessimo caricare in RAM i primi dieci record di un archivio studenti, potremmo utilizzare la seguente struttura: struct studente { char nome[20]; char cognome[20]; char classe[5]; int eta; }; typedef struct studente stud; stud studenti_5d[25]; /* array di 25 elementi di tipo stud */ FILE *fp = fopen("studenti.ang","r"); fread(studenti_5d,sizeof(stud),10,fp); /* legge 10 record di tipo stud da file binario e li memorizza nelle prime 10 posizioni della tabella studenti_5d */ Ancora qualche altra considerazione sull‟utilizzo degli archivi. Poiché il tempo di accesso alla memoria secondaria è molto più lento del tempo di accesso in RAM (circa 10-3 secondi, contro circa 10-6, quindi mille volte più lento), sarebbe opportuno cercare di minimizzare il numero di accessi a memoria secondaria, e lavorare il più possibile con la RAM. Come vedremo successivamente, ci sono funzioni che permettono di leggere (e scrivere) da file più di un dato alla volta, e anche più di una registrazione alla volta. Se, ad esempio, dobbiamo cercare nell‟archivio CLIENTI il cliente “Pippo Casu” e non ne conosciamo la posizione, l‟unico modo possibile è quello di scandire tutto il nostro archivio caricando in RAM una registrazione alla volta, e confrontare i campi nome e cognome del record che sta in memoria con le stringhe “Pippo” e “Casu”. Se il nostro archivio contiene 10000 clienti, nel caso più sfortunato dobbiamo effettuare 10000 accessi a memoria secondaria. Potremmo velocizzare le nostre operazioni caricando in memoria da file, ad esempio, 100 record alla volta, in una tabella, ed eseguire i confronti sull‟intera tabella (i confronti dobbiamo farli sempre record per record, ovvio, ma questa volta accedendo in RAM, quindi molto più velocemente). In questo modo, il numero degli accessi a memoria secondaria è limitato, nel caso peggiore, a 100. Questo modo di procedere, però, per quanto efficiente, risulta piuttosto difficile da gestire (non impossibile!), quindi in generale si preferisce caricare da file in RAM un record alla volta. Analizziamo ora in dettaglio le diverse funzioni del linguaggio C per leggere e scrivere su file: 5 ------------------------------------------APERTURA DI UN FILE: FOPEN ------------------------------------------Apre un file. Restituisce un puntatore a file e se si verifica un errore restituisce NULL. Sintassi: fopen("nomefile.estensione","modalita"); Modalità r = lettura, w = scrittura, a = append, r+ = lettura/scrittura. La modalità r richiede che il file esista, altrimenti dà errore. La modalità w riscrive il file da capo, e ne crea uno nuovo nel caso in cui non esiste. La modalità a continua a scrivere un file mantenendo il suo contenuto precedente, e crea un nuovo file nel caso in cui non esiste. La modalità r+ sarebbe da evitare, poiché l‟alternanza di operazioni di lettura/scrittura potrebbe essere difficilmente gestibile. E‟ utile per effettuare modifiche sul file, ma occorre stare attenti. esempio: FILE *fp = fopen("pippo.txt","r"); per fare le cose fatte bene... if((fp = fopen("..","..")) == NULL) { printf("errore! file non trovato"); } else... -------------------------------------------CHIUSURA DI UN FILE: FCLOSE -------------------------------------------Chiude un file precedentemente aperto. esempio: fclose(fp); /*fp puntatore a file già aperto in precedenza */ --------------------------------------------------LETTURA DI UN CARATTERE: GETC --------------------------------------------------Legge un carattere da un file di testo aperto in modalità lettura. Restituisce il carattere letto oppure EOF se alla fine del file. esempio: char ch; /*variabile di tipo carattere*/ ch = getc(fp); -----------------------------------------------------SCRITTURA DI UN CARATTERE: PUTC -----------------------------------------------------6 Scrive un carattere su file di testo. Se l'operazione ha avuto successo, restituisce il carattere scritto altrimenti EOF. esempio: char ch = 'a'; /* assegna un valore alla variabile */ putc(ch,fp); /* scrive il carattere 'a' sul file */ esempio: putc('\n',fp); /* va a capo sul file (inizia una nuova riga) */ -----------------------------FINE DI UN FILE: EOF -----------------------------Determina il raggiungimento della fine del file. EOF è una costante del linguaggio C. esempio: while(getc(fp) != EOF) /* legge un carattere fino alla fine del file */ { ........ } ---------------------------------FINE DI UN FILE: FEOF ---------------------------------Determina il raggiungimento della fine del file. Restituisce 1 se il file è terminato, 0 altrimenti. esempio: while(!feof(fp)) /* legge un carattere fino alla fine del file */ { ........ } ----------------------------------------------------LETTURA DI FILE DI TESTO: FSCANF ----------------------------------------------------Legge un dato formattato da un file di testo e lo mette in una variabile. Restituisce EOF se il file è terminato. Sintassi: fscanf(filepointer,"...",...); esempio: int t; fscanf(fp,"%d",&t); /* legge dal file un intero e lo mette nella variabile t; esempio: 7 if(fscanf(fp,"%d",&t) == EOF) printf("errore"); -------------------------------------------------------SCRITTURA DI FILE DI TESTO: FPRINTF -------------------------------------------------------Scrive su un file di testo un dato formattato. Sintassi: fprintf(filepointer,"...",...); esempio: float t = 1.5; fprintf("fp,"%f",t); esempio: fprintf("fp,"ciao mondo"); esempio: fprintf("fp,"\n”); esempio: fprintf("fp,"%d %d", 3, 8); ---------------------------------------------LETTURA DI FILE BINARI: FREAD ---------------------------------------------Legge dati non formattati da un file binario. Restituisce il numero di elementi letti. Sintassi: fread(address, numero_byte, numero_elementi, filepointer); Il parametro address rappresenta l‟indirizzo di memoria dove verranno caricati i dati letti. Il parametro numero_byte indica il numero di byte di ogni blocco letto (in genere, un blocco equivale a un record). Il parametro numero_elementi indica il numero di blocchi da leggere. Il parametro filepointer indica da quale file eseguire la lettura. esempio: /* legge un record di tipo persona e memorizza nel puntatore p l’indirizzo della zona di memoria dove è stato messo il record */ struct persona *p; p = (struct persona *)malloc(sizeof(struct persona)); fread(p, sizeof(struct persona), 1, fp); esempio: /* legge un record di tipo persona e lo memorizza nella variabile unapersona */ struct persona unapersona; fread(&unapersona, sizeof(struct persona), 1, fp); 8 esempio: /* legge 10 record di tipo persona e li memorizza persone a partire dalla prima posizione */ struct persona persone[10]; fread(&persone[0], sizeof(struct persona), 10, fp); nell'array esempio: /* legge 10 record di tipo persona e li memorizza nell'array persone a partire dalla prima posizione */ struct persona persone[10]; fread(persone, sizeof(struct persona), 10, fp); /* si ricordi che il nome di un array è il puntatore al suo primo elemento */ esempio: /* legge 10 record di tipo persona e li memorizza persone a partire dalla quinta posizione*/ struct persona persone[10]; fread(&persone[4], sizeof(struct persona), 10, fp); nell'array esempio: /* legge un record di tipo studente, lo inserisce nella tabella studenti_5d in prima posizione, e visualizza il numero di byte letti */ /* si fa riferimento in questo esempio alla dichiarazione del record studente fatta all’inizio della dispensa */ int quanti = fread(studenti_5d, sizeof(stud), 1, fp); printf("%d", quanti); ----------------------------------------------------SCRITTURA DI FILE BINARI: FWRITE ----------------------------------------------------Scrive su un file binario un dato non formattato. Sintassi: fwrite(address,numero_byte,numero_elementi,filepointer); I parametri hanno lo stesso significato della fread. esempio: /* scrive il record puntato da p su file */ struct persona *p; p = (struct persona *)malloc(sizeof(struct persona)); p->nome = "pippo"; p->cognome = "casu"; fwrite(p, sizeof(struct persona), 1, fp); esempio: /* scrive il record unapersona su file */ 9 struct persona unapersona; unapersona.nome = "pippo"; unapersona.cognome = "casu"; fwrite(&unapersona, sizeof(struct persona), 1, fp); esempio: /* scrive i primi 10 record dell'array persone su file */ /* si suppone l'array già contenente i dati */ struct persona persone[20]; ... fwrite(persone, sizeof(struct persona), 10, fp); esempio: /* scrive i record dal decimo al ventesimo dell'array persone su file */ /* si suppone l'array già contenente i dati */ struct persona persone[20]; ... fwrite(&persone[10],sizeof(struct persona),10,fp); ------------------------------------POSIZIONAMENTO: FSEEK ------------------------------------Stabilisce la nuova posizione del prossimo byte da leggere o scrivere. Sintassi: fseek(filepointer, offeset, base); La nuova posizione della testina viene indicata da due parametri: una base che puo' essere l'inizio del file (SEEK_SET), la posizione corrente (SEEK_CUR) oppure la fine del file (SEEK_END), ed uno scostamento cioè la distanza dalla base espressa in numero di byte, rappresentato dal parametro offset. Il significato dell‟istruzione è il seguente: muovi la testina di offset byte a partire da base sul file filepointer. Restituisce -1 in caso di errore, 0 se tutto OK. La funzione fseek è di fondamentale importanza per la realizzazione di archivi ad accesso diretto. esempio: /* lettura del dodicesimo carattere di un file di testo */ int c; FILE *fp = fopen("prova.txt","r") fseek(fp, 11, SEEK_SET); c = getc(fp); esempio: /* lettura del terzo record in archivio su file binario */ struct persona p; ... FILE *fp = fopen("clienti.dat", "r") ) fseek(fp, 2*sizeof(persona), SEEK_SET); fread(&p, sizeof(struct persona), 1, fp); esempio: 10 /* lettura dell’ultimo carattere di un file di testo */ int c; if ( !fp = fopen("prova.txt", "r") ) exit(); /* il programma termina se l’apertura del file non va a buon fine */ else { fseek(fp, -8, SEEK_END) /* torna indietro di 8 caratteri */ c = getc(fp); … } esempio: /* errore di spostamento */ /* supponiamo che il nostro file contiene solo 100 byte */ if (fseek(fp, 102, SEEK_SET) == -1) printf("spostamento errato!") 11