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.
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;
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';
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; }
Il linguaggi di programmazione ad oggetti si spingono oltre a questo. In tali linguaggi è possibile definire apposite operazioni possibili sulle varie strutture dati (le classi) ed è anche possibile ridefinire gli operatori standard. Ad esempio si può pensare di ridefinire l’operatore + per effettuare la somma tra due numeri complessi che operi come segue: (a + jb) + (c + jd) = (a + c) + j(c + d) ovvero sommi tra loro le parti reali e faccia altrettanto con le parti immaginarie.
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; } ...
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.
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).
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.
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).
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).