Capitolo 3

Transcript

Capitolo 3
3. Il Binding
I programmi sono composti di entità come variabili, routines e istruzioni. Ogni entità possiede certe
proprietà chiamate attributi. Per esempio, una variabile ha un nome, un tipo, un’area di memoria
riservata per la memorizzazione del proprio valore; una routine ha un nome, dei parametri formali
di un certo tipo, delle convenzioni sul passaggio dei parametri; un’istruzione ha delle azioni
associate. I valori degli attributi devono essere specificati prima che l’entità possa essere usata.
Specificare il valore di un attributo è un’azione detta binding. Per ogni entità, le informazioni sugli
attributi sono contenute in un apposito record detto descrittore.
Il binding è un concetto fondamentale nella definizione della semantica di un linguaggio di
programmazione. I linguaggi di programmazione si differenziano nel numero di entità che essi
possono manipolare, nel numero di attributi associati ad ogni entità, nel momento in cui ha luogo il
binding (binding time) e nella stabilità del binding (cioè, se una volta stabilito il binding possa
essere modificato). Ci sono quattro momenti in cui il binding può avere luogo. Essi sono:
•
•
•
•
Binding durante la definizione del linguaggio. Nella maggior parte dei linguaggi il tipo
intero è associato alla sua controparte matematica, con tutte le operazioni algebriche che lo
caratterizzano, già all’atto della definizione del linguaggio.
Binding durante l’implementazione del linguaggio. Consideriamo sempre il binding del
tipo intero. Uno degli attributi di un tipo di dato è lo spazio in memoria da riservare per la
sua rappresentazione. Varie implementazioni dello stesso linguaggio posso decidere di
utilizzare 16, 32 o 64 bit.
Binding durante la traduzione del programma. Nel linguaggio Pascal, ad esempio, il tipo
integer è predefinito, ma nulla vieta al programmatore di ridefinirlo. In questo caso quindi, il
binding del tipo intero avviene sì durante la definizione e l’implementazione del linguaggio,
ma esso può essere modificato all’atto della traduzione del programma.
Binding durante l’esecuzione del programma. Vi sono linguaggi di programmazione che
non richiedono che il tipo di una variabile venga definito a priori, in questo caso il binding
della variabile con il suo tipo viene effettuato solo durante l’esecuzione del programma e
può subire vari cambiamenti durante l’esecuzione.
Quando il binding viene stabilito prima dell’esecuzione del programma ed è, per questo, di solito
non modificabile, si dice che è statico, mentre quando esso avviene a tempo di esecuzione si dice
dinamico.
3.1 Le variabili
I calcolatori tradizionali si basano sull’utilizzo di una memoria principale formata da celle
elementari ognuna delle quali è identificata da un indirizzo. Il contenuto di una cella è la
rappresentazione codificata di un valore. Un valore è un’astrazione matematica; la sua
rappresentazione codificata in una cella di memoria può essere letta e modificata durante
l’esecuzione di un programma. La modifica consiste nel sostituire la codifica corrente con un’altra.
I linguaggi di programmazione basati sul paradigma imperativo possono essere visti come
un’astrazione, a vari livelli, del comportamento dei calcolatori tradizionali. In particolare, essi
introducono la nozione di variabile come un’astrazione della nozione di cella di memoria, il nome
1
di una variabile come un’astrazione dell’indirizzo di memoria e l’istruzione di assegnamento come
un’astrazione della modifica del valore contenuto in una cella di memoria.
Formalmente, una variabile è una quintupla < nome, scope, tipo, l-value, r-value >, dove
•
•
•
•
•
nome è una stringa di caratteri usata per rappresentare la variabile all’interno delle
istruzioni;
scope rappresenta la porzione di programma in cui la variabile può essere usata;
tipo è il tipo di dato associato alla variabile;
l-value è la porzione di memoria riservata alla variabile;
r-value è il valore codificato memorizzato nell’area di memoria riservata alla variabile.
3.1.1 Nome e scope
Il nome di una variabile viene introdotto di solito attraverso un’istruzione speciale, detta
dichiarazione e lo scope della variabile si estende normalmente dal punto in cui avviene la
dichiarazione a un certo altro punto la cui specifica dipende dal linguaggio. Si dice di solito che una
variabile è visibile all’interno del suo scope mentre è invisibile al suo esterno. Linguaggi di
programmazione diversi adottano regole diverse per eseguire il binding dello scope di una variabile.
Illustriamo queste differenze con alcuni esempi.
#include <stdio.h>
main()
{
int x,y;
scanf(“%d %d”,&x,&y);
{
int temp;
temp=x;
x=y;
y=temp;
}
printf(“%d %d”,x,y);
}
In questo programma C la dichiarazione di x e y le rende visibili all’interno dell’intero programma.
Il programma contiene un blocco interno che raggruppa una dichiarazione e alcune istruzioni. La
dichiarazione di temp la rende visibile soltanto all’interno del blocco in cui essa è effettuata. Ciò
significa che sarebbe un errore utilizzare temp all’interno dell’istruzione printf. Sempre
nell’esempio in questione, se nel blocco interno aggiungessimo la dichiarazione di una variabile
locale x, la variabile globale x definita nel main diventerebbe invisibile all’interno del blocco poiché
mascherata dalla dichiarazione locale con lo stesso nome. Essa torna ad essere visibile non appena il
flusso del programma esce dal blocco interno.
Il binding di una variabile al suo scope può avvenire sia staticamente che dinamicamente. Il binding
statico dello scope avviene sulla base della struttura lessicale del programma, cioè ogni riferimento
ad una variabile viene staticamente associato ad una dichiarazione semplicemente esaminando il
testo del programma senza eseguirlo. Questa soluzione è quella maggiormente usata. Il binding
dinamico dello scope avviene sulla base di una particolare esecuzione del programma. Tipicamente
ogni dichiarazione di variabile estende i suoi effetti su tutte le istruzioni successive finché non si
2
incontra una nuova dichiarazione della stessa variabile durante l’esecuzione. Illustriamo il
funzionamento dello scoping dinamico per mezzo di un esempio.
{
/* blocco A */
int x;
…..
}
…..
{
/* blocco B */
int x;
…..
}
…..
{
/* blocco C */
…..
x = …..;
…..
}
Se il linguaggio segue la regola dello scoping dinamico, un’esecuzione del blocco A seguita dal
blocco C fa sì che la variabile x nell’assegnamento del blocco C sia riferita a quella dichiarata nel
blocco A. Un’esecuzione del blocco B seguita dal blocco C, invece, fa sì che la variabile x
nell’assegnamento del blocco C sia riferita a quella dichiarata nel blocco B. La regola dello scoping
dinamico è molto semplice e facile da implementare, ma ha un grosso svantaggio in termini di
disciplina di programmazione ed efficienza dell’implementazione. I programmi diventano difficili
da leggere perché l’identità di una variabile è determinata di volta in volta sulla base di una
particolare esecuzione e quindi non può essere stabilita a priori.
3.1.2 Tipo
Il tipo di una variabile è definito come l’insieme dei valori che possono essere associati alla
variabile e delle operazioni che possono essere usate per creare, accedere e modificare tali valori.
Una variabile di un dato tipo è detta istanza del tipo.
Quando un linguaggio viene definito, certi tipi sono direttamente associati a certe classi di valori
con le relative operazioni. Ad esempio il tipo integer e gli operatori relativi sono associati alla
propria controparte matematica. I valori e le operazioni sono associati ad una certa rappresentazione
sulla macchina quando il linguaggio è implementato. Quest’ultimo tipo di binding può restringere
l’insieme dei valori che possono essere rappresentati sulla base di quanto spazio è riservato in
memoria per il tipo in questione.
In alcuni linguaggi, il programmatore può definire nuovi tipi per mezzo di una dichiarazione di tipo.
Per esempio, in Pascal si può scrivere
type vector = array[1..10] of integer;
3
Questa dichiarazione stabilisce un binding, a tempo di traduzione, tra il tipo vector e la sua
implementazione, cioè un array di 10 elementi interi. Come conseguenza di questo binding, il tipo
vector eredita tutte le operazioni che caratterizzano il tipo array sulla base del quale esso è definito.
Alcuni linguaggi supportano la definizione di veri e propri tipi definiti dall’utente permettendo di
definire sia l’insieme di valori rappresentati sia le operazioni applicabili su ogni sua istanza.
Vedremo in seguito come questo meccanismo è la struttura portante su cui si basano i linguaggi
orientati agli oggetti.
La maggior parte dei linguaggi effettua il binding delle variabili con il loro tipo a tempo di
traduzione e il binding non può essere cambiato durante l’esecuzione. Questa soluzione è chiamata
typing statico. In questi linguaggi, il binding tra variabile e tipo è specificato dalla dichiarazione di
variabile. Per esempio, in Pascal possiamo scrivere:
var x,y : integer;
var c : character;
Dichiarando che una variabile appartiene ad un determinato tipo, si fa in modo che le variabili siano
protette automaticamente dall’applicazione di operazioni illegali. Nel nostro esempio, il compilatore
è in grado di individuare l’occorrenza di assegnamenti illegali come:
x : = c;
y : = not y;
poiché essi violano le dichiarazioni precedentemente effettuate. La possibilità di eseguire dei
controlli prima che il programma sia eseguito (type checking statico) contribuisce ad anticipare
l’individuazione degli errori e facilita la lettura dei programmi.
In alcuni linguaggi (come il BASIC) la prima occorrenza di una nuova variabile è considerata anche
una dichiarazione implicita. Il vantaggio dell’utilizzazione delle dichiarazioni esplicite rispetto a
questo metodo sta nella maggior chiarezza e leggibilità del programma poiché problemi come gli
errori di battitura nel nome di una variabile possono essere individuati a tempo di traduzione. Per
esempio, in BASIC la situazione in cui si abbia una dichiarazione implicita di una variabile ALPHA
= 1 seguita poi da un’istruzione ALPA = ALPHA + 1, contenente un evidente errore di battitura,
non sarebbe individuata come errore. Bensì ALPA viene interpretata come una nuova variabile
dichiarata implicitamente. Va comunque notato come le dichiarazioni implicite o esplicite di una
variabile non differiscono dal punto di vista semantico. Entrambe eseguono il binding del tipo a
tempo di traduzione, l’unica differenza è che nella dichiarazione esplicita si richiede al
programmatore di specificare il tipo della variabile utilizzata, mentre in quella implicita il traduttore
lo determina automaticamente all’atto della prima istanziazione della variabile.
Il typing dinamico consiste nell’effettuare il binding delle variabili con il loro tipo soltanto a tempo
di esecuzione. Le variabili sottoposte a typing dinamico vengono dette variabili polimorfiche.
Secondo questa regola il binding viene eseguito implicitamente con appena un valore viene
assegnato alla variabile. In generale, il typing dinamico impedisce che si possa effettuare type
checking statico: poiché il tipo di una variabile non è noto a priori risulta impossibile controllare se
si verificano violazioni sul tipo prima che il programma sia eseguito. In questo caso, tali violazioni
possono essere scoperte soltanto a tempo di esecuzione attraverso il type checking dinamico. Per
poter eseguire type checking dinamico, le informazioni relative al tipo dinamico di una variabile
devono essere memorizzate nel descrittore per tutto il tempo di esecuzione, mentre per i linguaggi
che adottano il typing statico i descrittori sono necessari soltanto durante la traduzione.
4
3.1.3 l-value
L’l-value di una variabile è l’area di memoria ad essa associata durante l’esecuzione. Il tempo di
vita di una variabile è il lasso di tempo in cui la variabile esiste. L’area di memoria è usata per
memorizzare l’r-value della variabile. Con il termine oggetto si indica la coppia < l-value, r-value >.
L’azione di riservare dell’area di memoria per una variabile, e che quindi stabilisce il binding tra la
variabile e il suo l-value, è detta allocazione di memoria. Il tempo di vita va dal momento in cui
viene allocata la memoria fino a quando tale memoria non viene liberata (deallocazione di
memoria). In alcuni linguaggi, per certe variabili, l’allocazione è eseguita prima dell’esecuzione
mentre la deallocazione avviene soltanto alla terminazione del programma (allocazione statica). In
altri linguaggi, l’allocazione è eseguita durante l’esecuzione (allocazione dinamica) sia
automaticamente a seguito di una dichiarazione che direttamente dal programmatore attraverso
opportune istruzioni, e la deallocazione avviene anch’essa durante l’esecuzione (di solito quando si
sa che la variabile non verrà più utilizzata).
3.1.4 r-value
L’r-value di una variabile è il valore codificato memorizzato nell’area di memoria associata ad una
variabile (cioè il suo l-value). La rappresentazione codificata è interpretata a seconda del tipo della
variabile. Per esempio, una certa sequenza di bit sarà interpretata come un intero se il tipo è intero,
mentre sarà interpretata come una stringa se il tipo è un array di caratteri. Gli l-value e r-value delle
variabili sono i concetti principali legati all’esecuzione di un programma. Le istruzioni accedono
alle variabili attraverso il loro l-value e modificano eventualmente il loro r-value. I termini l-value e
r-value derivano dalla struttura convenzionale delle istruzioni di assegnamento, come x = y. La
variabile che appare a sinistra indica una locazione, mentre quella che appare a destra indica il
contenuto di una locazione, ovvero un valore. D’ora in poi, quando non ci sarà ambiguità di
notazione, useremo semplicemente il termine valore per indicare l’r-value di una variabile.
Il binding tra una variabile e il suo valore è ovviamente dinamico; il binding è stabilito
dall’operazione di assegnamento. Un assegnamento del tipo x = y; fa sì che l’r-value di y sia copiato
nell’area di memoria definito dall’l-value di x, cioè l’r-value di x è modificato. Di solito è anche
possibile fare in modo che il binding di una variabile con il proprio valore non sia più modificabile
una volta stabilito. In questo caso siamo di fronte alla definizione di una costante da parte del
programmatore. Il binding del valore di una costante può avvenire sia a tempo di traduzione (come,
ad esempio, nel Pascal) che a tempo di esecuzione (come avviene nel C dove una costante può
essere definita utilizzando un’espressione contenete anche delle variabili).
Infine, un classico problema che riguarda il binding tra una variabile e il suo valore è il seguente:
qual è il valore di una variabile immediatamente dopo la sua creazione? Alcuni linguaggi
richiedono che il programmatore specifichi un valore iniziale all’atto della dichiarazione della
variabile, altri linguaggi forniscono un’inizializzazione di default (per esempio, gli interi sono
inizializzati a 0, i caratteri sono inizializzati al carattere blank e così via), altri semplicemente
ignorano il problema considerando come valore iniziale della variabile la stringa di bit attualmente
memorizzata nel suo l-value, altri infine bloccano l’utilizzo di una variabile non inizializzata.
3.1.5 Variabili senza nome e puntatori
Alcuni linguaggi consentono l’accesso a una variabile attraverso l’r-value di un’altra variabile. Un
tale r-value è chiamato puntatore alla variabile. Le variabili alle quali si accede attraverso un
puntatore sono dette senza nome. Infatti, l’unico modo per accedere a queste variabile è attraverso
5
una qualche variabile con nome. La variabile senza nome può essere puntata direttamente da una
variabile con nome, oppure può essere puntata da un’altra variabile senza nome che, a sua volta, è
puntata da una variabile con nome. In generale, si può accedere ad un oggetto attraverso una catena
di puntatori (chiamato cammino di accesso) di lunghezza arbitraria.
Se A = < A_nome, A_scope, A_tipo, A_l-value, A_r-value > è una variabile con nome, l’oggetto <
A_l-value, A_r-value > è direttamente accessibile attraverso il nome A_nome all’interno di
A_scope seguendo un cammino di accesso di lunghezza 0. Se B = < --, --, --, B_l-value, B_r-value
>, dove -- sta per un valore non specificato, è una variabile e B_l-value = A_r-value, l’oggetto <
B_l-value, B_r-value > è indirettamente accessibile attraverso il nome A_nome all’interno di
A_scope seguendo un cammino di accesso di lunghezza 1. Allo stesso modo, si può definire il
concetto di oggetto accessibile indirettamente attraverso una variabile con nome seguendo un
cammino di accesso di qualsiasi lunghezza.
Per esempio, in Pascal possiamo definire un tipo PI come puntatore a un intero:
type PI = ^ integer;
Possiamo quindi allocare una variabile intera senza nome e puntarla attraverso una variabile pxi di
tipo PI:
var pxi : PI;
new(pxi);
Per poter accedere all’oggetto senza nome puntato da pxi è necessario usare l’operatore ^ che,
applicato ad una variabile puntatore, restituisce il suo r-value, cioè l’l-value dell’oggetto desiderato.
Per esempio, possiamo porre a zero la nostra variabile senza nome scrivendo:
pxi ^ : = 0;
Si può accedere alla variabile senza nome anche tramite un cammino di accesso di lunghezza 2 nel
seguente modo:
type PPI = ^ PI;
var ppxi : PPI;
new(ppxi);
ppxi ^ : = pxi;
Qui l’r-value di ppxi è l’l-value di una variabile senza nome il cui r-value è l’l-value di una variabile
senza nome il cui r-value è 0. La situazione ottenuta è descritta nella seguente figura.
0
ppxi
pxi
6
Il Pascal consente di puntare solo variabili senza nome. Altri linguaggi come il C consentono di
puntare anche variabili con nome. Ad esempio, il codice C
int x = 5;
int* px;
px = &x;
crea un oggetto intero, il cui r-value è 5, direttamente accessibile attraverso una variabile di nome x
e indirettamente accessibile attraverso px, dichiarato come un puntatore a interi. Questo è ottenuto
assegnando a px l’l-value di x ottenuto attraverso l’operatore &. L’accesso indiretto a x è ottenuto
utilizzando l’operatore *. L’istruzione
*px = 0;
assegna il valore 0 alla variabile puntata da px. In questo caso quindi la modifica si ripercuote anche
sulla variabile x che assumerà il valore 0. In questo caso, infatti, l’oggetto < x_l-value, x_r-value > è
condiviso da x e da px poiché entrambi dispongono di un cammino di accesso ad esso. La
condivisione di un oggetto può essere utile a volte per migliorare l’efficienza di un programma, ma
è comunque una cattiva pratica di programmazione e va usata con attenzione dato che il valore di
una variabile può essere modificato senza che il suo nome venga utilizzato.
3.2 Le routine
I linguaggi di programmazione consentono di strutturare un programma come composizione di un
certo numero di unità chiamate routine. In questo paragrafo analizzeremo le principali
caratteristiche sintattiche e semantiche delle routine e, in particolare, i meccanismi che controllano
il flusso di esecuzione tra le varie routine e la determinazione del binding all’atto dell’esecuzione di
una routine.
Secondo uno standard più o meno consolidato, le routine sono generalmente di due tipi: procedure e
funzioni. Le funzioni ritornano un valore al termine della loro esecuzione; le procedure no.
Riportiamo un esempio di funzione scritta nel linguaggio C.
int sum(int n)
{
int i,s;
s = 0;
for (i = 0; i <= n; i++)
s = s + i;
return s;
}
Così come le variabili, anche le routine hanno nome, scope, tipo, l-value e r-value. Il nome di una
routine è introdotto nel programma per mezzo di una dichiarazione di routine. Lo scope si estende
generalmente dal punto di dichiarazione a qualche punto di chiusura determinato staticamente o
dinamicamente a seconda del linguaggio. Ad esempio, nel linguaggio C lo scope di una funzione si
estende per tutto il file in cui avviene la dichiarazione.
Una routine viene eseguita a seguito di una chiamata nella quale vengono specificati il nome della
routine e i parametri su cui essa deve andare ad operare. Poiché una routine è attivata da una
7
chiamata, l’istruzione di chiamata deve trovarsi all’interno dello scope della routine. Oltre ad avere
il loro proprio scope, le routine definiscono anche uno scope per le dichiarazioni contenute al
proprio interno. Queste dichiarazioni locali sono visibili soltanto all’interno della routine. In base
alle regole di scoping del linguaggio, le routine possono fare riferimento anche a oggetti (variabili o
altre routine) non locali oltre e quelle dichiarate localmente. Oggetti non locali che risultino essere
visibili ad ogni unità del programma sono chiamate oggetti globali.
L’intestazione di una routine definisce il nome della routine, il tipo dei suoi parametri e il tipo del
valore eventualmente ritornato. Brevemente, l’intestazione di una routine definisce il tipo della
routine. Nell’esempio sopra il tipo di sum è “routine con un parametro intero che ritorna un intero”.
Il tipo di una routine può essere definito precisamente attraverso il concetto di segnatura. La
segnatura specifica i tipi dei parametri e il tipo eventualmente ritornato. Una routine fun con
parametri in ingresso T1, T2, …, Tn e valore ritornato di tipo R, può essere specificata dalla
seguente segnatura:
fun : T1 x T2 x … x Tn → R
La chiamata di una routine è di tipo corretto se è conforme al tipo della routine. Ad esempio la
chiamata della funzione sum attraverso l’istruzione i = sum(5); risulta corretta, mentre quella
effettuata tramite l’istruzione i = sum(5.3); è sbagliata.
L’l-value di una routine è un riferimento all’area di memoria in cui è memorizzato il corpo della
routine (cioè l’insieme di istruzioni che la definiscono). L’attivazione di una routine ne causa
l’esecuzione del corpo che costituisce l’r-value della routine. Normalmente, il binding di una
routine con il proprio r-value avviene a tempo di traduzione. Alcuni linguaggi prevedono l’uso di
puntatori a routine, il che fornisce un metodo per ottenere l’l-value di una routine che può essere
assegnato come r-value di un puntatore. Ad esempio, la seguente dichiarazione in C crea un
puntatore a un funzione con un parametro intero che ritorna un intero:
int(*ps)(int);
La seguente istruzione
ps = & sum;
fa puntare ps all’l-value della funzione sum precedentemente definita. Una chiamata a sum può
essere effettuata attraverso ps nel seguente modo:
int i = (*ps)(5);
L’uso dei puntatori a routine fornisce la possibilità di chiamare diverse routine ogni volta che uno
stesso puntatore viene valutato. Questo fornisce un modo per ottenere un binding dinamico.
Alcuni linguaggi distinguono tra dichiarazione e definizione di una routine. La dichiarazione di una
routine ne introduce l’intestazione senza specificarne il corpo. Il nome risulta visibile dal punto
della dichiarazione fino alla fine dello scope. La definizione, invece, specifica sia l’intestazione che
il corpo. Questa distinzione consente di implementare la mutua ricorsione tra routine, come
illustrato nel codice seguente:
8
int A(int x, int y);
float B(int z)
{
int w,u ;
…
w = A(z,u) ;
…
}
int A(int x, int y)
{
float t ;
…
t = B(x) ;
…
}
Quando si effettua una chiamata di routine, questa viene eseguita sulla base dei valori associati ai
parametri. La rappresentazione di una routine durante l’esecuzione è detta istanza della routine.
Un’istanza di routine è composta da un segmento di codice e un record di attivazione. Il segmento
di codice, il cui contenuto è fissato, contiene le istruzioni che definiscono la routine. Il contenuto
del record di attivazione è, invece, variabile. Esso contiene le informazioni necessarie per eseguire
la routine come, ad esempio, gli oggetti associati alle variabili locali della particolare istanza di
routine. La posizione relativa di un oggetto nel record di attivazione è detta offset. Per far sì che la
routine sia in grado di restituire il controllo alla porzione di programma in cui è stata effettuata la
chiamata, un puntatore di ritorno è salvato nel record di attivazione all’atto della chiamata.
L’ambiente di riferimento dell’istanza U di una routine consiste nelle variabili locali di U, che sono
associate agli oggetti memorizzati nel record di attivazione di U, e nelle variabili non locali di U,
che sono associate a oggetti memorizzati nei record di attivazione di altre unità. Gli oggetti locali
definiscono l’ambiente locale, mentre quelli non locali definiscono l’ambiente non locale. La
modifica di oggetti associati a variabili non locali da parte di una routine è detta effetto collaterale.
Nel caso in cui le routine possano essere attivate ricorsivamente, può avere luogo l’attivazione di
una nuova unità prima che quella dell’unità precedente sia terminata. Tutte le istanze di una stessa
unità hanno quindi lo stesso segmento di codice, ma differenti record di attivazione. In presenza di
ricorsione, dunque, il binding tra un record di attivazione e il suo segmento di codice deve essere
necessariamente dinamico.
Quando una routine viene attivata, l’istruzione chiamante deve passare i parametri alla routine
chiamata. Il passaggio dei parametri consente il trasferimento del flusso dei dati tra le varie unità di
un programma. Nella maggior parte dei casi, si possono passare soltanto oggetti. Ci sono però dei
linguaggi che permettono anche il passaggio di routine. Il passaggio di parametri e la
comunicazione attraverso oggetti non locali sono due modi diversi di ottenere il flusso di
informazione tra le unità. L’utilizzo del passaggio dei parametri ha indubbiamente molti vantaggi in
termini di leggibilità e modificabilità.
E’ necessario distinguere tra parametri formali (quelli che appaiono nell’intestazione di una
routine) e parametri attuali (quelli che appaiono nella chiamata della routine). La maggior parte dei
linguaggi di programmazione usa un metodo posizionale per associare i parametri attuali a quelli
formali nelle istruzioni di chiamata di routine. Se l’intestazione di una routine è
9
routine S(F1, F2, …, Fn);
e la sua chiamata
call S(A1, A2, …, An);
il metodo posizionale implica che il parametro formale Fi sia associato al parametro attuale Ai. In
alcuni casi il numero dei parametri attuali e formali può non essere necessariamente identico. Nel
C++ è possibile associare un valore di default ai parametri formali che vengono usati nel caso in cui
i corrispondenti parametri attuali non vengano specificati nella chiamata. Ad esempio, data una
funzione
int distance(int a = 0, int b = 0);
la chiamata distance() è equivalente alla chiamata distance(0,0), mentre la chiamata distance(10) è
equivalente a distance(10,0). Oltre al metodo posizionale, alcuni linguaggi (come l’ADA)
consentono un’associazione nominale dei parametri. Ad esempio, data una procedura
procedure Example(A : T1; B : T2 : = W; C : T3);
con parametri formali A, B e C di tipo rispettivamente T1, T2 e T3 e con un valore di default W
assegnato a B, assumendo che le variabili X, Y e Z siano rispettivamente di tipo T1, T2 e T3,
abbiamo che le seguenti chiamate sono tutte corrette:
Example(X,Y,Z);
Example(X, C => Z);
Example(C => Z, A => X, B => Y);
La prima chiamata usa l’associazione posizionale. La seconda usa l’associazione posizionale per A,
B prende il valore di default W mentre C è associato a Z attraverso l’associazione nominale. Infine,
la terza chiamate usa l’associazione nominale per tutti e tre i parametri.
A volte succede di dover scrivere delle routine molto simili come nel caso in cui si vuole ordinare
sia un array di interi che uno di stringhe. In questo caso sono necessarie due procedure, anche se
l’algoritmo utilizzato è lo stesso, poiché i tipi dei parametri sono diversi. Alcuni linguaggi offrono
la possibilità di definire routine generiche in modo da poter ovviare a questo inconveniente. Le
routine generiche sono dette anche templates. Riportiamo, come esempio, un template scritto in
C++ che scambia il valore di due oggetti dello stesso tipo, qualunque esso sia:
template < class T > void swap(T& a, T& b)
{
T temp = a ;
a=b;
b = temp ;
}
10
3.3 Aliasing e overloading
Fino ad ora abbiamo implicitamente assunto che ogni nome simbolico utilizzato all’interno di un
programma indichi una e una sola entità, sulla base delle regole di scoping del linguaggio utilizzato.
Non è questa la situazione reale. Consideriamo il seguente codice C:
int i,j,k;
float a,b,c;
…
i = j + k;
a = b + c;
In questo esempio, l’operatore + nelle due istruzioni di assegnamento denota due entità diverse.
Nella prima istruzione esso denota l’addizione tra interi, nella seconda quella tra numeri in virgola
mobile. Nonostante il nome delle due operazioni sia lo stesso, il binding tra l’operatore e la
rispettiva operazione è diverso nei due casi e l’esatto binding può essere determinato a tempo di
traduzione analizzando il tipo degli operandi.
Questo esempio può essere generalizzato introducendo il concetto di overloading. Un nome è detto
sovraccarico (overloaded) se si riferisce a più di una entità in un dato punto di un programma e se la
particolare occorrenza del nome fornisce informazioni sufficienti perché il relativo binding possa
essere stabilito univocamente.
L’aliasing è l’opposto dell’overloading. Due nomi sono alias se denotano la stessa entità in un dato
punto di un programma. Questo concetto è particolarmente rilevante nel caso delle variabili. Due
variabili alias condividono lo stesso oggetto nello stesso ambiente di riferimento. Un esempio di
può essere ottenuto attraverso il seguente codice C:
int x = 0;
int *i = &x;
int *j = &x;
11