A. Veneziani Files di record Caratteristiche dei file di record I file di record si differenziano rispetto a quelli di testo per avere i dati memorizzati in un formato predefinito ben preciso suddiviso in campi di varia lunghezza, ognuno contenente dati dello stesso tipo. Essi sono da considerarsi una specifica categoria di file binari (si ricorda che file di tipo binario sono anche, ad esempio, le immagini e i file eseguibili (.exe)). La definizione del formato interno di un file di record è data dalla struttura utlizzata per definire il record stesso. Infatti, mentre nelle operazioni di lettura / scrittura su files di testo operiamo con variabili, a seconda dei casi, di tipo char, vettore di char o string, ossia variabili appunto, per loro natura, atte a contenere testo o caratteri, sui file di record operiamo con variabili in vario modo strutturate 1. Tali variabili dovranno avere la precipua caratteristica di avere una ampiezza ben definita e quindi costante. In un file di record infatti la scrittura di un record comporta l aumento della dimensione del file di una ben precisa quantità di bytes. Uno schema che rappresenti la memorizzazione in un file di record può essere disegnato come: Alunno Voto Materia Rossi 6 Inglese Bianchi 5 Matematica... L esempio descrive un file di record dove il record è costituito, nel nostro caso, da tre campi: Alunno alfanumerico Voto numerico Materia alfanurico Per tali campi si dovrà definire una ampiezza ben definita, o uno specifico tipo con apiezza definita. Il fatto che il record sia un modulo le cui dimensioni hanno ampiezza fissa e nota semplifica molte operazioni rispetto ai file di testo. Ad esempio è semplice nei file di record effettuare la modifica dei contenuti di un record e la cancellazione di un record (con un eventuale successivo recupero di spazio). Il record di cui sopra potrebbe essere definito con la struttura C++: struct prova char alunno[30]; int voto; char materia[50]; ; Esso ha una ben precisa ampiezza per tutti i campi (30 byte per nome, 4 byte per il voto, 50 byte per la materia), la cui somma definirà l ampiezza di tutto il record 2, numero che comunque sarà sempre fisso per tutti i record. Apertura di un file di record L apertura di un file di record avverrà con il solito metodo open( ), usato in modo analogo ai file di testo, ma in modalità binaria. L oggetto a cui sarà applicato il metodo open sarà come al solito un oggetto della classe fstream, come per i file di testo. Nel campo modalità sarà presente quindi la specifica ios::binary, che indica tale modalità. Un tipo di estensione che può essere propriamente usata per tali file è.dat. 1 Di tipi definiti tramite l istruzione struct 2 a meno di byte di allineamento, spesso presenti per scopi interni al sistema, che possono far aumentare il numero di byte di qualche unità Pagina 1
Quindi alcune modalità comuni di apertura per i file di record sono: ios::in ios::binary Sola lettura ios::out ios::binary Sola scrittura + creazione ios::out ios::app ios::binary ios::in ios::out ios::trunc ios::binary Modalità di aggiunta in coda + creazione Lettura + scrittura + creazione Lettura e scrittura di record Quando si opera su files di record per legere e scrivere dovranno essere utilizzati due nuovi metodi specifici per essi. Il metodo read ha due parametri ed è conformato come: <oggetto fstream>.read((char*) &<var. strutturata>, <dimensione del record>); L indicazione (char*) ha lo scopo di effettuare un cast di puntatori, tale per cui il puntatore alla variabile strutturata, viene convertito in un puntatore a carattere (o meglio indicante il punto di inizio di una serie di caratteri). Il primo parametro quindi fornisce l indirizzo di inizio della struttura in memoria. Il secondo la sua ampiezza, permettendo quindi di avere dati utili per svolgere l operazione di lettura. Analoga sintassi si ha per la scrittura di un record, solo che qui si utilizzerà il metodo write: <oggetto fstream>.write((char*) &<var. strutturata>, <dimensione del record>); Si noti che la sintassi è analoga anche perchè i dati passano dal programma al file tramite parametri by reference (puntatori), quindi è un passaggio atto ad operare sia dal programma verso il metodo e quindi il file, sia dal metodo verso il programma. Esempi concreti di sintassi potrebbero essere: struct rec char nome[50]; int n; ;... int main() fstream fs; rec r; fs.write((char*) &r, sizeof(rec)); Funzione sizeof( ) Come mostrato negli esempi qui sopra il problema di individuare l ampiezza di una struttura può essere risolto tramite l uso della funzione sizeof(.), che attende come parametro il nome di una variabile (semplice o strutturata) o alternativamente un tipo di dato e come uscita rende la sua ampiezza come numero di byte. Ciò permette ai metodi read e write di conoscere quanti byte vadano letti o scritti. Pagina 2
Metodo eof() Anche nei file di record non è noto a priori quale sia il numero di record che andranno letti dal file. Data questa situazione, analogamente a quanto fatto nei file di testo, si controlla ad ogni lettura se i record siano stati letti tutti. In tal caso eof( ) sarà true. La lettura avviene, come al solito, tramite un ciclo. Un esempio di un tale ciclo di lettura di records potrà essere: while (! fs.eof()) if (! fs.eof()) cout << r.nome << " " << r.n << endl; ove il record letto viene trasferito, con una sola operazione, nella var. strutturata r, per poi essere utilizzato. Riposizionamento del punto di lettura - metodo seekg( ) Dopo la scrittura di un certo numero di records se il programma và a rileggere il file si scoprirà che esso non vede i suoi contenuti. Ciò è dovuto al fatto che anche il punto di lettura è in fondo al file. In tal caso bisogna operare per riportarlo in cima ad esso. Ciò può essere fatto con: fs.seekg(0); o in alternativa: fs.seek(0, ios::beg); Una volta effettuata questa operazione tutti i record saranno leggibili operando con altre opportune operazioni. In pratica il punto di lettura viene spostato all inizio del primo record, ossia all inizio del file di record. Lunghezza di un file Per conoscere la lunghezza di un file sia di testo che di record basterà ricorrere a questo codice: fs.seekg(0, ios::end); ampiezza = fs.tellg(); il valore ampiezza (int) renderà l ampiezza del file in di byte. Infatti: fs.seekg(0, ios::end); indica al cursore di spostarsi con il cursore alla fine del file (scostamento 0 byte, dalla fine del file - ios::end 3 ). L istruzione ampiezza = fs.tellg(); serve a indicare di quanti byte sia ora scostato il punto di lettura dall inizio del file. Le due operazioni combinate danno l ampiezza. Metodo clear() In alcuni casi (tipicamente dopo aver raggiunto uno stato di eof), l oggetto fstream (fs) deve essere resettato ad uno stato non di errore, per poter operare di nuovo. Questo viene fatto quando necessario tramite il metodo clear: fs.clear(); 3 Si noti l uso negli ultimi esempi delle due costanti ios::beg e ios::end, rappresentanti l inizio e la fine del file. Pagina 3
Posizionamento sul record n Potendo operare in modo modulare su un qualunque record è ovviamente importante essere capaci di posizionarsi correttamente sul record voluto. Ciò viene fatto tramite l istruzione: fs.seekg((n 1) * sizeof(rec)); questa istruzione permette di raggiungere direttamente il record n 4, evitando la lettura dei precedenti. Infatti se si pensa di saltare al primo record, si avrà che dato che (n 1) = (1 1) da 0, non si avrà spostamento da effettuare. Ciò è corretto essendo l inizio del record 1 all inizio del file. Per i record successivi il numero di spostamenti pari all ampiezza del record (valore ricavato tramite sizeof(rec) ), sarà anche in questo caso corretto. Ad esempio per raggiungere il record 3, dovrò spostarmi all inizio del record 3, quindi saltare due record, cosa che infatti viene indicata dalla istruzione precedente: (n 1) = (3 1) = 2 * sizeof(rec), ossia una spostamento pari all ampiezza di due record. Modifica di un record Come si è già detto precedentemente l operazione di modifica di un record è una operazione piuttosto comune e comporta: a) il posizionamento sul record da modificare b) la successiva lettura del contenuto c) la modifica dei valori d) un riposizionamento sul record n e) ed infine una scrittura dello stesso record, con sua sovrascrittura su disco in codice C++ le precedenti operazioni divengono: fs.seekg((n - 1) * sizeof(rec)); // posizionamento // lettura valori r.n = r.n * 3; // modifica - (si supponga che la modifica sia triplicare il valore numerico) fs.seekg((n - 1) * sizeof(rec)); // ri-posizionamento fs.write((char*) &r, sizeof(rec)); // scrittura valori modificati Il motivo del secondo posizionamento (indicato come ri-posizionamento) è che il cursore come al solito dopo una lettura avanza della quantità di byte letti (in questo caso di un intero record). Proprio per questo per posizionarsi di nuovo sul record voluto e non scrivere le modifiche sul successivo bisogna effettuare un ri-posizionamento che viene compiuto semplicemente eseguendo di nuovo una istruzione di posizionamento identica alla precedente. Cancellazione di un record Nel caso si vada a cancellare un record di solito si ricorre alla cancellazione logica. La cancellazione fisica del record infatti presuporrebbe il ricompattamento di tutti i record, ossia la riscrittura di parte del file, operazione assai onerosa. Per permettere la cancellazione logica, bisogna prevedere all interno del record un apposito campo che indichi o no se il record sia valido (ossia se i suoi dati siano o no cancellati). Talo valore sarà quindi di solito un campo booleano apposito (di solito inserito nell ultima posizione della struttura). Nel record da noi preso come esempio operativo a pagina 1, dovremo quindi mdoficare il record come: struct rec char nome[50]; int n; 4 I record di norma sono numerati partendo dal numero 1. Pagina 4
bool cancellato; ; In tal caso: una operazione di scrittura verrà sempre effettuata con il valore booleano a false, mentre una operazione di cancellazione su un certo record porterà tale campo a true, ossia ad assumere un valore che indichi il fatto che il record non è più valido. In questo modo le successive operazioni se contenenti opportuno codice potranno evitare di considerare i record che risultano come cancellati, controllando il valore del campo apposito. C è anche da notare che successive operazioni di inserimento dovrebbero recuperare lo spazio relativo a records liberi e non andare ad impegnare ulteriore spazio disco inutilmente. Viceversa il file di record alla lunga aumenterebbe la sua dimensione continuamente. Un frammento di codice che effettui la cancellazione selettiva e di conseguenza non ristampi il record cancellato, è riportato qui sotto: cout << "Fornisci il record da cancellare: "; cin >> n; fs.seekg((n - 1) * sizeof(rec)); r.cancellato = true; fs.seekg((n - 1) * sizeof(rec)); fs.write((char*) &r, sizeof(rec)); fs.clear(); fs.seekg(0); while (! fs.eof()) if (! fs.eof()) if (! r.cancellato) cout << r.nome << " " << r.n << endl; Pagina 5