15.1 Introduzione 15.2 Gli algoritmi ed i programmi

15.1 Introduzione
Un computer è una macchina a stati in grado di eseguire delle
operazioni aritmetico-logiche sui dei dati di ingresso e fornisce dei
risultati. La logica con la quale il sistema elabora i dati di ingresso
(input) per fornire dei risultati (output) è definita da quello che viene
detto programma.
Generalmente un programma (applicazione, file binario,
eseguibile, script) nasce dallʼesigenza di trovare la soluzione ad un
determinato problema (o classe di problemi) anche se i motivi che
spingono i programmatori a scrivere i programmi possono essere
molteplici: dal mero interesse pecuniario, allʼattrazione per la sfida
(posta dal problema da risolvere), alla voglia di apprendere (sia
perché le tecnologie sono sempre in evoluzione, sia perché i
programmi possono essere scritti per vari scopi e quindi possono
coinvolgere varie discipline, dalla grafica allʼaudio, alla gestione dei
dati, allʼintelligenza artificiale, ...), alla voglia di creare qualcosa che
possa essere utile per sé ed anche per altri.
15.2 Gli algoritmi ed i programmi
Il PC è una macchina programmabile, ovvero che è in grado di
elaborare dei dati in funzione di una determinata logica definita da
qualcuno. La descrizione della logica in base alla quale il sistema
deve elaborare i dati è detta algoritmo1 e consiste in un numero
finito di passi da seguire per arrivare alla soluzione. Un algoritmo può
essere espresso in varie forme, ma quelle più utilizzate sono
essenzialmente due: il linguaggio naturale, o il diagramma a blocchi.
Ad esempio, si considerino gli esempi riportati di seguito relativi ad un
algoritmo che visualizza il massimo tra due valori inseriti, il primo è
descritto col linguaggio naturale, mentre il secondo è un diagramma a
blocchi (in tali esempi a e b sono i valori inseriti).
Esistono essenzialmente due tipi di approccio per la stesura di un
algoritmo che hanno caratteristiche opposte
top-down è lʼapproccio più classico. È il metodo secondo il
quale chi scrive lʼalgoritmo deve avere da subito unʼidea della
struttura dellʼalgoritmo stesso: la stesura dellʼalgoritmo inizia dal
suo inizio e procede sequenzialmente fino ad arrivare alla fine;
bottom-up è lʼapproccio nato per la stesura di algoritmi relativi
a problemi più complessi. Si basa sul motto latino “divide et
impera”, ovvero il problema viene spezzato in sottoproblemi più
facili da risolvere e si inizia a stendere lʼalgoritmo per questi.
Quindi si combinano insieme le varie parti fino ad ottenere
lʼalgoritmo che risolve il problema nel suo complesso.
Nella realtà per la stesura di un algoritmo si fa generalmente uso di
entrambe le due tecniche: per lo sviluppo delle parti per le quali è
necessario avere una visione dʼinsieme del problema si utilizza la
tecnica top-down, mentre per sviluppare le parti per le quali si sente
più lʼesigenza di concentrarsi sul dettaglio, si utilizza la tecnica
bottom-up.
Un algoritmo, sia esso espresso in linguaggio naturale che
sottoforma di diagramma a blocchi, descrive i passi necessari per la
soluzione di un problema, ma non è comprensibile da un computer. A
tal fine esso deve essere trasformato in un programma. Un
programma infatti non è altro che lʼimplementazione di un algoritmo in
un linguaggio comprensibile allʼelaboratore, cioè una sequenza finita
ed ordinata di istruzioni eseguibili in un tempo finito.
La CPU è in grado di comprendere solo un particolare insieme di
istruzioni: il linguaggio macchina (o codice macchina) composto
da particolari sequenze di valori binari. Scrivere un programma in
linguaggio macchina non è né pratico né agevole. Ecco quindi che
sono nati i linguaggi di programmazione, dei linguaggi intermedi,
molto vicini al linguaggio naturale (rispetto al linguaggio macchina),
che rendono più facile la vita al programmatore. Una volta scritto un
programma con il linguaggio di programmazione desiderato esistono
degli appositi programmi che fanno la “traduzione” del programma in
linguaggio macchina, in maniera da renderlo comprensibile alla CPU
e, quindi, eseguibile.
Esistono essenzialmente due metodi per effettuare la traduzione di
un programma in linguaggio macchina: lʼinterpretazione e la
compilazione, che si basano su tecniche diverse descritte di seguito.
Un programma è generalmente memorizzato sul file system
allʼinterno di uno o più file, detti file sorgenti, che contengono quello
che viene definito appunto il codice (sorgente) del programma, cioè il
programma scritto nel linguaggio di programmazione scelto.
15.2.1 Lʼinterprete
Lʼinterprete è un programma in grado di tradurre in linguaggio
macchina e fare eseguire un file sorgente scritto in un particolare
linguaggio di programmazione. Ovviamente ogni interprete sarà in
grado di tradurre i programmi scritti nel linguaggio di programmazione
che esso comprende.
Lʼinterprete legge, traduce e fa eseguire alla CPU le istruzioni
presenti nel file sorgente, una dopo lʼaltra. Questo implica il fatto che
è necessario che lʼinterprete sia presente ed in esecuzione sul
sistema ogni volta che il programma deve essere eseguito, poiché
altrimenti la CPU non sarebbe in grado di eseguire il file sorgente: è
scritto in un linguaggio che essa non comprende.
Di seguito sono elencati i vantaggi e gli svantaggi dellʼutilizzo di un
interprete
• lʼinterprete deve necessariamente essere eseguito ogni volta
che deve essere eseguito il programma;
• durantre lʼesecuzione del programma la memoria del sistema
viene occupata sia dallʼinterprete (che deve essere
necessariamente eseguito) che dal programma da eseguire;
• poiché lʼinterprete traduce e fa eseguire alla CPU istruzione
per istruzione il programma, il tempo di esecuzione di ogni
istruzione è sovraccaricato dal tempo di traduzione
dellʼistruzione stessa. Il risultato è quello di avere tempi di
esecuzione più alti rispetto a quelli effettivamente necessari
allʼesecuzione dellʼistruzione da parte della CPU;
• lʼutilizzo dellʼinterprete rende molto snella ed immediata
la gestione della correzione e verifica dei programmi in fase di test;
15.2.2 Il compilatore
Il compilatore è un programma in grado di tradurre in linguaggio
macchina uno o più file sorgenti scritti in un particolare linguaggio di
programmazione. Esso fornisce come output un file eseguibile. Tale
file è il risultato della traduzione dei file sorgenti ed è quindi
direttamente comprensibile dalla CPU. Quindi per la sua esecuzione
non è necessario avere il compilatore.
La compilazione scinde la fase di traduzione delle istruzioni contenute
nei file sorgenti da quella di esecuzione. Essa è relativa soltanto alla
fase traduzione di un programma in linguaggio macchina ma non è
legata in alcun modo alla sua esecuzione. In particolare, la
compilazione di un programma si compone di due fasi distinte
(v. fig. 15.5):
compilazione è la fase di traduzione dei file sorgenti in
linguaggio macchina e fornisce come risultato dei file intermedi
detti file oggetto che non sono comprensibili dal sistema, ma
servono per la fase di linking;
linking è la fase di collegamento (link) dei file oggetto e delle
librerie (statiche) per la produzione del file eseguibile;
In genere un programma si appoggia a delle librerie di sistema (ed
eventualmente anche a delle librerie specifiche) per poter essere
avviato, terminato ed eseguito. Il codice eseguibile presente in tali
librerie deve essere inserito nel file eseguibile, risultato della
compilazione del programma, in maniera tale che il file stesso risulti
indipendente.
Di seguito sono elencati i vantaggi e gli svantaggi dellʼutilizzo di un
compilatore
• il compilatore è necessario soltanto in fase di traduzione dei
file sorgenti, non durante lʼesecuzione del programma. Il tempo
di compilazione è generalmente non trascurabile;
• durante lʼesecuzione del programma la memoria del sistema
viene occupata soltanto dal programma da eseguire;
• poiché il file eseguibile prodotto dalla compilazione è già
comprensibile dalla CPU, il tempo di esecuzione è quello
strettamente necessario allʼesecuzione delle istruzioni. Non ci
sono tempi morti durante lʼesecuzione;
• lʼutilizzo del compilatore rende un poʼ lenta la gestione della
correzione e verifica dei programmi in fase di test (perché il
tempo di compilazione può essere lungo);
15.3 I metodi di programmazione
Scrivere programmi è unʼattività delicata poiché sebbene sia
relativamente facile scrivere programmi, è difficile scrivere buoni
programmi. Un buon programma non è quello che funziona, ma
quello che, oltre a funzionare, permette unʼagevole manutenzione. Di
seguito sono elencate delle regole generali utili per la scrittura di
buoni programmi, anche se lʼesperienza e le capacità del
programmatore rimangono comunque i fattori determinanti della
bontà di un programma.
Quando si scrivono programmi, oltre a conoscere bene il
linguaggio di programmazione utilizzato (la sua sintassi, le sue
funzioni, ...) la prima cosa da non sottovalutare è la leggibilità del
codice. Al computer non importa affatto della leggibilità del codice,
ma è sufficiente che non vi siano errori di sintassi per poter eseguire
il programma. Bisogna però considerare che quel programma andrà
manutenuto o comunque eventualmente corretto e quindi la sua
correzione (che potrebbe avvenire anche in un momento piuttosto
distante da quello di termine dei lavori o da persone diverse da quelle
che lo hanno scritto) risulterà di gran lunga agevolata quanto più sarà
facile comprenderne il codice. A tal fine, oltre ad essere
estremamente
consigliato
lʼutilizzo
di
un
linguaggio
di
programmazione ad alto livello, si possono utilizzare vari espedienti,
come lʼuso di nomi esplicativi di varibili e funzioni (v. sez. 15.4), lʼuso
adeguato di commenti e dellʼindentazione2 delle istruzioni, ...
Per migliorare la leggibilità del codice è anche opportuno seguire
le regole della programmazione strutturata3. Il riutilizzo di porzioni
di codice già scritte è da incoraggiare, ma ciò non deve andare a
scapito della comprensibilità del codice. In pratica, si tratta di
eliminare i salti nellʼordine di esecuzione del codice a blocchi di
istruzioni scritti qua e là nel programma (questo generalmente viene
fatto con istruzioni tipo goto4), preferendo una logica di esecuzione
sequenziale ed utilizzando costrutti più vicini alla logica umana, come
quelli seguenti
istruzioni condizionali - costrutto if ... then ... else se la
condizione X è vera
allora
esegui
le seguenti
istruzioni ... altrimenti esegui le seguenti istruzioni ...
cicli di istruzioni - costrutto do ... while (o ciclo for) s
e la
condizione X è vera esegui le seguenti istruzioni ... ritorna ad
eseguire il flusso del programma a partire dallʼistruzione “se la
condizione X è vera ...”
La sintassi dei linguaggi ad alto livello contiene questi tipi di costrutti,
che aiutano a rendere più agevole e chiara la scrittura del codice.
Esiste comunque anche un uso dei salti di esecuzione allʼinterno del codice che va incontro
programmi. Questo accade quando tale istruzione è utilizzata per uscire dai cicli (anche annidati),
per tale esigenza.
Parti di codice indipendenti e già collaudate, che devono essere
riusate spesso nei programmi, possono essere utilizzate per formare
delle librerie (ovvero scritte e compilate una volta per tutte e messe a
disposizione per ulteriori utilizzi).
Inoltre, quando ci si accinge a scivere un programma è opportuno
chiedersi se qualcunʼaltro si sia già scontrato con il problema che si
tenta di risolvere, in modo da poterne eventualmente riutilizzare parte
del codice (che magari è anche già testato).
Sempre nellʼottica di migliorare la riusabilità del codice, la sua
comprensione e manutenibilità, è stata introdotta negli anni ʼ90 la
programmazione ad oggetti (OOP - Object Oriented Programming)
che estende il concetto classico di programmazione funzionale (o per
funzioni), suddividendo il programma in un insieme di oggetti
indipendenti di più facile manutenzione che possono essere riutilizzati
in altri programmi, come blocchi precostruiti, agevolandone la
realizzazione. Sono così nati i linguaggi di programmazione ad
oggetti (come il Java, il C++, lʼEiffel, ...) che permettono la scrittura di
codice ad oggetti. In particolare, la programmazione ad oggetti ha
delle potenzialità in più rispetto alla programmazione funzionale,
poiché ha il vantaggio di avere una relazione gerarchica tra gli
oggetti, che permette di gestirne lʼereditarietà, ovvero la
specializzazione degli oggetti ed il polimorfismo, cioè la capacità di
un oggetto di comportarsi come un suo “antenato”.
Infine, per avere ulteriori garanzie di sicurezza, si può sfruttare la
tecnica di programmazione per contratto (design by contract) di B.
Meyer. Essa si basa sullʼutilizzo di test sui dati subito prima della
chiamata di un metodo (precondition) e subito dopo il suo ritorno
(postcondition). In questo modo se il test fallisce significa che è inutile
proseguire perché si andrebbe incontro a una situazione
imprevedibile. Questo sistema rende leggermente più lenta la fase di
scrittura del codice a vantaggio di una più agevole fase di debug.
15.4 I linguaggi di programmazione
I linguaggi di programmazione si possono dividere in due grandi
categorie dipendentemente dal livello di astrazione da essi fornito. I
linguaggi di basso livello sono quelli più vicino alla logica della
macchina, non utilizzano i costrutti della programmazione strutturata
e fanno uso di registri e indirizzi di memoria e pertanto sono
dipendenti dalla piattaforma considerata. Lʼunico linguaggio di
programmazione di basso livello è lʼassembly (che varia
dipendentemente dalla CPU considerata). Per queste caratteristiche i
linguaggi di basso livello permettono il massimo controllo sulla
macchina e quini permettono di sfruttarne appieno tutte le
potenzialità, a scapito della facilità di scrittura del codice.
I linguaggi ad alto livello si avvicinano più alla logica umana,
astraendo dai meccanismi logici della macchina: fanno uso di codici
mnemonici per lʼidentificazione dei dati ed utilizzano i costrutti della
programmazione strutturata. Per questo i linguaggi di alto livello sono
generalmente meno performanti in termini di velocità di esecuzione
rispetto agli analoghi sviluppati in un linguaggio di programmazione a
basso livello.
Esitono anche linguaggi di alto livello che permettono lʼaccesso
agli indirizzi di memoria ed ai registri della CPU. Questo è il caso del
C (e C++), che per questo può essere considerato un linguaggio ad
alto livello con caratteristiche di basso livello che lo rendono molto
potente.
15.4.1 La memorizzazione dei dati
I linguaggi di programmazione di alto livello introducono apposite
strutture per la memorizazione dei dati ed inoltre permettono al
programmatore di assegnare un nome ad ogni locazione di memoria
effettivamente utilizzata nel corpo del programma per la
memorizzazione dei dati. Questo meccanismo di astrazione dalla
logica di memorizzazione dei dati a basso livello, rende più facile
riferirsi ai dati stessi, piuttosto che specificare esplicitamente i relativi
indirizzi di memoria, compito questo che viene totalmente demandato
al linguaggio di programmazione stesso (cioè al compilatore o
allʼinterprete). Inoltre, se il nome utilizato sarà scelto dal
programmatore in maniera oculata, sarà anche più agevole leggere il
programma stesso e capirne la logica.
Ad esempio, in un programma di contabilità, sarebbe opportuno
denominare una locazione di memoria atta a contenere un valore
intero che rappresenta lʼanno di riferimento come anno o anno_rif
piuttosto che utilizzare un nome generico tipo a, o valore, o ancora
pippo, che a distanza di pochi giorni dalla scrittura del codice a poco
servirebbe per indicare il significato della locazione di memoria anche
per lo stesso autore del programma.
15.4.1.1 Le costanti e le variabili
Esistono essenzialmente due tipi di utilizzo delle locazioni di
memoria: per la memorizzazione di dati che non possono essere
modificati durante il corso del programma o per la memorizzazione di
dati che possono essere modificati durante il corso del programma
(questo è il caso più frequente). Nel primo caso si parla di costanti,
ovvero il contenuto della locazione di memoria, che il sistema
riserverà per la memorizzazione del dato dichiarato come costante,
non potrà essere modificato durante il corso del programma:5
soltanto inizialmente sarà possibile poterlo modificare (per poterci
memorizzare il valore desiderato). Nel secondo caso si parla invece
di variabili6.
La dichiarazione di una costante/variabile indica al sistema di
riservare un apposito spazio in memoria per memorizzare un dato,
cioè una zona di memoria viene allocata (riservata) per la
memorizzazione di una particolare informazione.
Le costanti o le variabili possono essere quindi pensate come dei
contenitori, ognuno con il proprio nome (deciso dal programmatore),
in grado di contenere dei valori numerici, alfanumerici o logici,
dipendentemente dal tipo di cui sono state dichiarate. Poiché il
sistema tratta tutte le informazioni come valori numerici (v. sez. 1.3)
indicare il tipo di dato che una variabile conterrà indica al linguaggio
di programmazione il meccanismo di interpretazione dei valori in essa
contenuti: ad esempio se in una variabile è contenuto il valore
100010012 esso potrebbe indicare il numero intero senza segno 13710
oppure il numero con segno -11910 (se la locazione di memoria
utilizza solo 8 bit)7 o anche il carattere ASCII esteso con codice 89H.
Inoltre la tipizzazione dei dati aiuta il programmatore a non effettuare
errori inserendo inavvertitamente dati di un tipo allʼinterno di una
costante/variabile
dichiarata
di
tipo
diverso,
poiché
il
compilatore/interprete controllano tale occorrenza generando un
messaggio di errore.8
Ad esempio, nel linguaggio C si possono definire le costanti c_a
e c_b e le variabili v_a e v_b come riportato nellʼesempio
seguente
const int c_a = 4;
const char c_b = 'q';
int v_a;
char v_b;
In questo modo, le costanti c_a e c_b conterranno rispettivamente
il valore 4 e la lettera ʻqʼ. La variabile v_a potrà contenere al suo
interno un valore intero, mentre la variabile v_b potrà contenere al
suo interno un valore alfanumerico. In realtà quello che succede è
che le locazioni di memoria a cui fanno riferimento le costanti e le
variabili conterranno sempre valori numerici (v. sez. 1.3), ma questi
saranno opportunamente codificati/decodificati dal programma
dipendentemente dal tipo dichiarato per la costante/variabile
considerata.
I valori possono essere memorizzati allʼinterno di variabili per mezzo
di unʼistruzione particolare, detta assegnamento. Tale istruzione
cancella il valore precedentemente memorizzato nella variabile
considerata, inserendovi un nuovo valore.
Ad esempio, nel linguaggio C si può assegnare il valore 46 alla
variabile v_a ed il valore ʻeʼ alla variabile v_b con le istruzioni
riportate nellʼesempio seguente
v_a = 46;
v_b = 'e';
In questo modo, nella locazione di memoria a cui si riferisce la
variabile v_a verrà memorizzato il valore 001011102 cioè quello
corrispondente al numero decimale 46, mentre nella locazione di
memoria a cui si riferisce la variabile v_b verrà memorizzato il valore
011001012 cioè quello corrispondente alla codifica ASCII del carattere
ʻeʼ (v. tab. 1.3).
Figura 15.
6:
Esempi di assegnamento di
variabili.
15.4.1.2 Le strutture dati
Alcuni linguaggi di programmazione permettono di definire delle
strutture dati, ovvero nuovi tipi di dati formati da agglomerati di tipi di
dati già definiti. Ad esempio si potrebbe definire il tipo di dato che
serve per la gestione dei numeri complessi a + jb, dove j è lʼunità
immaginaria (j2 = -1). Questo può essere pensato come lʼinsieme di
due dati numerici in grado di memorizzare numeri con parte
decimale: il primo dato rappresenterà la parte reale (a) del numero
complesso, mentre il secondo rappresenterà quella immaginaria (b).
La struttura dati definibile in C è quella riportata nellʼesempio
seguente
struct Complesso
{
double re;
double im;
}
In questo modo una variabile var, dichiarata di tipo Complesso,
avrà due proprietà o campi: var.re che rappresenta la parte reale e
var.im che rappresenta quella immaginaria del numero complesso
memorizzato appunto in var.
15.4.1.3 I vettori
Spesso è utile avere una serie di variabili da utilizzare per compiere
su di esse operazioni identiche, senza però dover necessariamente
scrivere varie linee di codice sostanzialmente uguali.
Ad esempio si supponga di avere a che fare con una serie di
valori numerici e che ad un certo punto dellʼelaborazione a tali valori
deve essere sommato il contenuto di una determinata variabile x. In
questo caso è conveniente che la serie di valori possa essere
memorizzata in un vettore (array), cioè un insieme di variabili
contigue alle quali si può far riferimento con il medesimo nome
distinguendole una dallʼaltra per mezzo di un indice, che indica la
posizione della stessa. Analogamente a quelli usati in algebra lineare,
i vettori possono essere pensati come una variabile con un numero n
di componenti: dalla 1 alla n (alcuni linguaggi di programmazione,
come il C, numerano le componenti partendo da 0 anziché che da 1).
Quindi, supponendo ad esempio di avere 10 valori numerici
memorizzati nel vettore vett ad ognuno dei quali si voglia
semplicemente aggiungere il valore memorizzato nella variabile x, in
C si possono utilizzare le istruzioni riportate nellʼesempio seguente
int vett[10];
...
for (i=0; i<10; ++i)
{
vett[i] += x;
}
...
In questo modo si indica al programma di ripetere il blocco di
istruzioni comprese tra le parentesi graffe, incrementando di volta in
volta di una unità il valore contenuto nella variabile i (++i), alla
quale viene assegnato come valore iniziale 0 (i=0), finché la stessa
variabile i risulta minore di 10 (i<10). Quindi il programma eseguirà
lʼistruzione vett[i] += x; con i che va da 0 a 9, dopodiché
continuerà ad eseguire le istruzioni successive nel programma.
Lʼistruzione vett[i] += x; indica di incrementare il valore
contenuto in vett[i] con quello contenuto nella variabile x (in
maniera meno criptica tale operazione può essere scritta come
vett[i] = vett[i] + x;).
Si noti che per mezzo dellʼuso dei vettori in questo caso si scrivono
soltanto 3 righe di codice, piuttosto che una per ogni componente del
vettore (cioè 10).
È opportuno sottolineare che lʼuso di vettori presuppone la
conoscenza a priori del limite massimo degli elementi del vettore,
poiché quando un vettore viene dichiarato, il compilatore ha la
necessità di sapere quanta memoria dedicargli. Quindi, una volta
compilato, un programma che fa uso di un vettore di n elementi non
ne può utilizzare n + 1.
15.4.1.4 Lʼallocazione dinamica della memoria
Non sempre il programmatore è in grado di conoscere a priori il
numero di elementi da memorizzare e nemmeno è in grado di poterlo
stimare. È generalmente impossibile lʼutilizzo di vettori per la
memorizzazione dei dati.9 Si può ricorrere quindi ad una tecnica di
allocazione dinamica della memoria.
Alcuni linguaggi di programmazione mettono a disposizione del
programmatore una serie di funzionalità che permettono la gestione
dellʼallocazione della memoria in maniera dinamica: durante
lʼesecuzione delle istruzioni, il programma, se opportunamente
istruito, può richiedere al sistema operativo la memoria di cui
necessita per la memorizzazione di dati e questʼultimo (se cʼè
memoria disponibile) gliela riserverà, in maniera tale che altri
programmi non possano utilizzarla. In seguito, quando la memoria
non sarà più utilizzata dal programma, questʼultimo potrà comunicarlo
al sistema operativo che non la renderà più accessibile al
programma.
La memoria utilizzata per lʼallocazione dinamica è la cosiddetta
memoria heap10, che nei processori Intel X386 è costituita da una
parte dello stack (v. sez. 15.4.5). Questa gestione della memoria è
generalmente a carico del programmatore, anche se i linguaggi di
ultimissima generazione tendono ad avere una gestione automatica
del recupero della memoria inutilizzata (garbage collection).
15.4.1.5 Le liste concatenate
In alcuni linguaggi di programmazione è possibile definire particolari
strutture dati come le liste concatenate (chained list). Una lista
(v. fig. 15.9) non è altro che un elenco di elementi concatenati lʼun
lʼaltro, come in un grafo. Ogni elemento della lista è detto nodo ed in
esso, oltre allʼinformazione vera e propria (data), è memorizzato
lʼindirizzo della locazione di memoria nella quale è presente il nodo
successivo (next) ed eventualmente anche quello della locazione di
memoria nella quale è memorizzato il nodo precedente (prev)11. Così
facendo è possibile raggiungere i vari elementi della lista scorrendoli
dal primo fino a quello interessato (nel caso di lista doppiamente
concatenata si possono scorrere i nodi anche in senso inverso): non
è possibile accedere in maniera diretta allʼn-esimo elemento come
invece avviene con i vettori, per il fatto che i nodi sono memorizzati in
zone di memoria non (necessariamente) contigue.
Un particolare tipo di lista è la lista ciclica che ha una struttura tale
che lʼultimo nodo indica come successivo il primo ed il primo nodo
indica come precedente lʼultimo. In questo modo la lista risulta
circolare (non cʼè più un primo o un ultimo nodo).
Queste strutture dati utilizzano generalmente lʼallocazione
dinamica della memoria. Quando cʼè la necessità di un ulteriore nodo,
il programma alloca la memoria necessaria, cioè la richiede al
sistema operativo che gliela rende disponibile; poi quando il nodo non
è più utilizzato, il programma restituisce la relativa memoria al
sistema.
15.4.1.6 Le pile
Esistono anche strutture dati particolari, dette sequenziali, poiché i
dati in esse memorizzati possono essere prelevati soltanto seguendo
lʼordine di memorizzazione o il suo inverso. Una pila (stack) è una
struttura nella quale i dati possono essere prelevati in ordine inverso
a quello nel quale sono stati memorizzati, cioè lʼultima informazione
memorizzata è quella che sarà possibile prelevare per prima. Per
questo motivo si parla anche di memorizzazione di tipo LIFO (Last In
First Out).
15.4.1.7 Le code
Una coda (queue) è una struttura nella quale i dati possono essere
prelevati nello stesso ordine di quello nel quale sono stati
memorizzati, cioè la prima informazione memorizzata è quella che
sarà possibile prelevare per prima. Per questo motivo si parla anche
di memorizzazione di tipo FIFO (First In First Out).