Paolo Guccini

Impossibile non é per sempre

UNA CLASSE PER GESTIRE
I RECORD A LUNGHEZZA VARIABILE

I sorgenti sono disponibili per il download.

La classe C++ presentata oltre che come ottimo strumento didattico potrà essere ampliata dal lettore per la creazione di un completo sistema di manipolazione e gestione file

Qualunque programmatore ha dovuto prima o poi gestire i file. Chi sviluppa in Basic dispone delle istruzioni PRINT# ed INPUT# che costituiscono un semplice ma efficacissimo metodo per la lettura e scrittura dei record a lunghezza variabile.
Chi utilizza il C/C++ dispone di librerie di I/O che purtroppo sono di tipo character oriented e conseguentemente non sono in grado di offrire funzioni native per la gestione dei record a lunghezza variabile.
In questo articolo verrà descritta una classe in C++ che consente la gestione dei record a lunghezza variabile (d’ora in poi, VRL per brevità, dall’inglese Variable Record Lenght). Chi non programma in C++ non si spaventi: questa classe è nata in C come una serie di funzioni e solo successivamente è stata convertita in classe C++; perciò non è difficile ritornare alle origini, anche se probabilmente è consigliabile avvicinarsi al C++ che, come ricorderete, è pienamente compatibile con il C standard.
In tempi successivi verranno pubblicate altre due classi: una servirà per la gestione dei file a lunghezza costante; l’altra verrà impiegata come metodo per accedere alle due classi precedenti, costituendo di fatto un’interfaccia standard per il programma per entrambi i tipi di file. Vedrete che saranno molto interessanti.


File con record a lunghezza variabile

In questo articolo verranno spiegate le caratteristiche generali di questo tipo di file nonché delle principali teorie e tecniche di gestione. I File con record a lunghezza variabile hanno le seguenti caratteristiche formali:

  • Ogni campo non numerico viene racchiuso fra delimitatori;
  • Fra i vari campi si trova un carattere separatore; Ogni record è terminato con uno o più caratteri; La fine del file è generalmente dichiarata da uno specifico carattere.
  • Non viene prestabilita la dimensione di ogni campo, ma varia di record in record.

Esiste uno standard nelle dichiarazioni delle caratteristiche sopra riportate: le doppie virgolette (carattere 34, hex 22) per racchiudere i campi non numerici, la virgola (carattere 44, hex 2C) per separare i campi, la coppia Carriage Retum e Line Feed (caratteri 13,10; hex OD, OA) per terminare il record, il carattere Ctrl-Z (26, lA) per la fine del file.
Ma non è una regola ferrea: capita spesso di trovare file senza i delimitatori per i campi non numerici, oppure che il separatore dei campi sia il punto e virgola o la tabulazione, od altro ancora. Un esempio di tipico record a lunghezza variabile può essere il seguente:

"Fabio Rossi" ,"Via Zucchini, 8" , 40124 , "Bologna" CR LF "Silvia Semeram" , "Via Maivoni 1" ,20132 ,"Milano" CR LF


(CR LF in fondo al record rappresentano il carattere Carriage Retum ed il carattere Line Feed)
Si riconoscono facilmente i quattro campi, due alfanumerici, uno numerico ed un altro alfanumerico, separati dalla virgola e con la coppia CR LF come terminatore record.
Essi assumono nei vari record lunghezze diverse: questa caratteristica consente di disporre di uno strumento molto flessibile ma anche di difficile gestione; non potendo sapere a priori la lunghezza che assumerà un determinato campo, è necessario comportarsi di conseguenza.
La variabilità della lunghezza del campo causa anche la variabilità della lunghezza del record.
Ciò consente solo un accesso sequenziale ai record in quanto non è possibile stabilire la posizione dell’n-esimo record all’interno del file.
Altra difficoltà di gestione si incontra quando si deve cancellare, inserire o modificare un record: queste operazioni si risolvono ricorrendo a due tecniche; la prima è quella utilizzata dagli editor: caricare nella memoria dinamica tutti i record e gestirli con una linked-list; così, sebbene l’accesso sarebbe ancora sequenziale saltando di elemento in elemento sino a raggiungere quello desiderato, permetterebbe di eseguire modifiche sul file con grande velocità ed estrema semplicità in quanto ogni operazione risulte rebbe ridotta a un semplice aggiornamento dei dati nella memoria dinamica e del relativo elemento della linked-list.
Per comodità di chi non abbia ancora familiarizzato con le linked list, è utile una succinta descrizione; sono costituite da una struttura di almeno due dati: il valore vero e proprio (nel nostro caso è il record che viene allocato nella memoria dinamica oppure puntatore ad esso), e un puntatore alla successiva struttura; quest’ultimo consente di conoscere l’indirizzo della successiva struttura e conseguentemente del prossimo record. Esempio tipico di una struttura per linked-List può essere il seguente:

struct LnkList {     char * record ;     struct LnkList * successivaLnkList ; } ;

Torniamo alla gestione dei file. Più in dettaglio, le operazioni di modifica del file possono essere così esemplificate: l’inserire un nuovo record dopo l’n-esimo, comporta una new o malloc() per memorizzare la nuova struttura e il record, prendere dall’elemento n il puntatore alla struttura successiva e memorizzarlo nell’elemento appena creato affinché quest’ultimo punti al record n+1, poi modificare il puntatore di n affinché ora punti alla nuova struttura anziché a n+ 1.
La cancellazione dell’n-esimo record comporta il sostituire il puntatore dell’n-1 esimo elemento con il valore del puntatore di n affinché ora punti a n+ 1 (invece di n che deve essere cancellato), ed eseguire una delete o free() per eliminare il record e relativa struttura (di n).
Per modificare un record è ancora più semplice: basta cancellare e ricreare l’elemento della linked list con il nuovo record, modificare il puntatore della struttura n-1 affinché punti al nuovo indirizzo di n ed avere cura che il nuovo elemento continui a puntare sempre all’n+l esimo record.
L’accodamento di un record è concettualmente identico all’inserimento dopo l’ultimo record.
Questo sistema basato sul caricare in RAM l’intero file presenta varie controindicazioni che bisogna tenere presenti affinché i programmi non ne risentano negativamente; le principali consistono nel non sapere a priori la dimensione dei file da trattare e che esistono ancora molti computer che dispongono di un solo megabyte di RAM o poco più.
La seconda tecnica per modificare un file di tipo testo consiste nel copiare su un file di appoggio tutti i record precedenti a quello che si deve modificare, scrivere su questo file il record modificato (o non scrivere nulla nel caso l’operazione fosse la cancellazione del record) e accodare sequenzialmente i successivi. Al termine il file di appoggio sostituirà il vecchio.
Un altro metodo di gestione consiste nello stabilire a priori la dimensione massima che il record a lunghezza variabile può raggiungere, e poi considerare ogni record sempre di lunghezza pari a quella stabilita. Questo è un metodo spurio basato sui file con record a lunghezza costante e quelli a lunghezza variabile, ereditandone vantaggi e svantaggi. Essi sono:

  • La possibilità di immediata accessibilità e modificabilità di ogni record, in quanto la sua posizione all’interno del file è determinabile con una semplice formula matematica come per il record a lunghezza costante (spiegato più avanti).
  • L’impossibilità di avere record più lunghi di quanto stabilito come limite massimo.
  • Uno spreco dello spazio all’interno di ogni record avente una lunghezza complessiva dei dati inferiore a quella stabilita per il record.

Nella pratica, quest’ultimo tipo di file non viene mai implementato. Comunque, tutta questa descrizione sulla gestione assume più un valore didattico e teorico che pratico in quanto i file di tipo testo sono utilizzati solo per interscambio e non come file dati, appunto a causa della loro inefficienza durante le operazioni di editing.
Passiamo ora ad affrontare la classe che ne permetterà la gestione relativamente all’aspetto dell’I/O.

La classe RecMgr

Innanzi tutto è bene chiarire cosa aspettarsi da questa classe. Essa non si preoccupa di eseguire l’accesso al file a basso livello, di gestire il record in quanto insieme di campi, da qui il suo nome RecMgr che significa Record Manager.
Si tratta dunque di una classe record oriented, quindi non sono supportate funzioni come la open() o la writeO che sono già disponibili dalle librerie standard; vi sono stati svariati motivi che hanno suggerito di non implementarle; questi motivi saranno chiariti al termine della presentazione di tutte e tre le classi come descritto all’inizio; comunque, una volta compresa la logica di queste classi sarà semplicissimo arricchirle di tutte le funzioni che vorrete.
Detto cosa non fa, passiamo a spiegare cosa offre.
Lo scopo di questa classe iniziale di RecMgr era suddividere un record nei suoi campi e viceversa. La forte parametrizzazione è di supporto a queste operazioni. Una classe che esegue lo split (suddivisione) ed il merge (unione) di campi in relazione al record può apparire cosa modesta, ma se siete interessati alla gestione dei file e avrete la pazienza di leggere anche i successivi articoli non ve ne pentirete di certo. Comunque, per farvi gustare cosa potrete ottenere da queste classi, troverete nel dischetto allegato un file di nome EXPDELIM.EXE che le sfrutta concretamente.
Ora vediamo le caratteristiche generiche in un classico file VRL:

  • ogni campo alfanumerico viene racchiuso fra doppie virgolette;
  • i campi sono fra loro separati da una virgola, da un punto e virgola oppure dalla tabulazione;
  • la fine del record viene dichiarata dalla coppia di caratteri Carriage Return e Line Feed (OxOA OxOA);
  • alla fine del file è generalmente presente il carattere EOF (OxlA).

Troviamo quindi quattro elementi:

  • il delimitatore dei campi
  • il separatore dei campi
  • il terminatore del record
  • il terminatore del file

Ognuno di essi è generalmente costituito da un solo carattere ad eccezione del terminatore record che potrebbe averne uno o due a seconda della modalità d’apertura del file e del sistema operativo.
La classe RecMgr permette di definire quale carattere oppure quale coppia di caratteri rappresentino i vari elementi all’interno del file. È importante notare che la coppia di caratteri non indica una possibilità di alternativa dei due caratteri, bensì una sequenza; facendo un esempio, la coppia di caratteri che dichiara la fine del record (OxOD OxOA) non rappresenta un’alternativa fra i due caratteri, ma entrambi devono essere presenti uno dopo l’altro.
È fondamentale comprendere che non si tratta quindi di una possibilità di scelta ma di una combinazione che, per essere riconosciuta, è indispensabile che sia presente come è stata dichiarata. Questa caratteristica di ogni elemento di poter essere dichiarato da uno o due caratteri consente di gestire dei record fuori dagli standard. Infatti, sebbene sia improbabile incontrare un file in cui i record abbiano i campi alfanumerici delimitati all’inizio dalla coppia di caratteri %$ anziché dal singolo carattere doppie virgolette, questa classe è in grado di gestirli senza alcun problema.
In pratica, un record come il seguente:

%$ABCD!!*%$GHIJ!!*%$MNO!!*12345*%$RST!!

in cui:

  • la coppia %$ (percentuale e dollaro) dichiara l’inizio di un campo alfanumerico
  • la coppia !! (due punti esclamativi) dichiara la fine di un campo alfanumerico
  • il carattere * (asterisco) separa fra loro i campi

può essere trattato senza problemi; questo esempio evidenzia anche un’altra possibilità: quella di avere un campo delimitato inizialmente da un carattere (o coppia) diverso da quello che ne dichiara la fine. Molto probabilmente starete pensando che queste sono caratteristiche che troveranno scarsa applicazione pratica.
Ma ora vediamo come sfruttare in maniera molto più interessante e proficua quanto abbiamo visto finora mediante un esempio pratico. Oramai moltissimi wordprocessor consentono di creare circolari o mailing, unendo un documento principale ad un altro documento che contiene tutti i nominativi dei destinatari. Questa è un’operazione che oramai tantissime persone svolgono di routine. Ma poniamo il caso che il file degli indirizzi fosse nato come una semplice lista di una decina di nominativi a cui spedire una circolare con la solita urgenza, a pochi sarebbe saltato in testa di prendere un database, caricarvi i dati, tornare al wordprocessor ed importare gli indirizzi. E se quella stessa lista fosse cresciuta a dismisura e si rendesse necessario passarla in un database, per esempio da Ms-Word all’intramontato Dbase III, potrebbero nascere vari problemi, magari connessi al carattere che separa i campi: in Word può essere la tabulazione, carattere che il Dbase non gradisce.
Per eseguire questa conversione, si può sfruttare la classe RecMgr. Scrivendo un programma che impiega una classe RecMgr per il file di input ed un’altra RecMgr per il file di output, è possibile in poche righe di C++ risolvere il problema: la prima classe dichiara che il separatore dei campi è il carattere tabulazione e che i campi alfanumerici sono non delimitati, mentre la seconda dichiarerà che il separatore campi è la virgola e che i campi sono delimitati dalle doppie virgolette.
Un loop che legge un record, esegue lo splitting dei campi mediante la prima classe RecMgr e li passa alla funzione di merging della seconda RecMgr ha risolto il tutto.
Facile, non è vero? In teoria certamente, ma per quanto attiene alla pratica non abbiamo ancora affrontato la cosa più importante: le funzioni.
Iniziamo dicendo che RecMgr sfrutta una base class: allRecMgr.
Essa nasce con lo scopo di fornire al programma un metodo d’accesso unico ai record indipendentemente dal tipo. Ma per comprendere bene come opera è necessario procedere per gradi affrontando prima RecMgr. Perciò verranno presentate le sole funzioni di allRecMgr strettamente necessarie rimandando la trattazione delle altre.

Le funzioni

Iniziamo col dire che le funzioni inizianti con set servono per avvalorare una variabile della classe, mentre il suffisso get identifica le funzioni che restitui scono valori.
Nella tabella 1 c’è un elenco delle funzioni che servono a dichiarare le caratteristiche del file e del record, mentre in tabella 2 c’è la lista delle funzioni che servono ad acquisirne le caratteristiche prendendo i valori dalla classe tramite le funzioni set...().
Per inciso, è possibile dichiarare assente un elemento passando alla relativa funzione il valore -1; in tal modo, per dichiarare che il record non ha i campi alfanumerici delimitati, si invocano le funzioni setFldDelimiterStart() e setFldDelimiterEnd() con -1 come parametro.
Vediamo ora in dettaglio le funzioni più importanti della classe: readRecord(), splitting() e mergingO.
La funzione readRecord() si occupa di estrarre dal file un record e metterlo in un buffer il cui indirizzo riceve come parametro dal programma. Si è reso necessario scrivere questa funzione anziché utilizzare funzioni standard perché si è parametrizzato il terminatore record, conseguentemente le librerie non disponevano di funzioni adeguate, mentre per la scrittura del record è sufficiente utilizzare la write() specificando da quanti caratteri è composto il record.
I parametri di readRecord() sono:

  • il buffer in cui copiare quanto legge del file
  • la lunghezza massima del buffer
  • lo stream da cui leggere

Nella pratica, il programma che richiama questa funzione deve avere già aperto il file ed allocato la memoria per contenere il buffer. Sarebbe stato possibile anche creare una funzione che assolvesse a questi passi, ma, oltre a non essere lo scopo della classe che vorrebbe occuparsi solo della gestione dei record e non dei file, la classe stessa non avrebbe consentito tutte le libertà al programmatore che invece permette nella sua attuale struttura. Rimane sempre valida la possibilità di sfruttarla come base class per un’ipotetica FileMgr. La funzione che si occupa di prendere un record e suddividerlo nei vari campi da cui è composto si chiama splittingO; essa vuole i seguenti parametri:

  • puntatore al buffer in cui è stato caricato il record
  • lunghezza del record
  • array di char * ove mettere i puntatori alla memoria dinamica ove vengono allocati i campi estratti dal record
  • numero massimo di puntatori che l‘array di char * può accettare
  • array di int in cui memorizzare per ogni campo la sua lunghezza

e restituisce il numero di campi estratti. Nella pratica il programma deve allocare due array, uno di char * ed un altro di integer. La funzione caricherà in essi i dati relativi.
Era possibile gestire tutto all’interno della classe, scaricando quindi il problema di allocazioni da parte dei programmatore, ma è stata preferita questa soluzione per ragioni di performance: un programma che legge un file mediante RecMgr e lo copia sfruttando un’altra RecMgr sfrutta gli array come parametri per entrambe le classi, altrimenti si sarebbe resa necessaria un’operazione di esportazione di ogni singolo campo dalla prima classe per importano all’interno della seconda con immaginabili conseguenze sul fattore tempo.
La funzione merging() si occupa di ricevere un array di char * per fonderli in un unico record. Essa richiede i seguenti parametri:

  • puntatore al buffer del record che deve essere costruito
  • lunghezza massima che il record può assumere
  • numero di campi presenti nell’array di char *
  • array di char * contenenti i campi
  • array di int contenenti la lunghezza di ogni singolo campo

e restituisce la lunghezza del record.
La funzione clearFld() è una funzione fondamentale per l’impiego della classe RecMgr. Abbiamo visto che la funzione splitting() chiede un array di char * per memorizzare i puntatori alla memoria dinamica ove pone i vari campi che estrae dal record.
La funzione clearFld() si occupa di rilasciare la suddetta memoria prima che una successiva splitting() eseguita dalla stessa classe causi la perdita dei puntatori alla memoria dinamica e conseguente impossibilità di rilasciarla. Perciò ogni loop dovrebbe prevedere una chiamata alla clearFld().
Il simbolo DBG9, che viene spiegato dopo, consente di attivare un controllo per evitare quest’inconveniente.

DBG9 e altro ancora

DBG9 è un simbolo creato con una #define. Serve ad attivare alcuni controlli della classe su se stessa. Potete dichiararlo in fase di compilazione, se avete necessità di effettuare del debugging: esso permette di verificare anche la correttezza nella chiamata alle funzioni della classe da parte del programma. La variabile signatureMgr è un integer nel quale viene messo un valore costante quando viene invocato il costruttore della classe. È utile anch’esso in fase di debugging, soprattutto se si fa largo uso di puntatori alla classe: per verificare se tutto è a posto, è sufficiente controllare che il valore di questa variabile sia quello atteso; comunque è una variabile protected, ovvero non accedibile direttamente dall’esterno, ma il debugger è comunque in grado di consentire un suo inspect. Se non vi bastasse, potrete sempre renderla public o creare una funzione public che ne restituisce il valore.

Il nostro futuro

Nei prossimi articoli vedremo:

  • la classe per la gestione dei record a lunghezza costante
  • la classe virtuale per la gestione di entrambi i tipi di file.
  • approfondimenti su tutte e tre le classi con esempi pratici
  • osservazioni e basi per le implementazioni delle classi in piena filosofia C++ per ottenere degli stream che consentano istruzioni del tipo:
    allrecMgr << str1 << str2 << str3 ;
    ed ottenere conseguentemente qualcosa di simile al PRINT# e INPUT# del BASIC.

Conclusioni

Siamo giunti al termine di questo nostro primo appuntamento. Spero che l’argomento sia stato di vostro gradimento e che il mese prossimo siate ancora più numerosi. Nel frattempo potete guardare i sorgenti delle classi e trarne dei suggerimenti oppure sfruttarle immediatamente all'interno dei vostri programmi. Del resto esse sono qui per questo.

Il testo e' stato acquisito tramite OCR dalla rivista su cui e' stato pubblicato e velocemente ricontrollato.
Le segnalazioni di errori saranno molto gradite e si possono fare alla pagina Contatti.

Tratto da:
Paolo Guccini
Rivista DEV Computer Programming
Edizioni Infomedia
1995