Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Concetti Fondamentali di Programmazione: Un'Introduzione ai Paradigmi, Dispense di Tecniche E Linguaggi Di Programmazione

Paradigmi per l'esame di Linguaggi di Programmazione

Tipologia: Dispense

2019/2020

Caricato il 24/03/2020

andrea-orsini
andrea-orsini 🇮🇹

4.9

(8)

6 documenti

1 / 24

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Roberto Russo
Paradigmi di Programmazione
2009
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18

Anteprima parziale del testo

Scarica Concetti Fondamentali di Programmazione: Un'Introduzione ai Paradigmi e più Dispense in PDF di Tecniche E Linguaggi Di Programmazione solo su Docsity!

Roberto Russo

Paradigmi di Programmazione

Capitolo 1 – Macchine astratte.

Dato un linguaggio di programmazione L , una macchina astratta per questo linguaggio sarà ML , un insieme di algoritmi e strutture dati che permetteranno la memorizzazione ed esecuzione di programmi scritti in L. Una macchina astratta è composta sostanzialmente da memoria ed interprete. L'interprete compie determinate azioni in base al linguaggio che interpreta appunto. Nonostante esistano infiniti linguaggi le azioni compiute dall'interprete sono principalmente quattro.

  1. Operazioni per l'elaborazione dei dati primitivi.
  2. Operazioni di controllo della sequenza.
  3. Operazioni e strutture dati per il controllo del trasferimento dati.
  4. Operazioni e strutture dati per la gestione della memoria. I dati primitivi del primo caso sono quei dati che sono direttamente rappresentabili dalla macchina: quindi avremo un insieme di operazioni definite (per esempio sui dati di tipo numerico avremo a disposizione la somma, moltiplicazione, ecc. ) per l'elaborazione di questi. L'interprete ha inoltre la possibilità di modificare l'indirizzo della prossima istruzione da eseguire. Può controllare il flusso di operazioni da compiere, e nel caso vi siano dei salti condizionati sarà opportuno disporre di operazioni per la manipolazione degli indirizzi di memoria per l'esecuzione della prossima istruzione. Ovviamente l'interprete avrà a disposizione delle strutture dati atte al trasferimento dei dati (pile e stack) per il trasferimento dei dati dalla memoria all'interprete e viceversa e sarà fornito di alcuni meccanismi per la gestione della memoria. Il ciclo d'esecuzione dell'interprete sarà schematizzato come segue.
  5. Acquisizione della prossima istruzione da eseguire.
  6. Decodifica.
  7. Acquisizione degli operandi.
  8. Esecuzione dell'operazione.
  9. Arresto oppure ripetizione del punto 1. Il linguaggio L compreso dalla macchina ML è detto linguaggio macchina di ML. Una macchina astratta ML utilizzerà dei dispositivi per poter eseguire le istruzioni del linguaggio L. Esistono tre modi per realizzarla.
  10. Realizzazione a livello Hardware.
  11. Realizzazione a livello Software.
  12. Realizzazione a livello firmware. Con la realizzazione a livello hardware si avranno prestazioni maggiori rispetto agli altri casi. Tuttavia è da considerare che se il linguaggio è ad alto livello vi saranno notevoli problematiche in fase di realizzazione. Solitamente quando si parla di realizzazione a livello hardware si ha a che fare con linguaggi di basso livello e con macchine che prevedono scopi dedicati. Con la simulazione software si ha l'idea di un linguaggio L' ed una macchina M'L'. Con i programmi scritti in L' e l'ausilio di M'L' si ha la possibilità di creare ML ma a discapito di una notevole lentezza d'esecuzione. Inoltre con l'introduzione di M'L ' è stato aggiunto un ulteriore livello di astrazione.

Capitolo 2 – Descrivere un linguaggio di programmazione.

Un linguaggio di programmazione è un formalismo artificiale con il quale poter esprimere algoritmi. Questo linguaggio deve essere definito in base alla sua grammatica , semantica e pragmatica. La grammatica è quella parte della descrizione di un linguaggio che risponde alla domanda “Quali frasi sono corrette?”. Per prima cosa nella grammatica si definisce l'alfabeto, tramite quest'ultimo si compongono delle sequenze di lettere, ossia le parole (o token). Una volta definito l'alfabeto e le parole, la semantica seleziona , a partire da tutte le possibili combinazioni di parole, un sottoinsieme di frasi del linguaggio stesso. 1) Grammatica Supponiamo di avere a disposizione un alfabeto A definito come segue: A={a,b}. Dato questo alfabeto ci poniamo il problema di ricavare tutte le parole palindrome. Una generica stringa palindroma P potrà essere una stringa vuota, una a , una b oppure una stringa palindroma preceduta e seguita da una a o una b. P → P → a P → b P → aPa P → bPb La grammatica appena descritta si chiama grammatica libera da contesto. Le grammatiche libere da contesto si possono definire come delle quadruple di non terminali, terminali, produzioni o regole ed infine simbolo iniziale: ciò da come risultato G = (NT,T,P,S). Descriviamo un esempio di operazioni aritmetiche di tipo somma e sottrazione. G = ([{E,I}, {a, b, +, -}, P, E) P = {E → I; E → E + E; E → E – E; E → (E); I → a; I → b; I → Ia; I → Ib} Introduciamo ora il BNF, ossia un modo differente (ma standard e convenzionale) di definire le grammatiche. La grammatica appena descritta si può riscrivere come segue. _ ::= | | | <(E)> ::= | | | _ Ora i può dimostrare che l'espressione a – b + (b – ab) è un'espressione corretta secondo la grammatica appena descritta. Si parte ovviamente con E (simbolo iniziale). Il simbolo ⇒ indica una derivazione. ⇒ E +E (applico una produzione)E – E + EE – E + (E)E – E + (E – E)* I – I + (I – I) (applico più di una regola)* a – b + (b – Ib)a – b + (b – ab)

Si nota che l'ordine con il quale si è proceduto non è obbligatorio e che ci si può ricondurre alla stringa sopracitata anche effettuando le derivazioni in ordine diverso. Quest'ordine può essere rappresentato mediante l'uso degli alberi come segue. Purtroppo questo tipo di rappresentazione ammette ambiguità: quando una stringa può essere espressa con due o più alberi di derivazione. E' necessario dunque, seppur aumentando la complessità, aggiungere tutte le informazioni necessarie alla grammatica affinché non si presentino ambiguità. Si nota ora ciò che avviene in fase di compilazione.

  1. Analisi lessicale : vengono letti sequenzialmente tutti i caratteri da sinistra verso destra, e vengono raggruppati tali caratteri in unità logicamente significative ( parole o token ).
  2. Analisi sintattica : viene generato un albero di derivazione con i token precedentemente acquisiti. Questo procedimento è detto parser , e gli alberi generati avranno come foglie i token e leggendo l'albero da sinistra verso destra si avranno frasi di senso compiuto rispetto alla nostro grammatica.
  3. Analisi semantica : se nella fase precedente viene garantita la correttezza sintattica in questo caso partendo dagli alberi di derivazione vengono controllate le dichiarazioni, e tipi di dato ed il numero di parametri delle funzioni. viene inoltre creata la tabella dei simboli e vengono ampliati gli alberi di derivazione.
  4. Generazione della forma intermedia : Dagli alberi precedentemente ampliati viene generato un codice privo di ottimizzazione.
  5. Ottimizzazione del codice :viene eliminato il codice inutile da codice precedentemente generato (fattorizzazione delle sottoespressioni, ottimizzazione dei cicli, eliminate chiamate a funzione con funzione stessa).
  6. Generazione del codice: viene generato il codice oggetto a partire dal codice ottimizzato, successivamente possono avvenire ulteriori ottimizzazioni come la memorizzazione di alcune variabili in registri del processore. 2) Semantica In questa fase si attribuisce un significato alle frasi ottenute precedentemente. Si analizzano gli stati e il modo con cui questi cambiano in relazione ai comandi eseguiti ossia tutte le varie computazioni. Gli esempi aiutano a capire meglio. _ → t_ Eseguendo il comando c lo stato o muta in t. o[X ← 1] Lo stato o rimane immutato tranne che per la variabile X che assume il valore 1. _ → → → _ Significa che se la prima computazione e la seconda sono vere allora anche la terza lo è.

In ogni ambiente possiamo quindi trovare associazioni sia attive che inattive: le operazioni che si possono svolgere sulle associazioni nell'ambiente sono il naming, il referencing, la disativazione e riattivazione e la distruzione. Gli oggetti possono essere creati, acceduti, modificati e distrutti. La fase di creazione e di assegnazione può essere compiuta in un solo passo.

  1. Scope Statico {int x = 0; void pippo(int n){ x = n+1; // Il valore di x lo si cerca nel blocco esterno. } pippo(3); write(x); // Stampa 4 {int x = 0; pippo(3); write(x); // Stampa 0 } write(x); // Stampa 4 }
  2. Scope Dinamico {const x = 0; void pippo(){ // Il valore di x va cercato nell'ambiente precedente in senso temporale. write(x); // Stampa 1 } void pluto(){ const x = 1; { const x = 2; } pippo(); } pluto(); }

Capitolo 5: La gestione della memoria

La gestione della memoria può essere statica o dinamica. Nel caso in cui il linguaggio non supporti la ricorsione la gestione della memoria in modo statico è sufficiente. In linguaggi come il C dove è lecito allocare dinamicamente la memoria con operazioni tipo MALLOC e FREE si utilizzano strutture opportune dette HEAP, in tutti gli altri casi una PILA è sufficiente. La gestione statica della memoria indica che vengono memorizzate in fase di esecuzione tutte le variabili locali, globali e le costanti: ovviamente non essendoci la “ricorsione” può essere memorizzato anche tutto il resto in modo statico.

La gestione dinamica della memoria mediante PILA avviene rispettando la politica LIFO. Ogni procedura attiva un RDA (record di attivazione) e memorizza le proprie variabili. La PILA detta “pila di sistema” con operazioni di PUSH inserisce un nuovo RDA mentre con operazioni tipo POP elimina un RDA e dealloca lo spazio riservatogli. Un RDA è composto come segue. ● Variabili locali ○ Sono le variabili dichiarate all'interno del blocco stesso, il compilatore in base al loro tipo alloca una quantità di spazio di memoria. In alcuni linguaggi con gli array dinamici non è possibile conoscere a priori la loro capacità. ● Risultati intermedi ○ In alcuni casi è utile memorizzare dei risultati intermedi ancora prima di conoscere il risultato effettivo dell'operazione (es. nell' RDA potrebbe esserci un'associazione tipo x+y → valore). ● Puntatore catena dinamica ○ E' un puntatore all'ultimo RDA inserito nella PILA. L'insieme di questi puntatori prende il nome di CATENA DINAMICA. ● Puntatore catena statica ○ Necessario a gestire le regole di scope statico. ● Indirizzo di ritorno ○ Contiene l'indirizzo della prima istruzione da eseguire all'uscita del blocco. ● Indirizzo del risultato ○ Contiene l'indirizzo in cui è memorizzato l'eventuale parametro di ritorno. ● Parametri ○ Sezione in cui vengono memorizzati i valori passati alla funzione / procedura. La differenza tra una procedura ed una funzione sta nel fatto che la funzione prevede un valore di ritorno, mentre la procedura no. I blocchi possono essere in-line, ossia inseriti di seguito ad un blocco con un annidamento oppure chiamati appunto tramite procedure o funzioni. Nel caso di procedure ovviamente non l'indirizzo del risultato non ha ragione d'esser presente. Per generalizzare utilizzo il termine procedura. Nella pila di sistema che contiene i vari RDA vi è un Frame Pointer ossia il puntatore all'ultimo RDA inserito. Un altro puntatore è lo Stack Pointer il quale indica la prima posizione di memoria libera presenta nella pila. La procedura che “generano” un altro procedura è detta chiamante, la procedura creata è detta chiamata. Il compilatore inserisce opportune righe di codice quando incontra un “creazione” di procedura: al chiamante viene aggiunta un parte di codice detta “sequenza di chiamata” mentre al chiamato vengono aggiunte due porzioni di codice dette prologo ed epilogo. Ciò che avviene nella PILA successivamente alla sequenza di chiamata è: ● Modifica del Program Counter. ● Allocazione di spazio di memoria per il nuovo RDA. ● Modifica del Frame Pointer e Stack Pointer. ● Passaggio dei parametri (tra l' RDA del chiamante ed il nuovo RDA del chiamato). ● Salvataggio dei registri. ● Esecuzione d'inizializzazione (presente solo in alcuni linguaggi, utile ad inizializzare gli elementi del RDA).

Le liste liste libere multiple riducono i costi di allocazione utilizzando due liste con dimensioni diverse dei blocchi. Le dimensioni dei blocchi possono essere statiche oppure dinamiche: in quest'ultimo caso si utilizzano due tecniche (buddy system e Fibonacci). ● Buddy System. ○ I blocchi sono di dimensione pari alle potenze di 2. Se la richiesta è di N viene cercato un blocco di dimensione 2K^ ≥ N. Se tale blocco è libero, viene allocato. Se tale blocco non risulta disponibile si cerca il blocco 2K+1^ ≥ N: di questo blocco si alloca solo metà dello spazio, la metà rimanente si restituisce alla LL. Quando si dealloca un blocco che precedentemente era stato diviso per l'allocazione, questo lo si ricompatto alla metà mancante (nel caso questa sia ancora non allocata). ● Fibonacci. ○ Stesso procedimento del Buddy System ma essendo la serie di Fibonacci “più lenta” rispetto alle potenze di 2, si evita maggiormente il problema della frammentazione interna. Nella struttura dell' RDA precedentemente si è accennato al puntatore a catena statica. Per inserire quest'informazione nell' RDA si procede in due modi. ● Il blocco chiamato è esterno al blocco chiamante. ○ Si prede la profondità del blocco chiamante e si sottrae la profondità del blocco chiamato. La profondità delle procedura si calcola in fase di compilazione poiché il codice del programma è statico. Il puntatore dell' RDA “risale” di N posizioni la catena statica ottenute dal precedente calcolo. ● Il blocco chiamato è interno al blocco chiamante. ○ In questo caso il puntatore a catena statica prende come indirizzo l' RDA del blocco chiamante. Nel caso in cui vi sia una diversità di profondità tra procedure notevole (in realtà questo numero non è quasi mai superiore a 3) , vi saranno rallentamenti nel calcolo del puntatore a catena statica. Per ovviare a questo problema si fa uso dei display. Il Display è in pratica un array di N elementi tanti quanti sono i livelli di annidamento. E' una soluzione più costosa della catena statica poiché introduce appunto questo vettore in più. Grazie a questo meccanismo si riducono gli accessi in memoria ad una costante, ossia 2. In ogni “cella” del display vi è un riferimento all' RDA attivo per quel livello di profondità, quando si esce da questo RDA bisogna riattivare il suo RDA chiamante (pertanto ciò indica che bisogna memorizzare ad ogni entrata anche il riferimento all' RDA chiamante). Nel caso dello scope dinamico è facile pensare che scorrendo la pila di sistema a ritroso si possa trovare le associazioni che non compaiono localmente nell' RDA attivo. Alternativamente a questa soluzione (valida ma poco conveniente) si può utilizzare la A-list (lista delle associazioni) nella quale sono memorizzati tutti i nomi di variabile: ● entrata in un nuovo blocco ○ vengono memorizzati nella lista le nuove associazioni. ● uscita da un blocco ○ vengono eliminate dalla lista le associazioni correnti. ● ricerca di un' associazione ○ si scorre a ritroso la lista alla prima occorrenza e si fa riferimento alla prima occorrenza del nome cercato trovato.

○ il primo inconveniente sta nel fatto che il riferimento avviene in base al nome di ogni singola variabile e non in base ad uno specifico RDA. ○ il secondo inconveniente risiede nell'inefficienza di questo tipo di ricerca. Se si deve ad esempio cercare una variabile globale memorizzata all'inizio dell'esecuzione, bisognerà scorrere l'intera lista. Per ovviare a queste problematiche si introducono le CRT (central referencing table) a discapito di un rallentamento in fase di entrata e uscita da un blocco. Le CRT includono tutti i riferimenti presenti nel programma opportunamente aggiornati in base al blocco attivo a run-time. Ad ogni associazione si indica tramite un flag il fatto che sia attiva o meno ed un relativo puntatore al valore. Inoltre vi può essere una pila nascosta che comprende tutte le associazioni non attive. Ovviamente bisogna tener traccia delle associazioni ogni qual volta queste vengano aggiornate in base all'entrata di un nuovo blocco cosicché all'uscita del tale blocco il valore possa essere ripristinato. In fase di ricerca di un'associazione basta accedere alla CRT e prelevare il puntatore connesso all'associazione cercata.

Capitolo 6: Strutturare il Controllo

In questo capitolo si affronta il problema del controllo di sequenza. Nei linguaggi di basso livello è sufficiente aggiornare il registro PC (Program Counter) mentre nei linguaggi ad alto livello le cose si complicano. Ovviamente nel caso di un operazione aritmetica bisogna specificare l'ordine d'esecuzione delle varie operazioni che compongono un' espressione. Le espressioni sono i componenti essenziali di ogni linguaggio. Un' espressione è un'entità sintattica la cui valutazione produce un valore oppure non termina, nel qual caso l'espressione è indefinita. L'espressione è composta da un operatore o da un'entità singola e da una serie di argomenti. Esistono tre diverse notazioni per esprimere un'espressione. ● Notazione Infissa. ● Notazione Prefissa. ● Notazione Postfissa. La notazione infissa è la formula comune (3 + 5) * 2. Questa notazione necessita di parentesi o di regole di precedenza che indichino l'ordine con il quale eseguire le operazioni: in mancanza di queste si generano ambiguità sul risultato. La notazione prefissa è la formula * ( + (3 5) 2). La valutazione di tale notazione risulta essere più facile della notazione infissa: si fa uso di una pila nella quale memorizzare i simboli e di un contatore che indica il numero degli operandi per ogni operando. L'algoritmo è il seguente:

  1. Lettura simbolo S. PUSH(S) su PILA.
  2. Se S è un operatore C = N dove N = numero di argomenti dell'operatore S.
  3. Se S è un operando C--.
  4. Se C ≠ 0 torna al punto 1.
  5. Se C = 0 a. Applica l'operatore l'ultimo operatore agli operandi inseriti successivamente e ottiene R come risultato. Sostituisce R agli operandi e all'operatore nella PILA. b. Se non vi sono altri simboli tipo operatore vai al punto 6. c. C = N – M con N = numero d'argomenti e M = numero di operatori. Vai al punto 4.
  6. Se la sequenza dell'espressione che rimane da leggere non è vuota vai al punto 1.

● Valutazione con corto circuito ○ Nel caso di operazioni logiche tipo < a == 0 || b/a > 2 > non si valutano entrambi gli operandi per verificare che l'espressione restituisca un valore vero o falso: infatti nel caso in cui il primo operando restituisca un valore vero, indipendentemente dal secondo operando si può concludere che il valore dell'espressione sarà vero. ● Ottimizzazioni ○ Nel caso in cui si presentino una serie di espressioni come < a = v[i]; b = a x a + c + d; > alcuni linguaggi valuteranno prima la seconda espressione (c + d) poiché il valore di a potrebbe non essere ancora disponibile (deve essere effettuato un accesso in memoria). I compilatori possono cambiare l'ordine degli operandi per ragioni di efficienza lasciando il codice in maniera semanticamente equivalente. Un comando è un entità sintattica la cui valutazione non necessariamente restituisce un valore ma può vere un effetto collaterale. Nello specifico si può dire che un comando ha un effetto collaterale quando influenza il risultato di una computazione senza restituire alcun risultato. Un comando di differisce da un'espressione poiché il risultato della valutazione di un'espressione è un valore mentre quello di un comando è un nuovo stato (dovuto ad un nuovo assegnamento). Una variabile “modificabile” in matematica è un'incognita che può assumere un qualsiasi valore appartenente ad un definito insieme. Nello specifico è una sorta di contenitore (con riferimento a memoria fisica) che può assumere un nome e contenere dati di tipo omogeneo (caratteri, interi, ecc). Questo valore può cambiare nel tempo tramite le assegnazioni. Alcuni linguaggi imperativi considerano le variabili come riferimenti ad un valore, altri le considerato come degli identificatori che denotano un valore. Il comando di base che permette di modificare il valore ad una variabile è l'assegnamento (di conseguenza modifica anche lo stato). La stringa x = 2 assegna alla variabile denotata dal nome di x il valore numerico 2. La stringa x = x +1 assegna nella locazione di memoria denotata dalla prima x il valore della variabile denotata dal nome x aggiungendo un'unità numerica. In questo ultimo caso si evince la differenza tra la prima x (l-valore) e la seconda x (r-valore) poiché svolgono ruoli differenti. In generale gli l-valori denotano le locazioni di memoria nelle quali memorizzare i risultati. In alcuni linguaggi (come ad esempio il C), con l'assegnamento, oltre a produrre un effetto collaterale, si ritorna il valore assegnato quindi è corretta una stringa come x = y = 2 (assegna sia ad x che a y il valore y). Gli incrementi: ● x++ ○ restituisce il valore di x, successivamente ne incrementa di un'unità il valore. ● x-- ○ restituisce il valore di x, successivamente ne decrementa di un'unità il valore. ● ++x ○ incrementa di un'unità il valore di x e successivamente ne restituisce il valore. ● --x ○ decrementa di un'unità il valore di x e successivamente ne restituisce il valore. ● x += y ○ incrementa di y il valore di x e successivamente ne restituisce il valore. ● x -= y ○ decrementa di y il valore di x e successivamente ne restituisce il valore.

Oltre al comando d'assegnamento vi sono altri comandi, i quali si dividono principalmente in tre categorie: comandi per il controllo di sequenza, comandi condizionali e i comandi iterativi. I comandi per il controllo di sequenza sono quei comandi tipo il “;” che delimitano l'inizio e la fine di un comando: per esempio < C1 ; C2 > indica che sarà eseguito prima il comando C1 poi il comando C2. In questa categoria vi sono anche i comandi composti , ossia quelli che precedentemente abbiamo definito blocchi, denotati da “{“ ”}” in C e “begin” e “end” in Algol. Il GOTO è un comando che deriva dalle istruzioni Assembly il cui uso rende i codice poco leggibile, difficile da modificare e può creare situazioni complesse (come il salto all'interno di un blocco), pertanto il suo uso è fortemente sconsigliato: inoltre è stato dimostrato che il GOTO non cambia l'espressività di un programma quindi nei moderni linguaggi di programmazione (come JAVA) il GOTO è completamente scomparso. Comandi appartenenti a questa categoria sono anche i comandi RETURN e BREAK i quali rispettivamente terminano l'esecuzione di un'iterazione e di una funzione. Altri comandi di questo tipo sono i comandi che gestiscono le eccezioni. I comandi condizionali sono detti anche di selezione ed esprimono due o più alternative sul proseguo delle istruzioni di un programma. Tra questi analizziamo i comandi IF ed CASE. ● IF b-exp then C1 else C ○ il comando analizza un'espressione booleana ed in base al suo risultato esegue o il comando C1 o il comando C2. Il ramo else può anche essere omesso. L'annidamento di più comandi IF può creare ambiguità facilmente risolvibili. In alcuni linguaggi è presente il suo terminatore ENDIF. ● CASE ○ il comando CASE si comporta in maniera analoga al comando IF annidato. Tuttavia l'uso del CASE rende più leggibile il codice e aumenta l'efficienza in fase di compilazione. Si può utilizzare una tabella di salto cosicché una volta valutata l'espressione ”condizionale” si possa usare questo valore come indice di salto ed eseguire il comando opportuno. Lo svantaggio delle tabelle di salto (sono locazioni di memoria contigue) è lo “spreco” di memoria. Al termine di ogni CASE è opportuno inserire un BREAK come separatore dei comandi altrimenti si eseguono più comandi per una sola valutazione dell'espressione di condizione. I comandi visti si qui permettono d'esprimere computazioni di lunghezze statiche. Con i comandi iterativi alcune porzioni di codice possono essere rieseguite più volte. ● WHILE b-exp do C ○ esegue il comando C1 fintanto che l'espressione booleana è vera. ● FOR i=1 to 10 by 1 do C1 (Algol) oppure FOR exp1 exp2 exp3 do C1 (JAVA) ○ il comando FOR ha subito diversi cambiamenti nel corso degli anni e diversi linguaggi ne hanno fatto un uso differente. Nel caso di Algol il comando FOR è un'iterazione determinata poiché è possibile conoscere a priori il numero di volte che il comando C verrà eseguito: il caso contrario avviene per il C o per JAVA dove il FOR è un'iterazione indeterminata. Il fatto che vengano utilizzati sia iterazioni determinate che indeterminate (ossia sia cicli WHILE che FOR) è per una questione di leggibilità. Nel caso specifico di C o JAVA possiamo notare come il FOR si comporti come abbreviazione del WHILE. ● FOR-EACH e : a (e è un singolo dato, a è un'array di dati tipo il dato e) ○ Le iterazioni sono spesso utilizzate su dati di tipo array (o collezioni di dati dello stesso tipo) e pertanto si può utilizzare questo comando per ottenere un codice più leggibile e più elegante.

● Uscita chiamante ← chiamato ○ Passaggio x Risultato ■ Avviene un collegamento tra parametro formale e parametro attuale. ● Ingresso e Uscita chiamante chiamato ○ Passaggio x Riferimento ■ Avviene un'associazione tra parametro attuale e parametro formale (aliasing). Le modifiche effettuate sul parametro formale si ripercuotono anche nel parametro attuale. ○ Passaggio x Valore-Risultato ■ Molto simile al Passaggio x Riferimento ma non prevede l'aliasing. ○ Passaggio x Nome ■ Viene sostituito il parametro formale con il parametro attuale per tutte le occorrenze presenti nel corpo della funzione chiamata. Un linguaggio si può dire di ordine superiore se ammette la presenza di funzioni di ordine superiore: queste funzioni sono dette tali se possono prendere come parametro un'atra funzione oppure la possono restituire. Solitamente i linguaggi che prevedono funzioni che restituiscono altre funzioni sono detti funzionali. Con le funzioni di ordine superiore è necessario definire il concetto di deep-binding e shallow- binding, ossia il riferimento all'ambiente non locale per una funzione passata come parametro. Il deep-binding utilizza come ambiente non locale l'ambiente attivo al momento della creazione del legame tra parametro formale e parametro attuale (ovviamente riferito alla funzione). Lo shallow- binding invece fa riferimento all'ambiente attivo durante l'invocazione della funzione passata come parametro. Le eccezioni, in alcuni linguaggi, possono essere controllate definendo un gestore ed incapsulando la porzione di codice nella quale si possa verificare appunto un'eccezione. Nel caso in cui tale eccezione si verifichi, il controllo passa al gestore associato.

Capitolo 8: Strutturare i dati

Un linguaggio di programmazione offre costrutti e meccanismi per strutturare i dati. Ogni linguaggio di programmazione ha un proprio sistema di tipi. ● Insieme dei tipi predefiniti. ● Meccanismi per definire nuovi tipi. ● Meccanismi per il controllo dei tipi (equivalenza, compatibilità, influenza). ● specifiche dei vincoli. I tipi di un linguaggio di programmazione possono essere classificati in base a come i valori possono essere manipolati ● denotabili (possono essere associati ad un nome). ● esprimibili (possono essere il risultato di un'espressione complessa). ● memorizzabili (possono essere memorizzati in una variabile). Se i controlli dei vincoli di tipizzazione avvengono in fase compilativa si ha la tipizzazione statica, nel caso avvenga in fase interpretativa si ha la tipizzazione dinamica.

Riassunto dei tipi di dati. ● Tipi scalari. ○ Tipi predefiniti. ■ Booleani, caratteri, interi, a virgola fissa, complessi, reali e VOID. ○ Enumerazioni (serie di valori) e intervalli. ○ Tipi ordinali (hanno la nozione di precedente e successivo). ● Tipi composti. ○ Record, array, insiemi, puntatori e tipi ricorsivi. Nel caso dei puntatori si presenti un caso del genere: int* p; int* q = (int*)malloc(sizeof(int)); p = q; free(q); si nota che il puntatore p fa riferimento ad una porzione di memoria non allocata, ciò è un errore detto Dangling Reference. I tipi ricorsivi sono tipicamente gli alberi e le liste. Per comparare due tipi di dato si necessita di regole di equivalenza le quali possono essere di tipo opaco (equivalenza per nome) e di tipo trasparente (equivalenza strutturale). Nel primo caso è il nome stesso a definire un nuovo tipo, pertanto due tipi sono equivalenti solo se hanno lo stesso nome. Nel secondo caso invece si analizza la struttura del tipi e quindi: struct A{ int x; int y; }; struct B { int x; int y; }; struct C { int w; int z; }; si ha che A e B sono equivalenti tra loro, ma A non è equivalente con C. Il concetto di compatibilità determina se due tipi sono tra loro compatibili: per esempio è il controllo che avviene nel momento in cui si passa dal parametro attuale al parametro formale e nel caso di un' assegnamento. Siano dati due tipi di dato A e B, la loro compatibilità è dimostrata da: ● A e B sono equivalenti. ● i valori di A sono un sottoinsieme dei valori di B. ● tutte le operazioni ammesse in B lo sono anche in A.

Nel caso dei lock & keys si utilizzano anziché dei puntatori intermedi delle parole generate random o incrementali che fungono da lucchetti e chiavi. Il programma precedente viene schematizzato come segue. p (123) → 1 (123) Viene generata la parola 123 per il puntatore p e 124 per q. q (124) → 2 (124) La chiave viene associata al dato ai quali si riferiscono i puntatori. q (123) → 1 (123) Nel caso di un nuovo riferimento la chiave e il lucchetto sono copiati. q (123) → NULL (0) La chiave e il lucchetto non combaciano poiché al momento della deallocazione il valore della parola viene posto a 0. Il garbage collector si concentra su due fasi principali: il riconoscimento degli oggetti attivi e il possibile riuso degli spazi di memoria non più utilizzati. ● Reference counter (contatore dei riferimenti) ○ Consiste nel tener traccia di quanti puntatori fanno riferimento ad ogni oggetto: quando questo valore è 0 questo oggetto non è più attivo. Questa tecnica è poco efficiente e nel caso di puntatori ricorsivi non la si può adoperare. (p → q; q → p;) ● Mark & Sweep. ○ Vengono marcati inizialmente tutti gli oggetti sull'heap come “non utilizzati”, poi con una visita “a ritroso” dell'heap si marcano tutti gli oggetti attivi. La funzione analoga ripercorre tutti gli oggetti lasciando immutati quelli marcati come attivi e restituendo alla lista libera i rimanenti. Rimane il problema della frammentazione. ● Il rovesciamento dei puntatori. ○ Per la visita “a ritroso” di un grafo come per la Sweep, si fa uso della tecnica del rovesciamento dei puntatori: con questa tecnica si evita il problema dello spreco di memoria poiché si usano solo due puntatori ausiliari. ● Mark & Contact. ○ La funzione di Sweep può essere incorporata con quella di compattazione della memoria. Nonostante il suo uso comporti un costo computazionale notevole, le locazioni di memoria attive e inattive risultano essere contigue e ciò agevola la ri-allocazione. ● Copia (stop & copy). ○ Si suppone di avere due “zone” di memoria distinte (fromspace e tospace): con questa tecnica si ha un costo computazionale ragionevole e si evitano i problemi inerenti alla frammentazione della memoria. Durante l'esecuzione solo una ”zona” risulta allocabile e quando lo spazio a disposizione si esaurisce avviene una copia da una zona all'altra. Vi è inoltre un root-set, ossia una pila di puntatori agli oggetti attivi, e grazie al suo impiego vengono copiati tutti gli oggetti nella nuovo “zona” in modo contiguo ovviamente. Quando la copia termina il ruolo delle due “zone” di memoria viene invertito.

Capitolo 9: Astrarre sui dati

Concettualmente si può dire che una macchina fisica riconosce un solo tipo di dati, ossia le stringhe di bit. Il concetto di astrazione fa si che il programmatore possa adoperare diverse tipologie di dato (int, boolean, char, ecc.). Il tipo di dato astratto, detto ADT, è caratterizzato da: ● nome del tipo. ● rappresentazione e implementazione del tipo. ● insieme dei nomi delle operazioni per manipolare tale tipo di dato. ● per ogni operazione un'implementazione come al secondo punto. ● capsula di protezione che separa il nome del tipo dalla sua implementazione. Se l' ADT serve ad incapsulare un tipo di dato con le sue relative operazioni, un modulo serve per definire un tipo composto da più tipi, descrivendo le varie regole di visibilità grazie alle quali si realizza l'incapsulamento dell'informazione. Il modulo distingue ciò che è pubblico, cioè visibile all'esterno da ciò che è privato, ossia che rimane occultato all'esterno.

Capitolo 10: Il paradigma orientato agli oggetti

Con la programmazione orientata agli oggetti l' ADT viene sostituito da un concetto più complesso che si riassume in quattro punti: ● permettere l'incapsulamento e l'occultamento dell'informazione. ● possibilità di ereditare implementazioni da costrutti analoghi. ● nozione di compatibilità in termini di operazioni ammissibili su diversi tipi. ● selezione di operazioni in funzione del tipo usato. Il costrutto principale della programmazione orientata agli oggetti è appunto l'oggetto, il quale è composto da dati ed operazioni che tramite un'interfaccia è possibile manipolare (salvo regole di visibilità). I dati di un oggetto sono detti campi o variabili d' istanza. Ogni oggetto appartiene almeno ad una classe e la sua istanziazione si presenta in questa forma: Pippo pluto = new Pippo(); dove con Pippo si indica una classe, con pluto il nome dell'oggetto, con new si alloca uno spazio di memoria per tale oggetto e Pippo() rappresenta il costruttore per tale oggetto. I costruttori solitamente hanno la stessa nomenclatura della classe alla quale appartengono e possono essere presenti più volte all'interno del corpo con tipi e numero di parametri differenti (polimorfismo). I metodi ed i campi di una classe possono essere statici, ossia non facenti riferimento ad uno specifico oggetto ma appartenenti direttamente come caratteristica di una classe. Supponiamo di avere un oggetto di classe B che estende un oggetto di classe A: l'interfaccia di A sarà un sottoinsieme dell'interfaccia di B e B si dice sottoclasse di A ed in modo analogo possiamo definire A super-classe di B o ancora B estende A. Una sottoclasse oltre ad ereditare tutti i metodi ed i campi dalla super-classe può aggiungerne di nuovi e sovrascrivere quelli ereditati: ciò è detto overriding. Le regole di visibilità sono quindi di tre tipi e si applicano sia ai campi che ai metodi: ● pubbliche (tutti possono vedere). ● private (solo la classe corrente può vedere). ● protette (solo le classi che estendono la classe corrente può vedere).