Paolo Guccini

"Impossibile" non è mai la risposta giusta

Classe per gestire
i record a lunghezza costante

I sorgenti sono disponibili per il download.

Nell’articolo apparso il mese scorso abbiamo iniziato a parlare di classi dedicate alla gestione dei file di tipo testo incominciando da quelli con record a lunghezza variabile e presentando una classe in linguaggio C++ per la loro gestione. Questo mese continuiamo affrontando i file con record a lunghezza costante vedendone le caratteristiche e presentando un’altra classe adatta alla loro gestione.

Caratteristiche

Le peculiarità che contraddistinguono i file con record a lunghezza costante (d’ora in poi chiamati CRL, dall’acronimo inglese di Constant Record Lenght) sono le seguenti:

  • ogni campo ha una lunghezza costante all’interno del record;
  • ogni campo ha una posizione costante all’interno del record;
  • ogni record ha una lunghezza costante;
  • ogni record contiene un numero costante di campi;
  • i campi campi alfanumerici non sono fra loro non sono delimitati in alcun modo;
  • i campi non sono fra loro separati da caratteri separatori;
  • è opzionale la presenza di caratteri che dichiarino la fine del record ;
  • il file non contiene al suo interno dei caratteri che ne segnalino la fine (EOF);

Vediamo un esempio di record con quattro campi le cui rispettive lunghezze siano di 15,16,5 e 10 byte. La somma delle lunghezze dei singoli campi più gli eventuali caratteri di fine record (generalmente CR+LF) consente di conoscere la lunghezza del record, che nel nostro caso è di 46 byte ed è privo del terminatore record:

Fabio.Rossi......Via.Zucchini.8...40124Bologna
Silvia.Semeraro..Via.Marconi.1....20116Milano.

(il carattere . indica uno spazio; il record è privo del terminatore record, perciò eseguendo il comando DOS type apparirebbero senza soluzione di continuità. Qui sono stati invece separati su due linee per semplificame la lettura. Nel caso il file avesse previsto come terminatore record la coppia di caratteri Carriage Return e Line Feed, ogni record sarebbe stato lungo 48 byte anziché 46 ed il comando Type li avrebbe visualizzati come appaiono sopra).

Vantaggi

A parte l’enorme semplicità e celerità di gestione rispetto i file VRL (Variable Record Lenght), i file CRL costituiscono il metodo più sfruttato da moltissimi programmi per la memorizzazione dati in quanto consentono sia un accesso diretto ad ogni record che la possibilità di modificarne il contenuto senza particolari accorgimenti: queste sono caratteristiche imprescindibili per certi tipi di applicativi, quali i data base, i programmi di contabilità, eccetera.

Come gestirli

Per quanto concerne la lettura di un record, si possono adottare varie tecniche. Qui di seguito ne vengono spiegate alcune, ma è utile indicare che questo paragrafo ha solo uno scopo propedeutico e non è quindi necessario al fine dell’apprendimento del funzionamento della classe RecCLenMgr, la quale viene illustrata più avanti.
La tecnica più semplice consiste in un loop ripetuto un numero di volte pari alla lunghezza del record, il quale contenga al suo interno questo frammento di codice:

*buffer ++ = getc( file ) ;

che si occupa di prelevare tutti i caratteri del record mettendoli in un buffer di tipo char[]. Successivamente il programma si occuperà di suddividere il buffer nei suoi vari campi.
Più impegnativo, ma estremamente più in stile C/C++, possiamo ideare una funzione che agisca in modo parametrizzato, creando:

  • un array di int contenente le lunghezze dei singoli campi da estrarre;
  • tutti i vari char[] costituenti i campi veri e propri, dichiarandoli di lunghezza pari a quella prevista per il campo incrementata di uno per permettere l’inserimento del NULL in coda al fine di ottenere una semplificazione nella gestione dei campi (si possono così adoperare tutte le funzioni relative alle stringhe e quelle di trasformazione di stringa in numero).
  • un array di char* che puntano ai vari campi dichiarati come sopra descritto;

si passa poi ad eseguire un loop ripetuto tante volte quanti sono i campi, il quale estrae un numero di caratteri come specificato nel relativo array, avvalorando direttamente il campo di destinazione individuato tramite l’n-esimo elemento dell’array dei puntatori.
Al termine dell’estrazione di ogni singolo campo si accoda il carattere NULL.

È da tenere presente che il record può avere il terminatore record: in questo caso, al termine del loop di estrazione del record, è necessario estrarre anche il terminatore. Se non viene fatto, alla successiva lettura, il primo campo conterrà al suo inizio il terminatore record e tutti i successivi campi conterranno come primi caratteri quelli che sarebbero in realtà gli ultimi del campo precedente; quest’errore si amplificherebbe poi di record in record.

La gestione dell’accesso all’n-esimo record è di fatto immediata: è sufficiente calcolarne la posizione all’interno del file mediante la seguente formula:

((n-esimo record) -1 ) ) * (lunghezza record) + 1

poi, utilizzando le funzioni standard del C e C++, è possibile posizionarsi sul record desiderato.
Inoltre, i file a lunghezza costante permettono di modificare i vari record senza dover gestire l’intero file: infatti è sufficiente posizionarsi su di esso, leggerlo, cambiare i dati e riscriverlo senza avere preoccupazioni sulla dimensione che il nuovo record può assumere in quanto costante (a differenza dei file a lunghezza variabile che non consentono questa gestione perché i nuovi dati hanno generalmente una lunghezza diversa da quella del record da sostituire).

I campi

I campi subiscono un processo di allineamento al momento di essere scritti sul record.
I dati nei campi alfanumerici vengono allineati a sinistra. verificandone la lunghezza: se il numero di caratteri da memorizzare eccede la dimensione del campo, allora l’eccedenza viene scartata con conseguente perdita di informazioni; se invece vi sono meno caratteri rispetto alla lunghezza del campo, vengono aggiunti tanti caratteri di riempimento (generalmente Io spazio, dec.32, hex 20) fino a raggiungere la lunghezza desiderata.
Per i campi numerici il discorso è concettualmente identico, sennonché l’allineamento avviene sulla destra e il carattere di riempimento può essere lo zero (dec. 48, hex 30) oppure lo spazio.
Ma quasi sempre, per risparmiare spazio, i campi numerici vengono scritti nel record ricorrendo alle rappresentazioni binarie utilizzando 2, 4, 8 byte. Per chi conosce il Basic le funzioni corrispondenti sono CVI, CVS e CVD con le rispettive MKI$, MKS$, MKD$.
Sinteticamente, la rappresentazione binaria opera nella seguente maniera:
si traduce il numero decimale nella sua rappresentazione binaria utilizzando tanti bit (non byte) quanti ne contiene il campo: sedici bit per i campi da due caratteri, 32 per i campi da 4 e 64 per quelli lunghi 8 byte.
Il numero binario così ottenuto viene suddiviso in gruppi di 8 bit:
ogni gruppo viene memorizzato nel campo come byte partendo da destra verso sinistra.
Oltre ai campi alfanumerici e numerici esistono altri tipi come, per esempio, le date o gli orari; nella pratica non vengono riconosciuti o gestiti direttamente da tutti i linguaggi, per cui non li tratteremo.

La classe RecCLLenMgr

Dopo la panoramica sui file CRL. affrontiamo nella pratica la loro lettura e scrittura sfruttando una classe già pronta allo scopo: RecCLenMgr.
Essa si occupa della lettura e scrittura dei record; conseguentemente non supporta funzioni di apertura chiusura del file così come accadeva per la classe RecMgr (la abbiamo vista il mese scorso per la gestione dei record a lunghezza variabile).
Il suo scopo è fornire uno strumento pronto all’uso per leggere un record e suddividerlo nei vari campi che lo costituiscono, oppure di prendere i vari campi e raggrupparli in un record unico.
RecCLenMgr è sotto l’aspetto della programmazione molto semplice da realizzare in quanto sono pochi i problemi da affrontare e le variabili da considerare. Di conseguenza essa contiene poche funzioni.
La classe richiede che il programma abbia allocato il buffer ove viene memorizzato il record, un array di char * che, ad ogni chiamata di ::splitting() verrà utilizzato per memorizzare gli indirizzi della memoria dinamica ove vengono messi i vari campi estratti dal record e un array di integer contenente la lunghezza di ogni singolo campo.
Questi dati verranno sfruttati delle varie funzioni che ora analizzeremo.
La prima che incontriamo è ::readRecord(). Essa legge un numero di byte pari alla lunghezza del record e li copia in un buffer che riceve come parametro. In pratica si tratta solo di una funzione un poco più evoluta della fstream::read(); il motivo della sua creazione è di compatibilità con la classe allRecMgr; infatti in essa troviamo ::readRecord() dichiarata come pure virtual e conseguentemente ogni classe derivata deve fornirne una propria; questo consente al programmatore di sfruttare allRecMgr per gestire entrambi i tipi di file senza grosse difficoltà.
I parametri richiesti sono: il puntatore al buffer deve essere stato preventivamente allocato, la lunghezza del record in byte, il puntatore al file da leggere.
Restituisce il numero di caratteri messi nel buffer: se questo numero differisce dalla lunghezza del record significa che siamo in presenza di una situazione di errore.
Nella figura 1 viene ripreso l’esempio che è apparso all’inizio dell’articolo che servirà da base per gli altri. In essa appare quanto ::readRecord() memorizza nel buffer dopo la lettura dal file.
La funzione ::splitting() di scomporre un record nei campi che lo costituiscono. Essa richiede vari parametri: puntatore al record e puntatore ad array di char * che verrà sfruttato per memorizzare in ogni suo elemento; l’indirizzo di ogni singolo campo estratto; il numero dei campi da estrarre e il puntatore all’array di integer contenente la lunghezza di ogni campo.
Il numero complessivo dei campi estratti viene restituito al programma chiamante. Non viene effettuato nessun controllo per verificare se sono stati utilizzati tutti i caratteri del record in quanto si suppone che il programma chiamante abbia eseguito una analisi di congruenza fra i vari parametri passati a questa funzione: la lunghezza del record deve essere uguale alla somma della lunghezza dei campi più gli eventuali caratteri che dichiarano la fine del record.
Nella figura 2 viene schematizzato quanto accade dopo aver richiamato la funzione ::splitting(): l’array dei campi (chiamato fldArray) contiene gli indirizzi della memoria dinamica ove sono stati memorizzati i vari campi estratti dal record.

E' importante non dimenticare di richiamare la funzione allRecMgr::clearFld() alla fine di ogni ciclo o comunque prima di chiamare nuovamente questa funzione. Il motivo è semplice: ogni campo viene memorizzato nella memoria dinamica ed i relativi puntatori vengono messi nell’array di char * dei campi descritto prima fra i parametn richiesti dalla funzione; ad ogni chiamata di ::splitting() l’array di char * contie ne nuovi indirizzi di memoria dinamica e conseguentemente i precedenti vengono persi.
Se non si è provveduto al rilascio delle precedenti allocazioni, tale RAM non sarà più accessibile né cancellabile così, ad ogni ciclo, verrà allocata nuova RAM perdendo l’utilizzabilità della precedente giungendo, dopo numerosi cicli, a possibili condizioni di errore per insufficiente memoria dinamica. Per maggior precisione, è da notare che la funzione ::clearFld() è costituita da un semplice loop che, per ogni elemento del char ** che riceve come parametro, esegue una delete e poi ne cambia il valore in NULL.
Poiché il char ** è dichiarato esternamente alla classe e quest’ultima lo riceve sempre solo come parametro, la cancellazione dei vari campi può essere gestita senza problemi anche a livello del programma e non quindi della classe. Si può quindi anche eseguire la ::clearFld() con un’istanza della classe diversa da quella utilizzata per richiamare lo ::splitting().
È da osservare inoltre che anche per questa classe è attivo il simbolo DBG9 (dichiarato mediante una #define), che, se compilato, consente di avere il controllo della avvenuta cancellazione dei campi prima di invocare la ::splitting().
Di conseguenza, se si vuole sfruttare questo tipo di controllo automatico è indispensabile richiamare la ::ClearFld() relativa alla stessa istanza della classe utilizzata per la ::splitting(). Inoltre, se all’interno del programma non viene mai richiamata la funzione ::splitting(), allora la delete dei campi puntati dal char ** può essere eseguita da qualunque istanza o dallo stesso programma senza nessun tipo di anomalie.
Questo ovviamente se i campi sono stati allocati nella memoria dinamica e non mediante array statici, per i quali non si deve procedere alla deallocazione.
L’ultima funzione della classe RecCLenMgr è la ::merging().
Essa crea un record partendo dai vari campi che riceve come parametro; dopo aver creato il record tramite questa funzione lo si può scrivere sul file ricorrendo alle funzioni di libreria del C++.
Essa necessita di numerosi parametri. Essi sono suddivisibili in parametri relativi al record, ai valori dei campi, alle caratteristiche dei campi rispetto al record. Sono: il puntatore al buffer che conterrà il record che questa funzione prepara e la lunghezza che il record deve avere (quest’ultimo è presente solo per motivi di eredità dalla base class);
il numero di elementi che costituiscono l’array di char * contenenti i valori dei singoli campi che dovranno essere copiati nel record, nonché un array di integer che contiene la lunghezza dei vari campi contenuti nell’array di char *; il numero di campi che il record contiene e un array di integer che specifica la lunghezza di ogni campo in seno al record.
Nella terza figura viene schematizzato cosa compie la funzione ::merging().
I campi di tipo binario necessitano di una breve digressione.
Questa classe, come del resto RecMgr e AllRecMgr, è stata ideata per i file di tipo testo; di conseguenza non viene fornito supporto ai campi di tipo binario. Nella pratica lo si può realizzare senza grosse difficoltà e comunque in un futuro relativamente prossimo verrà affrontata la loro integrazione nella classe RecCLenMgr; per chi non potesse attendere può contattarmi via E-mail (l’indirizzo è in calce all’articolo).

Esempi pratici

Vediamo ora qualche frammento di codice per vedere nella pratica come può essere impiegata questa classe.
Le assert() che appaiono devono essere considerate solo come punti ove mettere gli opportuni controlli e, conseguentemente, andranno sostituite nel programma finale da opportune gestioni e/o segnalazioni.

Gli esempi possono essere compilati e provati avendo cura di includere i file "ALLRECMG.CPP" e "RECCLEN6.CPP" nel make o project file.
Iniziamo con il listato numero 1. Esso contiene la struttura minima indispensabile per un programma che sfrutti la classe RecCLenMgr per leggere un file. In esso compaiono le dichiarazioni delle seguenti variabili:

  • inRec: puntatore al buffer del record.
    Può anche essere creato come char [] se si conosce la dimensione del record oppure se si stabilisce a priori la lunghezza massima gestibile dal programma;
  • inFldLen: array di integer che contiene le lunghezze di ogni singolo campo.
    Esso appare dimensionato a 64 elementi e con la dichiarazione delle lunghezze dei campi: ovviamente sono solo valori impiegati a titolo di esempio;
  • inFldNum: numero dei campi contenuti in un record.
    L’array inFldLen dovrebbe essere dimensionato con lo stesso valore;
  • inRecLen: lunghezza in byte del record.
    Essa deve essere identica alla somma delle lunghezze dei singoli campi più gli eventuali caratteri terminatori record;
  • fldValue: è l’array che conterrà gli indirizzi dei valori dei campi.
    Come abbiamo già visto, viene sfruttato da ::splitting() che lo impiega per memorizzare gli indirizzi dei campi estratti dal record, e da ::merging() da cui prende i campi che costituiranno il record;
  • readRecLen: lunghezza del record letto. Il suo valore deve essere sempre identico a inRecLen, altrimenti si è verificato un errore;
  • readNumFld: è una variabile di controllo che contiene il numero di campi ottenuti da::splitting(). Deve sempre coincidere inFldNum altrimenti si è verificato un errore.

Il programma è composto da due parti: nella prima appare un loop che calcola la lunghezza del record sommando le lunghezze de i singoli campi; troviamo poi l’allocazione del buffer del record e l’open del file da leggere.
La seconda parte è costituita dal ciclo principale del programma che ha termine quando la variabile RecCLenMgr::fileStatus.eof è vera.
La verifica della condizione di EOF è analizzabile anche mediante l’uso della funzione ifstream::eof(). Viene invece sfruttato questo sistema per compatibilità verso la classe RecMgr: in essa,come è stato spiegato a suo tempo, la fine logica del file precede la sua fine fisica in quanto dichiarata da un carattere (carattere EOF, hex: 0x1A) all’interno dei file stesso e, conseguentemente, la ifstream::eof() non segnalerebbe in tempo la condizione di EOF.
Nel secondo listato appare un esempio di programma che costituisce un’estensione ed integrazione del precedente. Esso compie le seguenti operazioni:

  • 1. legge un record;
  • 2. controlla l’eventuale situazione di End Of File e, nell’eventualità, esce dal ciclo;
  • 3. suddivide il record nei suoi campi costituenti;
  • 4. riunisce i campi in un nuovo record rispettando le caratteristiche di quest’ultimo;
  • 5. esegue la release della memoria ove i campi siano stati allocati dalla ::splitting();

Il programma potrebbe essere perfezionato tenendo conto che se l’ultimo record contiene un numero di caratteri inferiori a quelli attesi, il file raggiungerà la condizione di EOF. Conseguentemente il punto 2 causerà l’uscita dal ciclo e quindi la mancata elaborazione di quanto sia stato estratto dal record (anche se si è sicuramente in presenza di un errore). Fra il punto 3 ed il 4 si dovrebbe inserire quanto attiene alla gestione dei campi da parte del programma.

Conclusioni

Abbiamo dato uno sguardo tecnico ai file CRL e visto una classe che possa facilitare i programmatore nella loro gestione. Nell’articolo precedente abbiamo invece affrontato i record a lunghezza variabile. Abbiamo quindi ora la classe idonea per entrambi i tipi. Il mese prossimo vedremo la classe AllRecMgr che è la base class delle due classi precedenti. Con essa vedremo anche alcune caratteristiche delle classi di cui è stato necessario rimandarne la trattazione dopo AllRecMgr. Il mio indirizzo di E-mail è riportato in calce all’articolo: ogni giudizio, suggerimento o critica costruttiva sarà sempre gradita. Nel frattempo, poiché anche i sorgenti di AllRecMgr vengono pubblicati in allegato, disponete di tutte e tre le classi per muovervi agevolmente coi file di tipo testo.
Arrivederci al prossimo numero.

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
1995 Edizioni Infomedia