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