Capitolo 6 - CLASSIFICAZIONE DEI TIPI DI DATI

 

 

 

 

            6.1 - Tipi fondamentali, o semplici.

 

                La risorsa elementare della macchina, cioè la capacità di rappresentare l'unità minima di informazione detta bit è stata oggetto di diverse aggrega­zioni: la prima in blocchi 'paralleli' di 8 bit o byte e successivamente le concatenazioni di più byte consecutivi, in genere in numero pari, 2, 4 o 8.

 

                Ne è derivata la capacità 'naturale' di trattare tre diversi generi di oggetti logici:

 

  (6.1.1)                   carattere :             un singolo byte

                                intero     :              due o quattro byte

                                reale       :              quattro od otto byte

 

di cui il secondo ed il terzo sono adatti al calcolo aritmetico mentre il primo, che pure è definito in termini di codifica numerica, rappresenta invece entità simboliche: i simboli grafici o altri codici.

 

                Per quasi tutte le macchine questi sono i tipi di dati che costituiscono l'insieme di riferimento per ogni altro dato ed ogni operazione e per questo motivo sono detti fondamentali o semplici, indicando i successivi con gli aggettivi derivati, o strutturati.

 

                Il termine tipo fissa tre caratteristiche:

 

  (6.1.2)                   (a) - allocazione di memoria

                                (b) - dominio dei valori ammissibili

                                (c) - insieme di operazioni eseguibili

 

per (a) si tratta del numero di byte per la rappresentazione fisica, o imple­mentazione del dato, per (b) dell'intervallo di costanti min .. max codifica­bili in tale spazio, implicitamente ordinate in modo naturale e per (c), infine, degli algoritmi per le manipolazioni dei dati, cui in genere corri­spondono appositi circuiti per la esecuzione.

 

                La maggior parte delle operazioni di macchina seguono la logica di cui abbiamo già visto esempi nel caso della somma di due numeri (3.2.3); cioè nella forma di un comando espresso come verbo imperativo, riferito ad uno o più argomenti, o operandi:

 

  (6.1.3)                   FAI questo con il tale dato ,   oppure:

                                FAI questo con i tali dati ,

 

in cui tra azione richiesta e tipo di dato (o tipi) vi deve essere coerenza.

 

                Se ad esempio l'azione è la somma di due numeri, è necessario specifica­re se gli addendi sono interi (ordinari o lunghi) o reali (in precisione sem­plice o doppia). A seconda del tipo dei dati le operazioni eseguite dalla macchina sono diverse: le differenze strutturali tra i diversi casi richiedono cioè la attivazione di circuiti distinti e/o sequenza operative distinte ed il fatto che nella nostra interpretazione esse siano tutte associate nel termine somma è inessenziale.

 

                Stabilito quale somma si richiede, è ancora necessario che i due termini siano tra loro omogenei e coerenti con la richiesta, perché i circuiti opera­tivi interpreteranno i bit dei due operandi in un modo ben preciso e diverso per i quattro casi; la mancanza di coerenza comporta certamente  o l'insorgere di impossibilità o errori nei risultati.

 

 

                In definitiva, la corretta e coerente esecuzione di una azione ha come premessa indispensabile l'esatta definizione del tipo di dato e quindi delle sue proprietà logiche.

 

                Ogni linguaggio di programmazione deve quindi per prima cosa fissare le regole sui tipi di dati, nei termini:

 

  (6.1.4)                   (1) - quali tipi riconosce, ossia su quali opera ;

                                (2) - con quali regole un dato è associato ad un tipo .

 

                I vari linguaggi si comportano al riguardo in modi diversi; limitandoci ai quattro citati fin dall'inizio, Basic, C, FORTRAN e Pascal, tutti ricono­scono i tre tipi citati in (6.1.1) ed aggiungono alla lista un nuovo tipo:

 

  (6.1.5)                   logico   :                uno, o due, o quattro byte

 

per il quale sono disponibili i due valori di costanti vero / falso e le operazioni logiche di base: congiunzione, disgiunzione, negazione; tra i vari linguaggi vi sono però notevoli differenze a questo proposito. FORTRAN e Pascal prevedono una apposita definizione, che è invece assente in Basic e C; tutti sono però in grado di 'calcolare' espressioni logiche.

 

 

                Per certi aspetti, si può considerare di base anche un altro tipo:

 

  (6.1.6)                   stringhe  :              lunghezza arbitraria

 

in quanto molte macchine dispongono di operazioni progettate appositamente per operare su questo genere di dato: copia di stringhe, ricerca e sostituzione di caratteri o sottostringhe, concatenazione, ecc.; per le stringhe si ha pure un ordinamento 'naturale' lessicografico, indotto da quello dei caratteri[1].

 

                La impossibilità di fissare una lunghezza (o una gamma di lunghezze) ed un molto più ampio insieme di costanti di riferimento possono però fare rite­nere più appropriata la collocazione delle stringhe tra i dati strutturati.

 

                Mentre Basic e Fortran possiedono un criterio di riconoscimento implici­to, o di default, per cui ogni dato è associato automaticamente ad un tipo, salvo esplicita richiesta contraria (tutti reali per il Basic, reali oppure interi per il Fortran), Pascal e C non possiedono invece alcun criterio prede­finito e l'utente è responsabile dell'associazione esplicita al tipo per ogni dato che intende trattare, almeno per le variabili.

 

                La seconda soluzione è migliore, perché permette un maggior grado di controllo del compilatore, che può segnalare errori altrimenti assai difficili da trovare e l'evoluzione dei linguaggi ha suggerito di inserire anche nel Basic e nel Fortran opzioni con le quali l'utente rinuncia alla 'agevolazione' della classificazione predefinita, impegnandosi ad un controllo più accurato sui dati che sono l'oggetto del suo programma.

 

 

 

 

 

            6.2 - Costanti e variabili.

 

                Per ogni tipo di dato il dominio delle costanti è rigidamente fissato dalla implementazione del tipo di dato stesso: per i caratteri 0 .. 255, oppure in certi casi -128 .. 127, per i numeri interi -32768 .. 32767 (tipo a 2 byte) e così via, secondo quanto visto nel capitolo precedente.

 

                Una costante ha la caratteristica di autodescriversi; scrivendone il valore è cioè implicitamente fissato anche il tipo di dato, che deve però essere coerente con l'azione richiesta: ad esempio per una operazione su un carattere no si deve ammettere un valore negativo, o che superi 255.

 

                La evoluzione dei linguaggi ha permesso due facilitazioni; la prima è la possibilità di assegnare esplicitamente un tipo anche alle costanti, la secon­da è la definizione di costanti simboliche.

 

                Scrivere una sola volta che il 'nome' Pigreco (creato dall'utente) deve essere inteso come sinonimo di una certa sequenza di caratteri decimali è assai meglio che essere obbligati a riscrivere la medesima sequenza un numero indefinito di volte, perché evita ripetizioni inutili e diminuisce le probabi­lità di errori.

 

                Le costanti simboliche rendono in genere molto più leggibile un pro­gramma; inoltre, nel caso delle variabili strutturate (vedi oltre), che spesso richiedono il dimensionamento con costanti é più facilmente controllabile la manutenzione, ossia le modifiche successive.

 

                Le capacità applicative della macchina sarebbero però molto limitate se essa fosse in grado di trattare solo dati costanti; il già citato esempio della somma (3.2.3) ha valore generale proprio perché non è formulato in termini di costanti, ma di variabili: i dati sono cioè trattati in modo simbo­lico, definendo azioni e relazioni astratte, non legate a valori particolari.

 

                Nelle specificazioni di un linguaggio il primo compito è quello di classificare i tipi di oggetti che esso può trattare, ma subito dopo si deve passare ai suoi criteri di definizione delle variabili, regolandone la formazione dei nomi, che significa stabilire per esse:

 

  (6.2.1)                   - l'alfabeto impiegabile;

                                - gli eventuali vincoli su particolari caratteri;

                                - la lunghezza massima dei nomi;

                                - la forma delle dichiarazioni che le associano ad un tipo.

 

                Per queste ultime possono essere eventualmente stabilite convenzioni implicite predefinite.

 

 

 

 

 

            6.3 - Notazione degli indici.

 

                Le variabili impiegate nei linguaggi di programmazione sono nomi simbo­lici che rappresentano generici valori all'interno di un insieme di costanti, non diversamente da quanto avviene nell'algebra. Nel caso attuale le costanti (di base) sono condizionate dalle decisioni prese per l'implementazione.

 

                La associazione:

 

  (6.3.1)                   nome  --->  singolo valore

 

limita però fortemente le possibili applicazioni, non diversamente da quanto accade nello sviluppo della matematica.

 

                Se si considera ad esempio un sistema di due equazioni in due incognite:

 

  (6.3.2)                   a * x + b * y = c

                                c * x + d * y = e

 

la notazione impiegata è perfettamente adeguata al problema, ma se il numero di equazioni e di incognite cresce diviene poco pratico aumentare indefinita­mente il numero di nomi impiegati.

 

                Con notazioni come la (6.3.2) è addirittura impossibile formulare il problema nei termini più generali, cioè per un qualunque numero di equazioni ed incognite.

 

                Si rende necessario un 'salto di qualità', che riconoscendo la natura omogenea dei coefficienti, dei termini noti e delle incognite rispettivamente, non le consideri più come tanti elementi ognuno dei quali fa 'storia a sé, ma ne evidenzi la natura di insieme introducendo una nuova notazione.

 

                Se si indicano con:

 

  (6.3.3)                   A, B, X

 

i tre generici insiemi, questi sono ancora nomi che rappresentano entità variabili, ma i valori ai quali fanno riferimento non sono più singoli dati, ma loro aggregazioni. Per menzionare un dato in particolare non basta citare uno dei tre nomi, ma lo si deve corredare di una informazione ausiliaria, che permetta di stabilire di quale elemento dell'insieme si tratta.

 

                Ciò si ottiene con la consueta notazione degli indici, la comprensione del cui significato è probabilmente più semplice della descrizione:

 

  (6.3.4)                   a1,1 * x1 + a1,2 * x2 + .... + a1,n = b1

                                a2,1 * x1 + a2,2 * x2 + .... + a1,n = b2

                                ........................................

                                am,1 * x1 + am,2 * x2 + .... + am,n = bm

 

                L'esigenza di una simile trattazione generalizzata delle variabili si ha anche in campi non strettamente matematici; ad esempio, se si desidera formu­lare il problema dell'ordinamento di tre dati dello stesso tipo si può dire:

 

  (6.3.5)                   ordinare a, b, c

 

ma ci si vuole esprimere in termini generali si deve dire:

 

  (6.3.6)                   ordinare  x1, x2, ..., xn .

 

                La notazione degli indici impiegata nelle (6.3.4) e (6.3.6) è il metodo più semplice per risolvere il problema posto dalle nuove variabili d'insieme come quelle in (6.3.3). Poiché si tratta di liste lineari, o unidimensionali o di tabelle bilineari bidimensionali, in cui i vari elementi si distinguono per il posto occupato, è sufficiente introdurre una o due informazioni ausiliarie indipendenti (gli indici) per individuare esattamente un elemento dell'insie­me.

 

 

                Se è sufficiente un indice, cioè se basta individuare il posto nella sequenza, il relativo insieme viene anche detto vettore: di esso importa distinguere la prima componente, la seconda, ecc.; si invece l'ordinamento interno prevede un doppio schema di righe e colonne l'insieme è detto matrice ed un elemento è determinato specificando due indici indipendenti.

 

                Mentre gli elementi degli insiemi (cioè le costanti di riferimento) possono essere generici (caratteri, interi, reali, ecc.), gli indici sono invece certamente interi, dei quali al massimo si può discutere se sia più opportuno iniziare con uno oppure con zero.

 

                Il termine inglese array descrive bene l'impiego degli indici, non importa se uno, due o più (ad esempio matrici tridimensionali). Esso può infatti essere tradotto con insieme (finito) di elementi omogenei ordinato secondo una o più dimensioni.

 

 

 

 

 

 

            6.4 - Tipi di dati strutturati.

 

                Gli array (vettori, matrici e loro generalizzazioni) sono il primo e più semplice esempio di dati non elementari, cioè non più immediatamente ricondu­cibili alle rappresentazioni di macchina (byte, word, ecc.).

 

                Essi sono invece ottenuti a partire dai dati elementari introducendo una forma di organizzazione o struttura che determina un insieme. Per quanto ri­guarda le operazioni, esse sono eseguite sempre e soltanto su dati elementari, ma possono essere ordinate in schemi funzionali che utilizzano la struttura per ottenere risultati di ordine generale.

 

                I linguaggi di programmazione definiscono tutti l'impiego degli indici, ognuno con convenzioni sue proprie, sia per la descrizione degli indici stes­si, sia per elementi tecnici quali il valore iniziale (uno o zero). Le pro­prietà 'linguistiche' sono poi tali da permettere un impiego molto efficiente di questo tipo di notazione.

 

                Un caso particolarmente importante di array si ha quando gli elementi di base sono caratteri: si ritrovano le stringhe, già menzionate più volte.

 

                Nella definizione degli array vi è però un elemento che ne limita la capacità applicativa: gli elementi debbono essere tra loro omogenei, tutti dello stesso tipo e quindi riferiti allo stesso insieme di costanti.

 

                Molti problemi sono invece naturalmente riferiti a dati organizzati in modo più generale; i dati possono presentare delle aggregazioni in insiemi, i cui elementi non sono però omogenei, per la loro stessa natura e/o per le operazioni da compiersi su di essi.

 

                Ad esempio, la descrizione di un pixel (punto) dello schermo potrebbe essere data nei termini seguenti, in cui le parentesi graffe sono la notazione consueta per gli insiemi:

 

  (6.4.1)                   pixel = { ascissa, ordinata, colore, stato } .

 

                Considerare l'insieme (6.4.1) come un array sarebbe una approssimazione molto grossolana; come minimo, i primi tre elementi possono anche essere considerati interi, ma il quarto, i cui valori sono acceso / spento deve essere invece trattato come una variabile di tipo logico.

 

                Ma non è neppure vero che le altre tre componenti sono tra loro omoge­nee, perché i valori ammissibili sono diversi nei tre casi; essi debbono cioè essere oggetto di elaborazioni e controllo tra loro distinti.

 

                E' quindi opportuno generalizzare il concetto di array ad una organizza­zione più flessibile. Il termine con il quale i linguaggi di programmazione trattato questo caso è in genere record (Pascal, FORTRAN, Basic), mentre nel C si preferisce struct, da structure.

 

                L'equivalente italiano di record è registrazione; il termine è stato originariamente introdotto nel Cobol, linguaggio specializzato per la tratta­zione di dati di tipo generale (ad esempio gestionale), nei quali costruzioni come la (6.4.1) sono la norma.

 

 

                Un altro esempio 'classico' di dato strutturato si ha con i numeri complessi, o coppie ordinate di numeri reali:

 

  (6.4.2)                   c = ( x, y )     con x, y reali ;

 

in questo caso le due componenti sono del tutto omogenee per la natura, ma ben distinte per le operazioni.

 

                Un aspetto importante delle strutture è che esse sono schemi per la definizione di oggetti logici complessi costruiti a partire da altri più semplici, che debbono essere noti in precedenza; nulla però impone che i loro elementi siano dati elementari, vale a dire in una struttura è possibile fare riferimento (impiegare componenti) che sono a loro volta strutture.

 

                Ad esempio, data la definizione (6.4.1), l'intero schermo può essere considerato come una lista lineare, o vettore di simili oggetti logici; questa scelta non è l'unica, né è necessariamente conveniente, ma è possibile.

 

                Con la definizione delle strutture si ha un passo importante nel senso dell'astrazione e della generalizzazione.

 

                Mentre inizialmente si pensa ai dati come realtà operative di macchina (interi, reali, ecc.), di cui è necessario interessarsi per descrivere in termini coerenti operazioni e dati, l'attenzione si sposta a schemi più astratti, dettati dalle esigenze dei problemi da trattare.

 

                E' sempre vero che la definizione di un tipo di dato deve in definitiva ridursi alla determinazione del numero di byte necessario per rappresentarlo, ma questa non è più rigidamente legata alle sole possibilità (6.1.1), (6.1.3).

 

                L'utente può cioè aggiungere schemi propri rispetto alle disponibilità di base, estendendo anche con nomi simbolici le definizioni dei dati e compor­tarsi come se esse fossero disponibilità effettive di una macchina virtuale, non più vincolata dalle sue particolarità costruttive ed operative.

 

                Si osservi, infine, che la definizione di schemi di dati è solo un pro­getto, o il manifestarsi di intenzioni sul modo di impiegare le risorse di memoria, ma che non le impegna effettivamente. Ciò avviene invece con i nomi delle variabili, ognuna delle quali deve effettivamente essere allocata secon­do quanto richiede il suo tipo.

 

 

 

 

 

            6.5 - Indirizzi e puntatori.

 

                Fino dalla prima rappresentazione (3.2.2) è emersa la necessità della rappresentazione in memoria dei dati; le 'celle' indicate simbolicamente con nomi di fantasia sono successivamente state meglio qualificate come byte od aggregati di byte, da interpretarsi secondo le esigenze del tipo di dato, semplice o strutturato che sia.

 

                In genere all'utente di un linguaggio di programmazione interessano solo la disponibilità dei nomi delle variabili e le regole per operare su di esse; quando però il testo di un programma è stato convertito nel suo equivalente in linguaggio macchina i nomi non hanno più importanza.

 

                La versione eseguibile del programma, cioè il prodotto finale della compilazione, è composta di sole istruzioni espresse in codici binari ed i dati sono riferiti direttamente con l'indirizzo che hanno in memoria.

 

                Durante il caricamento, l'immagine eseguibile del programma viene tra­sferita dal disco nella memoria di lavoro, che risulta quindi in parte occupa­ta dalle istruzioni ed in parte dalla 'zona dati', o più esattamente da quella parte di dati che definiremo statici.

 

                La determinazione dell'indirizzo di ogni dato può essere abbastanza complessa, ed avviene con modalità variabili da macchina a macchina; coinvolge in genere un punto di caricamento iniziale (nei vari casi, base, segmento o altro), uno scostamento (offset) rispetto a tale punto ed altri possibili elementi, quali costanti ed indici.

 

                Ogni variabile, oltre a possedere i caratteri fin qui acquisiti, numero di byte occupati e modo di interpretarli, è sempre accompagnata da un'altra informazione 'nascosta', che è il suo indirizzo in memoria.

 

                I linguaggi C e Pascal hanno fin dall'inizio definito nella propria sintassi dei mezzi per potere trattare simbolicamente l'informazione fornita dagli indirizzi, in modo più ampio il primo e più restrittivo il secondo. Con la evoluzione, tutti i linguaggi permettono oggi di trattare un tipo speciale di variabili, destinato a rappresentare indirizzi di memoria.

 

                Avere accesso diretto agli indirizzi significa definire un tipo speciale di variabile, i pointer, o puntatori alla memoria, i cui valori possibili sono appunto indirizzi di memoria. Per la loro varietà non si può però fissar­ne in astratto la forma, che per la quasi totalità dei costruttori richiede 32 bit; questi 4 byte sono però gestiti in parecchie forme distinte dai diversi tipi di microprocessori sul mercato.

 

                La definizione originaria del linguaggio C era già completa, prevedendo per ogni puntatore un nome (come variabile) accompagnato dalla indicazione implicita del numero di byte impegnati dal dato indirizzato, cosa che permette di sviluppare una speciale aritmetica dei puntatori.

 

                Per il Pascal, invece, lo stesso termine viene impiegato in connessione alla creazione, gestione e cancellazione di dati in una speciale memoria dinamica detta heap.

 

 

 

 

 

 

            6.6 - Memoria statica e dinamica.

 

                Abbiamo menzionato in precedenza i due aggettivi statica e dinamica in riferimento alla memoria; essi si riferiscono a due modi diversi, ed in un certo senso opposti di gestire tale risorsa.

 

                Con la compilazione da un lato si ha la traduzione del testo originario in istruzioni di macchina, e dall'altro la misura della quantità di memoria necessaria per rappresentare i dati su cui tali istruzioni debbono operare.

 

                L'immagine eseguibile del programma in memoria potrebbe quindi essere suddivisa in due parti, ad esempio collocando prima la zona dati, quindi la zona istruzioni, eventualmente ripetendo più volte la stessa operazione nel caso che, come accade di solito, il programma sia diviso in più unità funzio­nali indipendenti.

 

                In questo modo si considera implicitamente di occupare la memoria in modo statico: il programma viene caricato in modo completo e le istruzioni seguono il loro corso indirizzandosi alle zone dati preallocate fino alla conclusione delle operazioni, con cui la memoria torna di nuovo libera.

 

                Questo schema può però essere o non realizzabile per diverse ragioni, o se realizzabile può essere poco efficiente, o entrambe le cose.

 

                Su macchine a memoria limitata può essere impossibile caricare completa­mente il programma, sia per la parte di istruzioni, sia per quella dei dati; una delle prime tecniche cui si è fatto ricorso per ovviare al problema è detta di overlay o sovrapposizione.

 

                Essa consiste nel tenere in memoria solo la parte di programma necessa­ria (e/o possibile) in un determinato istante; quando serve un riferimento ad una diversa, quella che non serve più viene eliminata e la nuova parte è caricata nella zona di memoria che la prima occupava in precedenza.

 

                Programmare 'in overlay' richiede molta cura ed esperienza, perché è responsabilità del programmatore decidere quali segmenti di programma e quali dati debbano risiedere in memoria in quale ordine per evitare incongruenze; questa tecnica è stata indispensabile nel passato, ma le capacità attuali delle macchine la hanno resa piuttosto rara.

 

                l'overlay costituisce comunque un primo modo di utilizzare la memoria dinamicamente, con l'utilizzazione dei medesimi spazi per più fini diversi in tempi diversi.

 

                Il maggiore problema dell'overlay è che i dati contenuti nel segmento di programma che viene rimpiazzato in memoria sono persi; un eventuale recupero del segmento in uno stadio successivo non permetterà di utilizzarli.

 

                A ciò si è ovviato con gli schemi detti di memoria virtuale, basati sull'impiego congiunto della memoria e del disco, sul quale viene scaricata una copia del segmento sostituito così com'è, in modo da poterne più avanti riprendere l'elaborazione al punto in cui era rimasta.

 

                Quest'ultimo è oggi il metodo normale per i sistemi operativi e molti programmi applicativi; non interessa però direttamente l'utente nel suo impie­go dei linguaggi di programmazione, perché in genere avviene automaticamente a livello di sistema; per il programmatore l'impiego della memoria è sempre statico, almeno dal punto di vista concettuale.

 

                Convenuto che per gestione dinamica della memoria si deve intendere la allocazione flessibile dello spazio in dipendenza dallo stato della elabora­zione, diviene però naturale cercare di rendere disponibile questa possibilità direttamente dal linguaggio di programmazione.

 

                Vi possono essere due fondamentali motivi per questa richiesta:

 

  (6.6.1)                   (a) - non è possibile determinare a priori l'esatta quantità di memoria necessaria per la

                                        rappresentazione dei dati durante l'esecuzione; oppure, essa è determinabile, ma

                                        se ne vuole lasciare flessibile la gestione.

                                (b) - i dati hanno estensione tale da non potere essere tutti contenuti in memoria e/o se

                                        le operazioni avvengono su  certi dati, ne esistono altri che non interessano più.

 

                I puntatori del Pascal sono definiti in funzione della esigenza (a), per la quale una parte della memoria disponibile viene allocata (staticamente) sotto il nome di spazio heap (mucchio di fieno) e occupato o liberato secondo necessità.

 

                Qualcosa di simile si ha in Basic per l'allocazione di stringhe, vettori e matrici. In entrambi i casi le possibile eliminazioni di dati non più neces­sari impongono una gestione accurata dello spazio disponibile, che potrebbe divenire 'frammentato' in modo tale da non essere più utilizzabile anche se non saturato.

 

                Il caso (b) è invece più vicino alle funzioni di allocazione di memoria dinamica disponibili nelle librerie che accompagnano il linguaggio C, che non impiega particolari termini per indicarla, né richiede la preallocazione di uno spazio statico limitato; anche per esso si può porre il problema della ricostruzione dello spazio deteriorato, che avviene di solito con moduli di programma detti di garbage collection (raccolta della spazzatura).

 

 

 

 

 

 

            6.7 - Moduli funzionali e classi di memoria

 

                Il modo di soddisfare le domande del tipo delle (a) e (b) in (6.6.1) costituisce ciò che nel gergo informatico viene detto gestione 'dinamica' della memoria, in contrapposizione con il modo 'statico'.

 

                Nel corso di una elaborazione, vi possono però essere altre esigenze che richiedono una risposta dinamica, in genere non direttamente sotto il control­lo dell'utente dei linguaggi, ma di cui si deve essere coscienti per non incorrere in condizioni di difficile soluzione.

 

                Si consideri la richiesta di un calcolo come:

 

   (6.7.1)                  y = a * x * x - b * x + c

 

che, con lievi variazioni nella scrittura è una istruzione accettabile in tutti i linguaggi di cui ci occupiamo, che permettono assegnazioni il cui senso è assegna alla variabile a sinistra (y) il valore derivante dal calcolo della espressione aritmetica sulla destra.

 

                Poiché nell'esecuzione la macchina deve procedere a passi elementari, il calcolo richiesto deve venire spezzato in una sequenza nel corso della quale si utilizzano i dati contenuti in memoria, a, b, c, x, ma possono essere necessari anche altri valori temporanei per i calcoli intermedi, che non hanno più interesse a calcolo terminato.

 

                E' quindi opportuno potere disporre di una zona di memoria che operi come una 'lavagna' su cui annotare informazioni che, finito il loro scopo, possono venire sostituite da altre che ne riutilizzano lo spazio.

 

                Lo strumento più adatto allo scopo è una struttura denominata stack, che in inglese ha il senso di pila (di piatti, libri, ecc.); le sue modalità operative sono indicate con la sigla LIFO, da Last In First Out (ultimo den­tro, primo fuori).

 

                Ogni programma ha a disposizione uno spazio di memoria limitato e gesti­to in modo del tutto automatico, sul quale vengono collocate tutte le informa­zioni temporanee con operazioni dette push e pop, rispettivamente per aggiun­gere un nuovo elemento allo stack ed eliminare l'ultimo aggiunto.

 

                I risultati intermedi delle operazioni sono solo un esempio di impiego dello stack, e certamente non il principale. Di gran lunga più importante è ciò che accade quando un programma è diviso in moduli funzionali, ognuno dei quali può essere attivato su esplicita richiesta per un compito specifico.

 

                Questa idea è già stata presentata descrivendo le tecniche di overlay; in quel caso si pensava ai moduli come a insiemi di istruzioni + dati di natura statica.

 

                Ma ciò non è in generale vero per i dati che interessano solo il modulo: è invece sufficiente che essi siano disponibili al momento della sua attiva­zione, e non servono più quando le operazioni del modulo si sono concluse; Lo spazio disponibile nello stack è quindi ideale anche per questo scopo.

 

                Dei dati che possono essere trattati in questo modo si dice che essi hanno validità locale; non è necessario che siano noti, né che esistano in qualche forma in un contesto diverso da quelli del modulo stesso.

 

                Viceversa, possono esistere dati che debbono essere noti a più moduli distinti, che per contrapposizione vengono detti globali e richiedono memoria statica per essere rappresentati.

 

                Non si deve identificare 'globale' con 'statico' e 'locale' con 'dinami­co', perché è possibile che dati di interesse locale debbano essere collocati in memoria statica, ad esempio perché una successiva attivazione del modulo possa avere informazioni su una eventuale attivazione precedente.

 

                Abbiamo così definito almeno quattro modi di gestione della memoria, globale, locale statica, locale dinamica e spazio heap; con una terminologia usuale solo nel linguaggio C esse possono essere dette classi di memoria.

 

                Poiché in precedenza si è detto di possibili problemi nella gestione dello stack, è opportuno giustificare brevemente l'asserzione.

 

                Lo spazio disponibile per lo stack è di norma molto limitato: dimensioni di 4 Kbyte sono ad esempio abbastanza frequenti. Esso viene utilizzato per gli scopi illustrati sopra, ma il più importante è forse quello di costruire le strutture di comunicazioni tra moduli, che non possiamo esaminare in dettaglio in questa sede, ma che richiedono comunque un certo spazio.

 

                Poiché le variabili locali di un modulo vengono allocate sullo stack, si debbono evitare grandi richieste di spazio, in particolare se si utilizza una proprietà che viene illustrata con i singoli linguaggi e che è detta attiva­zione ricorsiva dei moduli funzionali.

               

 

 

 

 

 

            6.8 - Altre strutture

 

                I dati dei tipi carattere, numerici e logici sono la gamma 'normale' per Basic, C, FORTRAN e Pascal, con diverse possibili varianti; come già accenna­to, i primi due non trattano il tipo logico in modo indipendente.

 

                Ogni linguaggio aggiunge qualche elemento alla definizione dei tipi; ne descriviamo i casi più importanti.

 

 

                - STRINGHE -

 

                Il Basic le presenta come un tipo semplice indipendente, anche se in realtà si tratta di strutture piuttosto complesse, una cui implementazione può essere: allocare spazio in memoria statica per l'indirizzo e la lunghezza della stringa ed in memoria dinamica per il suo contenuto, che può anche essere nullo.

 

                Il FORTRAN tratta le stringhe come semplici vettori di caratteri ad allocazione sempre statica (almeno fino al FORTRAN 90), ma prevede operazioni che ne rendono particolarmente semplice la manipolazione; non sono però ammes­se le stringhe nulle.

 

                Anche per il C le stringhe sono vettori di caratteri, ma per riconoscer­le come tali è necessaria la 'chiusura' con un byte nullo. L'allocazione può essere statica o dinamica; per la trattazione delle stringhe sono particolar­mente interessanti le disponibilità dei puntatori.

 

                Nel Pascal, infine, la definizione delle stringhe costituisce un proble­ma da risolvere 'forzando' in qualche modo le rigide barriere della sintassi del linguaggio. Sono possibili soluzioni statiche e dinamiche, ma purtroppo non c'è accordo tra i costruttori dei principali compilatori.

 

 

                - NUMERI COMPLESSI -

 

                Solo il FORTRAN ne prevede la presenza come tipi indipendenti, ossia definibili nella sintassi del linguaggio e dotati di operazioni proprie; negli altri casi l'utente deve costruirli da sé, scegliendo tra la definizione come vettori di due numeri reali o come strutture.

 

 

                - INSIEMI -

 

                Si tratta di un tipo strutturato presente nel solo Pascal, che a partire da un insieme finito di elementi 'di base' permette la definizione di variabi­li che rappresentano tutti i possibili insiemi di tali elementi, o sottoinsie­mi dell'insieme di base; sono naturalmente disponibili tutte le principali operazioni insiemistiche, basate sulla relazione di appartenenza.

 

 

                - BIT FIELDS -

 

                Letteralmente, campi di bit, qui usato nel senso di generica aggregazio­ne di bit. E' un particolare tipo di struttura presente nel solo linguaggio C, che costituisce il solo caso di operazioni sui singoli bit, cioè al di sotto della soglia del byte, finora considerato 'indivisibile' ai fini della elabo­razione.

 

 

                - FILE -

 

                Solo C e Pascal comprendono nella propria sintassi questo termine per indicare un tipo di oggetto logico che il linguaggio è in grado di trattare.

 

                Il termine introduce il tema delle operazioni I/O, con le quali si prendono in considerazione i canali di comunicazione esterni, mentre finora l'attenzione è stata rivolta esclusivamente alla memoria di lavoro.

 

 



[1]  L'ordine lessicografico è quello consueto dei dizionari e degli elenchi del telefono, per cui il con­fronto inizia al primo carattere e prosegue sui successivi; la decisione si raggiunge quando nella stessa posizione le due stringhe possiedono caratteri distinti, di cui uno precede necessariamente l'altro.