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